changeset 4cad2ca5136b in modules/account:default
details: https://hg.tryton.org/modules/account?cmd=changeset;node=4cad2ca5136b
description:
Add group lines wizard
issue8213
review275091002
diffstat:
CHANGELOG | 2 +
__init__.py | 2 +
doc/index.rst | 3 +
exceptions.py | 8 +
message.xml | 23 ++++
move.py | 198 +++++++++++++++++++++++++++++++++++-
move.xml | 16 ++
tests/scenario_move_line_group.rst | 131 +++++++++++++++++++++++
tests/test_account.py | 5 +
view/move_line_group_start_form.xml | 10 +
view/move_reconciliation_form.xml | 8 +-
view/move_reconciliation_tree.xml | 2 +
12 files changed, 403 insertions(+), 5 deletions(-)
diffs (583 lines):
diff -r cad21d50ab50 -r 4cad2ca5136b CHANGELOG
--- a/CHANGELOG Fri Apr 12 15:59:24 2019 +0200
+++ b/CHANGELOG Fri Apr 12 19:43:53 2019 +0200
@@ -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
diff -r cad21d50ab50 -r 4cad2ca5136b __init__.py
--- a/__init__.py Fri Apr 12 15:59:24 2019 +0200
+++ b/__init__.py Fri Apr 12 19:43:53 2019 +0200
@@ -55,6 +55,7 @@
ReconcileLinesWriteOff,
ReconcileShow,
CancelMovesDefault,
+ GroupLinesStart,
PrintGeneralJournalStart,
tax.TaxGroup,
tax.TaxCodeTemplate,
@@ -93,6 +94,7 @@
UnreconcileLines,
Reconcile,
CancelMoves,
+ GroupLines,
PrintGeneralJournal,
CreateMove,
tax.OpenChartTaxCode,
diff -r cad21d50ab50 -r 4cad2ca5136b doc/index.rst
--- a/doc/index.rst Fri Apr 12 15:59:24 2019 +0200
+++ b/doc/index.rst Fri Apr 12 19:43:53 2019 +0200
@@ -177,6 +177,9 @@
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
********
diff -r cad21d50ab50 -r 4cad2ca5136b exceptions.py
--- a/exceptions.py Fri Apr 12 15:59:24 2019 +0200
+++ b/exceptions.py Fri Apr 12 19:43:53 2019 +0200
@@ -75,3 +75,11 @@
class ReconciliationError(ValidationError):
pass
+
+
+class DeleteDelegatedWarning(UserWarning):
+ pass
+
+
+class GroupLineError(UserError):
+ pass
diff -r cad21d50ab50 -r 4cad2ca5136b message.xml
--- a/message.xml Fri Apr 12 15:59:24 2019 +0200
+++ b/message.xml Fri Apr 12 19:43:53 2019 +0200
@@ -124,6 +124,10 @@
<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 @@
<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>
diff -r cad21d50ab50 -r 4cad2ca5136b move.py
--- a/move.py Fri Apr 12 15:59:24 2019 +0200
+++ b/move.py Fri Apr 12 19:43:53 2019 +0200
@@ -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.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 @@
'UnreconcileLines',
'Reconcile', 'ReconcileShow',
'CancelMoves', 'CancelMovesDefault',
+ 'GroupLines', 'GroupLinesStart',
'PrintGeneralJournalStart', 'PrintGeneralJournal', 'GeneralJournal']
_MOVE_STATES = {
@@ -430,6 +431,9 @@
'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):
@@ -480,6 +484,21 @@
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')
for reconciliation in reconciliations:
@@ -631,6 +650,9 @@
], '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 @@
@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 @@
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 @@
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'
diff -r cad21d50ab50 -r 4cad2ca5136b move.xml
--- a/move.xml Fri Apr 12 15:59:24 2019 +0200
+++ b/move.xml Fri Apr 12 19:43:53 2019 +0200
@@ -392,6 +392,22 @@
<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>
diff -r cad21d50ab50 -r 4cad2ca5136b tests/scenario_move_line_group.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_move_line_group.rst Fri Apr 12 19:43:53 2019 +0200
@@ -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
diff -r cad21d50ab50 -r 4cad2ca5136b tests/test_account.py
--- a/tests/test_account.py Fri Apr 12 15:59:24 2019 +0200
+++ b/tests/test_account.py Fri Apr 12 19:43:53 2019 +0200
@@ -1322,6 +1322,11 @@
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',
checker=doctest_checker,
diff -r cad21d50ab50 -r 4cad2ca5136b view/move_line_group_start_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/move_line_group_start_form.xml Fri Apr 12 19:43:53 2019 +0200
@@ -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>
diff -r cad21d50ab50 -r 4cad2ca5136b view/move_reconciliation_form.xml
--- a/view/move_reconciliation_form.xml Fri Apr 12 15:59:24 2019 +0200
+++ b/view/move_reconciliation_form.xml Fri Apr 12 19:43:53 2019 +0200
@@ -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>
diff -r cad21d50ab50 -r 4cad2ca5136b view/move_reconciliation_tree.xml
--- a/view/move_reconciliation_tree.xml Fri Apr 12 15:59:24 2019 +0200
+++ b/view/move_reconciliation_tree.xml Fri Apr 12 19:43:53 2019 +0200
@@ -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>