Add group lines wizard
issue8213 review275091002
This commit is contained in:
1 parent
c9fcc894ce
commit
aeabe70531
12 files changed
+403
-5
No files matched your search
@@ -1,3 +1,5 @@
|
||||
* Add group lines wizard
|
||||
* Manage delegate reconciliation
|
||||
* Remove starting wildcard when searching on codes and numbers
|
||||
* Remove display balance
|
||||
* Merge account kind and account type
|
||||
|
||||
@@ -55,6 +55,7 @@ def register():
|
||||
ReconcileLinesWriteOff,
|
||||
ReconcileShow,
|
||||
CancelMovesDefault,
|
||||
GroupLinesStart,
|
||||
PrintGeneralJournalStart,
|
||||
tax.TaxGroup,
|
||||
tax.TaxCodeTemplate,
|
||||
@@ -93,6 +94,7 @@ def register():
|
||||
UnreconcileLines,
|
||||
Reconcile,
|
||||
CancelMoves,
|
||||
GroupLines,
|
||||
PrintGeneralJournal,
|
||||
CreateMove,
|
||||
tax.OpenChartTaxCode,
|
||||
|
||||
@@ -177,6 +177,9 @@ possible. The configuration `reconciliation_chunk` in `account` section allow
|
||||
to define the length of lines that is allowed to search for proposal. The
|
||||
default is 10.
|
||||
|
||||
The *Group Lines* wizard allow to group payable and/or receivable lines in
|
||||
order to have a unique payable or receivable line.
|
||||
|
||||
|
||||
Tax Code
|
||||
********
|
||||
|
||||
@@ -75,3 +75,11 @@ class CancelWarning(UserWarning):
|
||||
|
||||
class ReconciliationError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteDelegatedWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
class GroupLineError(UserError):
|
||||
pass
|
||||
+23
@@ -124,6 +124,10 @@ Use the current period to cancel the move?</field>
|
||||
<record model="ir.message" id="msg_write_reconciliation">
|
||||
<field name="text">You cannot modify any reconciliation.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_reconciliation_delete_delegated">
|
||||
<field name="text">The reconciliation "%(reconciliation)s" is delegated to the line "%(line)s".
|
||||
You may need to cancel its move "%(move)s".</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_reconciliation_line_not_valid">
|
||||
<field name="text">You cannot reconcile non-valid line "%(line)s".</field>
|
||||
</record>
|
||||
@@ -167,5 +171,24 @@ Use the current period to cancel the move?</field>
|
||||
<record model="ir.message" id="msg_line_party_set">
|
||||
<field name="text">To create a line on account "%(account)s", you must remove the party on line "%(line)s".</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.message" id="msg_group_line">
|
||||
<field name="text">Line "%(line)s" can not be grouped.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_group_line_single">
|
||||
<field name="text">You cannot group a single line.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_group_line_many_parties">
|
||||
<field name="text">You cannot group lines with different parties.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_group_line_same_company">
|
||||
<field name="text">You cannot group lines of different companies.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_group_line_same_second_currency">
|
||||
<field name="text">You cannot group lines of different second currencies.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_group_line_maximum_account">
|
||||
<field name="text">You cannot group lines from more than 2 different accounts.</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -2,7 +2,7 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
from itertools import groupby, combinations
|
||||
from itertools import groupby, combinations, chain
|
||||
from operator import itemgetter
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -26,7 +26,7 @@ from trytond.tools import reduce_ids, grouped_slice
|
||||
from trytond.config import config
|
||||
|
||||
from .exceptions import (PostError, MoveDatesError, CancelWarning,
|
||||
ReconciliationError)
|
||||
ReconciliationError, DeleteDelegatedWarning, GroupLineError)
|
||||
|
||||
__all__ = ['Move', 'Reconciliation', 'Line', 'WriteOff', 'OpenJournalAsk',
|
||||
'OpenJournal', 'OpenAccount',
|
||||
@@ -34,6 +34,7 @@ __all__ = ['Move', 'Reconciliation', 'Line', 'WriteOff', 'OpenJournalAsk',
|
||||
'UnreconcileLines',
|
||||
'Reconcile', 'ReconcileShow',
|
||||
'CancelMoves', 'CancelMovesDefault',
|
||||
'GroupLines', 'GroupLinesStart',
|
||||
'PrintGeneralJournalStart', 'PrintGeneralJournal', 'GeneralJournal']
|
||||
|
||||
_MOVE_STATES = {
|
||||
@@ -430,6 +431,9 @@ class Reconciliation(ModelSQL, ModelView):
|
||||
'Lines')
|
||||
date = fields.Date('Date', required=True, select=True,
|
||||
help='Highest date of the reconciled lines')
|
||||
delegate_to = fields.Many2One(
|
||||
'account.move.line', "Delegate To", ondelete="RESTRICT", select=True,
|
||||
help="The line to which the reconciliation status is delegated.")
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
@@ -479,6 +483,21 @@ class Reconciliation(ModelSQL, ModelView):
|
||||
super(Reconciliation, cls).validate(reconciliations)
|
||||
cls.check_lines(reconciliations)
|
||||
|
||||
@classmethod
|
||||
def delete(cls, reconciliations):
|
||||
pool = Pool()
|
||||
Warning = pool.get('res.user.warning')
|
||||
for reconciliation in reconciliations:
|
||||
if reconciliation.delegate_to:
|
||||
key = '%s.delete.delegated' % reconciliation
|
||||
if Warning.check(key):
|
||||
raise DeleteDelegatedWarning(key,
|
||||
gettext('account.msg_reconciliation_delete_delegated',
|
||||
reconciliation=reconciliation.rec_name,
|
||||
line=reconciliation.delegate_to.rec_name,
|
||||
move=reconciliation.delegate_to.move.rec_name))
|
||||
super().delete(reconciliations)
|
||||
|
||||
@classmethod
|
||||
def check_lines(cls, reconciliations):
|
||||
Lang = Pool().get('ir.lang')
|
||||
@@ -631,6 +650,9 @@ class Line(ModelSQL, ModelView):
|
||||
], 'State', readonly=True, required=True, select=True)
|
||||
reconciliation = fields.Many2One('account.move.reconciliation',
|
||||
'Reconciliation', readonly=True, ondelete='SET NULL', select=True)
|
||||
reconciliations_delegated = fields.One2Many(
|
||||
'account.move.reconciliation', 'delegate_to',
|
||||
"Reconciliations Delegated", readonly=True)
|
||||
tax_lines = fields.One2Many('account.tax.line', 'move_line', 'Tax Lines')
|
||||
move_state = fields.Function(fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
@@ -1158,13 +1180,15 @@ class Line(ModelSQL, ModelView):
|
||||
|
||||
@classmethod
|
||||
def reconcile(
|
||||
cls, *lines_list, date=None, writeoff=None, description=None):
|
||||
cls, *lines_list, date=None, writeoff=None, description=None,
|
||||
delegate_to=None):
|
||||
"""
|
||||
Reconcile each list of lines together.
|
||||
The writeoff keys are: date, method and description.
|
||||
"""
|
||||
pool = Pool()
|
||||
Reconciliation = pool.get('account.move.reconciliation')
|
||||
delegate_to = delegate_to.id if delegate_to else None
|
||||
|
||||
reconciliations = []
|
||||
for lines in lines_list:
|
||||
@@ -1200,6 +1224,7 @@ class Line(ModelSQL, ModelView):
|
||||
reconciliations.append({
|
||||
'lines': [('add', [x.id for x in lines])],
|
||||
'date': max(l.date for l in lines),
|
||||
'delegate_to': delegate_to,
|
||||
})
|
||||
return Reconciliation.create(reconciliations)
|
||||
|
||||
@@ -1782,6 +1807,173 @@ class CancelMovesDefault(ModelView):
|
||||
description = fields.Char('Description')
|
||||
|
||||
|
||||
class GroupLines(Wizard):
|
||||
"Group Lines"
|
||||
__name__ = 'account.move.line.group'
|
||||
start = StateView('account.move.line.group.start',
|
||||
'account.move_line_group_start_view_form', [
|
||||
Button("Cancel", 'end', 'tryton-cancel'),
|
||||
Button("Group", 'group', 'tryton-ok', default=True),
|
||||
])
|
||||
group = StateTransition()
|
||||
|
||||
def transition_group(self):
|
||||
pool = Pool()
|
||||
Line = pool.get('account.move.line')
|
||||
|
||||
lines = Line.browse(Transaction().context['active_ids'])
|
||||
grouping = self.grouping(lines)
|
||||
|
||||
move, balance_line = self.get_move(lines, grouping)
|
||||
move.save()
|
||||
|
||||
to_reconcile = defaultdict(list)
|
||||
for line in chain(lines, move.lines):
|
||||
if line.account.reconcile:
|
||||
to_reconcile[line.account].append(line)
|
||||
|
||||
balance_line.move = move
|
||||
balance_line.save()
|
||||
|
||||
for lines in to_reconcile.values():
|
||||
Line.reconcile(lines, delegate_to=balance_line)
|
||||
|
||||
return 'end'
|
||||
|
||||
@classmethod
|
||||
def grouping(cls, lines):
|
||||
if len(lines) == 1:
|
||||
raise GroupLineError(gettext('account.msg_group_line_single'))
|
||||
companies = set()
|
||||
parties = set()
|
||||
accounts = set()
|
||||
second_currencies = set()
|
||||
for line in lines:
|
||||
if not cls.allow_grouping(line.account):
|
||||
raise GroupLineError(gettext('account.msg_group_line'))
|
||||
companies.add(line.move.company)
|
||||
parties.add(line.party)
|
||||
accounts.add(line.account)
|
||||
second_currencies.add(line.second_currency)
|
||||
try:
|
||||
company, = companies
|
||||
except ValueError:
|
||||
raise GroupLineError(
|
||||
gettext('account.msg_group_line_same_company'))
|
||||
try:
|
||||
party, = parties
|
||||
except ValueError:
|
||||
raise GroupLineError(
|
||||
gettext('account.msg_group_line_many_parties'))
|
||||
try:
|
||||
second_currency, = second_currencies
|
||||
except ValueError:
|
||||
raise GroupLineError(
|
||||
gettext('account.msg_group_line_same_second_currency'))
|
||||
if len(accounts) > 2:
|
||||
raise GroupLineError(
|
||||
gettext('account.msg_group_line_maximum_account'))
|
||||
return {
|
||||
'company': company,
|
||||
'party': party,
|
||||
'second_currency': second_currency,
|
||||
'accounts': accounts,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def allow_grouping(cls, account):
|
||||
return account.type.payable or account.type.receivable
|
||||
|
||||
def get_move(self, lines, grouping, date=None):
|
||||
pool = Pool()
|
||||
Date = pool.get('ir.date')
|
||||
Move = pool.get('account.move')
|
||||
Period = pool.get('account.period')
|
||||
Line = pool.get('account.move.line')
|
||||
|
||||
if not date:
|
||||
date = Date.today()
|
||||
period = Period.find(grouping['company'].id, date=date)
|
||||
|
||||
move = Move()
|
||||
move.date = date
|
||||
move.period = period
|
||||
move.journal = self.start.journal
|
||||
move.description = self.start.description
|
||||
|
||||
accounts = {a: 0 for a in grouping['accounts']}
|
||||
amount_second_currency = 0
|
||||
maturity_dates = {a: None for a in grouping['accounts']}
|
||||
|
||||
counterpart_lines = []
|
||||
for line in lines:
|
||||
if maturity_dates[line.account]:
|
||||
maturity_dates[line.account] = min(
|
||||
maturity_dates[line.account], line.maturity_date)
|
||||
else:
|
||||
maturity_dates[line.account] = line.maturity_date
|
||||
line = self._counterpart_line(line)
|
||||
accounts[line.account] += line.debit - line.credit
|
||||
if line.amount_second_currency:
|
||||
amount_second_currency += line.amount_second_currency
|
||||
counterpart_lines.append(line)
|
||||
move.lines = counterpart_lines
|
||||
|
||||
balance_line = None
|
||||
if len(accounts) == 1:
|
||||
account, = grouping['accounts']
|
||||
amount = -accounts[account]
|
||||
if amount:
|
||||
balance_line = Line()
|
||||
balance_line.account = account
|
||||
else:
|
||||
first, second = grouping['accounts']
|
||||
if accounts[first] != accounts[second]:
|
||||
balance_line = Line()
|
||||
amount = -(accounts[first] + accounts[second])
|
||||
if abs(accounts[first]) > abs(accounts[second]):
|
||||
balance_line.account = first
|
||||
else:
|
||||
balance_line.account = second
|
||||
if balance_line:
|
||||
if balance_line.account.party_required:
|
||||
balance_line.party = grouping['party']
|
||||
if amount > 0:
|
||||
balance_line.debit, balance_line.credit = amount, 0
|
||||
else:
|
||||
balance_line.debit, balance_line.credit = 0, -amount
|
||||
if grouping['second_currency']:
|
||||
balance_line.second_currency = grouping['second_currency']
|
||||
balance_line.amount_second_currency = (
|
||||
-amount_second_currency)
|
||||
balance_line.maturity_date = maturity_dates[balance_line.account]
|
||||
return move, balance_line
|
||||
|
||||
def _counterpart_line(self, line):
|
||||
pool = Pool()
|
||||
Line = pool.get('account.move.line')
|
||||
|
||||
counterpart = Line()
|
||||
counterpart.account = line.account
|
||||
counterpart.debit = line.credit
|
||||
counterpart.credit = line.debit
|
||||
counterpart.party = line.party
|
||||
counterpart.second_currency = line.second_currency
|
||||
if line.second_currency:
|
||||
counterpart.amount_second_currency = -line.amount_second_currency
|
||||
else:
|
||||
counterpart.amount_second_currency = None
|
||||
return counterpart
|
||||
|
||||
|
||||
class GroupLinesStart(ModelView):
|
||||
"Group Lines"
|
||||
__name__ = 'account.move.line.group.start'
|
||||
|
||||
journal = fields.Many2One('account.journal', "Journal", required=True)
|
||||
description = fields.Char("Description")
|
||||
|
||||
|
||||
class PrintGeneralJournalStart(ModelView):
|
||||
'Print General Journal'
|
||||
__name__ = 'account.move.print_general_journal.start'
|
||||
|
||||
@@ -392,6 +392,22 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="act_group_lines_wizard">
|
||||
<field name="name">Group Lines</field>
|
||||
<field name="wiz_name">account.move.line.group</field>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="act_group_lines_keyword1">
|
||||
<field name="keyword">form_action</field>
|
||||
<field name="model">account.move.line,-1</field>
|
||||
<field name="action" ref="act_group_lines_wizard"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="move_line_group_start_view_form">
|
||||
<field name="model">account.move.line.group.start</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">move_line_group_start_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="print_general_journal_start_view_form">
|
||||
<field name="model">account.move.print_general_journal.start</field>
|
||||
<field name="type">form</field>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
====================
|
||||
Group Lines Scenario
|
||||
====================
|
||||
|
||||
Imports::
|
||||
|
||||
>>> import datetime as dt
|
||||
>>> from decimal import Decimal
|
||||
>>> from proteus import Model, Wizard
|
||||
>>> from trytond.tests.tools import activate_modules
|
||||
>>> from trytond.modules.currency.tests.tools import get_currency
|
||||
>>> from trytond.modules.company.tests.tools import create_company, \
|
||||
... get_company
|
||||
>>> from trytond.modules.account.tests.tools import create_fiscalyear, \
|
||||
... create_chart, get_accounts
|
||||
|
||||
Install account::
|
||||
|
||||
>>> config = activate_modules('account')
|
||||
|
||||
Create company::
|
||||
|
||||
>>> usd = get_currency('USD')
|
||||
>>> eur = get_currency('EUR')
|
||||
>>> _ = create_company(currency=usd)
|
||||
>>> company = get_company()
|
||||
|
||||
Create fiscal year::
|
||||
|
||||
>>> fiscalyear = create_fiscalyear(company)
|
||||
>>> fiscalyear.click('create_period')
|
||||
>>> period = fiscalyear.periods[0]
|
||||
|
||||
Create chart of accounts::
|
||||
|
||||
>>> _ = create_chart(company)
|
||||
>>> accounts = get_accounts(company)
|
||||
>>> payable = accounts['payable']
|
||||
>>> receivable = accounts['receivable']
|
||||
>>> revenue = accounts['revenue']
|
||||
>>> expense = accounts['expense']
|
||||
|
||||
Create parties::
|
||||
|
||||
>>> Party = Model.get('party.party')
|
||||
>>> party = Party(name="Party")
|
||||
>>> party.save()
|
||||
|
||||
Create Lines to group::
|
||||
|
||||
>>> Journal = Model.get('account.journal')
|
||||
>>> Move = Model.get('account.move')
|
||||
>>> journal_revenue, = Journal.find([
|
||||
... ('code', '=', 'REV'),
|
||||
... ])
|
||||
>>> journal_expense, = Journal.find([
|
||||
... ('code', '=', 'EXP'),
|
||||
... ])
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.journal = journal_revenue
|
||||
>>> move.date = period.start_date
|
||||
>>> line = move.lines.new()
|
||||
>>> line.account = revenue
|
||||
>>> line.credit = Decimal(100)
|
||||
>>> line = move.lines.new()
|
||||
>>> line.account = receivable
|
||||
>>> line.party = party
|
||||
>>> line.debit = Decimal(100)
|
||||
>>> line.second_currency = eur
|
||||
>>> line.amount_second_currency = Decimal(90)
|
||||
>>> line.maturity_date = period.start_date + dt.timedelta(days=2)
|
||||
>>> move.save()
|
||||
|
||||
>>> move = Move()
|
||||
>>> move.journal = journal_expense
|
||||
>>> move.date = period.start_date
|
||||
>>> line = move.lines.new()
|
||||
>>> line.account = expense
|
||||
>>> line.debit = Decimal(50)
|
||||
>>> line = move.lines.new()
|
||||
>>> line.account = payable
|
||||
>>> line.party = party
|
||||
>>> line.credit = Decimal(50)
|
||||
>>> line.second_currency = eur
|
||||
>>> line.amount_second_currency = Decimal(45)
|
||||
>>> line.maturity_date = period.start_date + dt.timedelta(days=4)
|
||||
>>> move.save()
|
||||
|
||||
>>> receivable.reload()
|
||||
>>> receivable.balance, receivable.amount_second_currency
|
||||
(Decimal('100.00'), Decimal('90.00'))
|
||||
>>> payable.reload()
|
||||
>>> payable.balance, payable.amount_second_currency
|
||||
(Decimal('-50.00'), Decimal('-45.00'))
|
||||
|
||||
Group lines::
|
||||
|
||||
>>> journal_cash, = Journal.find([
|
||||
... ('code', '=', 'CASH'),
|
||||
... ])
|
||||
|
||||
>>> Line = Model.get('account.move.line')
|
||||
>>> lines = Line.find([('account', 'in', [payable.id, receivable.id])])
|
||||
>>> len(lines)
|
||||
2
|
||||
>>> group = Wizard('account.move.line.group', lines)
|
||||
>>> group.form.journal = journal_cash
|
||||
>>> group.form.description = "Group lines"
|
||||
>>> group.execute('group')
|
||||
|
||||
>>> receivable.reload()
|
||||
>>> receivable.balance, receivable.amount_second_currency
|
||||
(Decimal('50.00'), Decimal('45.00'))
|
||||
>>> payable.reload()
|
||||
>>> payable.balance, payable.amount_second_currency
|
||||
(Decimal('0.00'), Decimal('0.00'))
|
||||
|
||||
>>> Reconciliation = Model.get('account.move.reconciliation')
|
||||
>>> reconciliations = Reconciliation.find([])
|
||||
>>> len(reconciliations)
|
||||
2
|
||||
>>> all(r.delegate_to for r in reconciliations)
|
||||
True
|
||||
>>> delegate_to = reconciliations[0].delegate_to
|
||||
>>> delegate_to.account == receivable
|
||||
True
|
||||
>>> delegate_to.debit
|
||||
Decimal('50')
|
||||
>>> delegate_to.maturity_date == period.start_date + dt.timedelta(days=2)
|
||||
True
|
||||
@@ -1321,6 +1321,11 @@ def suite():
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
suite.addTests(doctest.DocFileSuite(
|
||||
'scenario_move_line_group.rst',
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
checker=doctest_checker,
|
||||
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
|
||||
suite.addTests(doctest.DocFileSuite(
|
||||
'scenario_move_template.rst',
|
||||
tearDown=doctest_teardown, encoding='utf-8',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="journal"/>
|
||||
<field name="journal"/>
|
||||
<newline/>
|
||||
<label name="description"/>
|
||||
<field name="description" colspan="3"/>
|
||||
</form>
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form col="2">
|
||||
<form>
|
||||
<label name="name"/>
|
||||
<field name="name"/>
|
||||
<field name="lines" colspan="2"/>
|
||||
<label name="date"/>
|
||||
<field name="date"/>
|
||||
<label name="delegate_to"/>
|
||||
<field name="delegate_to"/>
|
||||
<field name="lines" colspan="4"/>
|
||||
</form>
|
||||
@@ -3,4 +3,6 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="delegate_to"/>
|
||||
</tree>
|
||||
Reference in new issue
Block a user