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>

Reply via email to