changeset 81860b110704 in modules/account_invoice:default
details: 
https://hg.tryton.org/modules/account_invoice?cmd=changeset&node=81860b110704
description:
        Support alternative payees on invoice

        issue11646
        review435641003
diffstat:

 CHANGELOG                                    |    2 +
 __init__.py                                  |    3 +
 account.py                                   |   21 ++++
 invoice.py                                   |  130 +++++++++++++++++++++++--
 invoice.xml                                  |   18 +++
 party.py                                     |    6 +-
 tests/scenario_invoice_alternative_payee.rst |  131 +++++++++++++++++++++++++++
 view/invoice_form.xml                        |    7 +-
 view/move_line_list_payment.xml              |    1 +
 view/move_line_list_to_pay.xml               |    1 +
 view/pay_start_form.xml                      |    6 +-
 11 files changed, 308 insertions(+), 18 deletions(-)

diffs (598 lines):

diff -r 9a2b0ae5129e -r 81860b110704 CHANGELOG
--- a/CHANGELOG Mon Aug 08 22:25:23 2022 +0200
+++ b/CHANGELOG Sun Sep 11 17:46:09 2022 +0200
@@ -1,3 +1,5 @@
+* Support alternative payees on invoice
+
 Version 6.4.0 - 2022-05-02
 * Bug fixes (see mercurial logs for details)
 * Use invoice date to enforce sequence on customer invoice
diff -r 9a2b0ae5129e -r 81860b110704 __init__.py
--- a/__init__.py       Mon Aug 08 22:25:23 2022 +0200
+++ b/__init__.py       Sun Sep 11 17:46:09 2022 +0200
@@ -15,6 +15,7 @@
         payment_term.TestPaymentTermViewResult,
         invoice.Invoice,
         invoice.InvoiceAdditionalMove,
+        invoice.AlternativePayee,
         invoice.InvoicePaymentLine,
         invoice.InvoiceLine,
         invoice.InvoiceLineTax,
@@ -44,10 +45,12 @@
         invoice.PayInvoice,
         invoice.CreditInvoice,
         invoice.RescheduleLinesToPay,
+        invoice.DelegateLinesToPay,
         party.Replace,
         party.Erase,
         account.RenewFiscalYear,
         account.RescheduleLines,
+        account.DelegateLines,
         module='account_invoice', type_='wizard')
     Pool.register(
         invoice.InvoiceReport,
diff -r 9a2b0ae5129e -r 81860b110704 account.py
--- a/account.py        Mon Aug 08 22:25:23 2022 +0200
+++ b/account.py        Sun Sep 11 17:46:09 2022 +0200
@@ -440,3 +440,24 @@
                 'additional_moves': [('add', [move.id])],
                 })
         return move, balance_line
+
+
+class DelegateLines(metaclass=PoolMeta):
+    __name__ = 'account.move.line.delegate'
+
+    @classmethod
+    def delegate_lines(cls, lines, party, journal, date=None):
+        pool = Pool()
+        Invoice = pool.get('account.invoice')
+        move = super().delegate_lines(lines, party, journal, date=None)
+
+        move_ids = list({l.move.id for l in lines})
+        invoices = Invoice.search(['OR',
+                ('move', 'in', move_ids),
+                ('additional_moves', 'in', move_ids),
+                ])
+        Invoice.write(invoices, {
+                'alternative_payees': [('add', [party.id])],
+                'additional_moves': [('add', [move.id])],
+                })
+        return move
diff -r 9a2b0ae5129e -r 81860b110704 invoice.py
--- a/invoice.py        Mon Aug 08 22:25:23 2022 +0200
+++ b/invoice.py        Sun Sep 11 17:46:09 2022 +0200
@@ -166,6 +166,14 @@
             })
     payment_term = fields.Many2One('account.invoice.payment_term',
         'Payment Term', states=_states)
+    alternative_payees = fields.Many2Many(
+        'account.invoice.alternative_payee', 'invoice', 'party',
+        "Alternative Payee", states=_states,
+        size=If(~Eval('move'), 1, None),
+        context={
+            'company': Eval('company', -1),
+            },
+        depends=['company'])
     lines = fields.One2Many('account.invoice.line', 'invoice', 'Lines',
         domain=[
             ('company', '=', Eval('company', -1)),
@@ -210,7 +218,10 @@
         'invoice', 'line', string='Payment Lines',
         domain=[
             ('account', '=', Eval('account', -1)),
-            ('party', 'in', [None, Eval('party', -1)]),
+            ['OR',
+                ('party', 'in', [None, Eval('party', -1)]),
+                ('party', 'in', Eval('alternative_payees', [])),
+                ],
             ['OR',
                 ('invoice_payment', '=', None),
                 ('invoice_payment', '=', Eval('id', -1)),
@@ -253,9 +264,10 @@
         super(Invoice, cls).__setup__()
         cls.create_date.select = True
         cls._check_modify_exclude = {
-            'state', 'payment_lines', 'move', 'cancel_move',
-            'additional_moves', 'invoice_report_cache',
-            'invoice_report_format', 'lines'}
+            'state', 'alternative_payees', 'payment_lines',
+            'move', 'cancel_move', 'additional_moves',
+            'invoice_report_cache', 'invoice_report_format',
+            'lines'}
         cls._order = [
             ('number', 'DESC NULLS FIRST'),
             ('id', 'DESC'),
@@ -324,6 +336,11 @@
                         ~Eval('lines_to_pay') | Eval('reconciled', False)),
                     'depends': ['lines_to_pay', 'reconciled'],
                     },
+                'delegate_lines_to_pay': {
+                    'invisible': (
+                        ~Eval('lines_to_pay') | Eval('reconciled', False)),
+                    'depends': ['lines_to_pay', 'reconciled'],
+                    },
                 'process': {
                     'invisible': ~Eval('state').in_(
                         ['posted', 'paid']),
@@ -977,7 +994,10 @@
                     line.debit - line.credit))
         line.account = self.account
         if self.account.party_required:
-            line.party = self.party
+            if self.alternative_payees:
+                line.party, = self.alternative_payees
+            else:
+                line.party = self.party
         line.maturity_date = date
         line.description = self.description
         return line
@@ -1307,14 +1327,19 @@
                     '.msg_invoice_payment_lines_greater_amount',
                     invoice=self.rec_name))
 
-    def get_reconcile_lines_for_amount(self, amount):
+    def get_reconcile_lines_for_amount(self, amount, party=None):
         '''
         Return list of lines and the remainder to make reconciliation.
         '''
         Result = namedtuple('Result', ['lines', 'remainder'])
 
-        lines = [l for l in self.payment_lines + self.lines_to_pay
-            if not l.reconciliation]
+        if party is None:
+            party = self.party
+
+        lines = [
+            l for l in self.payment_lines + self.lines_to_pay
+            if not l.reconciliation
+            and (not self.account.party_required or l.party == party)]
 
         best = Result([], self.total_amount)
         for n in range(len(lines), 0, -1):
@@ -1330,7 +1355,7 @@
 
     def pay_invoice(self, amount, payment_method, date, description=None,
             amount_second_currency=None, second_currency=None, overpayment=0,
-            overpayment_second_currency=0):
+            overpayment_second_currency=0, party=None):
         '''
         Adds a payment of amount to an invoice using the journal, date and
         description.
@@ -1343,6 +1368,9 @@
         Line = pool.get('account.move.line')
         Period = pool.get('account.period')
 
+        if party is None:
+            party = self.party
+
         pay_line = Line(account=self.account)
         counterpart_line = Line()
         lines = [pay_line, counterpart_line]
@@ -1396,7 +1424,7 @@
 
         for line in lines:
             if line.account.party_required:
-                line.party = self.party
+                line.party = party
 
         period_id = Period.find(self.company.id, date=date)
 
@@ -1516,6 +1544,9 @@
                 moves.append(invoice.move)
             if invoice.additional_moves:
                 moves.extend(invoice.additional_moves)
+            if len(invoice.alternative_payees) > 1:
+                invoice.alternative_payees = []
+        cls.save(invoices)
         if moves:
             Move.delete(moves)
 
@@ -1686,6 +1717,12 @@
         pass
 
     @classmethod
+    @ModelView.button_action(
+        'account_invoice.act_delegate_lines_to_pay_wizard')
+    def delegate_lines_to_pay(cls, invoices):
+        pass
+
+    @classmethod
     @ModelView.button
     def process(cls, invoices):
         paid = []
@@ -1793,6 +1830,17 @@
         'account.move', "Additional Move", ondelete='CASCADE')
 
 
+class AlternativePayee(ModelSQL):
+    "Invoice Alternative Payee"
+    __name__ = 'account.invoice.alternative_payee'
+
+    invoice = fields.Many2One(
+        'account.invoice', "Invoice",
+        ondelete='CASCADE', required=True, select=True)
+    party = fields.Many2One(
+        'party.party', "Payee", ondelete='RESTRICT', required=True)
+
+
 class InvoicePaymentLine(ModelSQL):
     'Invoice - Payment Line'
     __name__ = 'account.invoice-account.move.line'
@@ -1803,12 +1851,19 @@
         'get_invoice')
     invoice_party = fields.Function(
         fields.Many2One('party.party', "Invoice Party"), 'get_invoice')
+    invoice_alternative_payees = fields.Function(
+        fields.Many2Many(
+            'party.party', None, None, "Invoice Alternative Payees"),
+        'get_invoice')
     line = fields.Many2One(
         'account.move.line', 'Payment Line', ondelete='CASCADE',
         select=True, required=True,
         domain=[
             ('account', '=', Eval('invoice_account')),
-            ('party', '=', Eval('invoice_party')),
+            ['OR',
+                ('party', '=', Eval('invoice_party', -1)),
+                ('party', 'in', Eval('invoice_alternative_payees', [])),
+                ],
             ])
 
     @classmethod
@@ -1827,6 +1882,7 @@
             result[name] = {}
         invoice_account = 'invoice_account' in result
         invoice_party = 'invoice_party' in result
+        invoice_alternative_payees = 'invoice_alternative_payees' in result
         for record in records:
             if invoice_account:
                 result['invoice_account'][record.id] = (
@@ -1837,6 +1893,9 @@
                 else:
                     party = None
                 result['invoice_party'][record.id] = party
+            if invoice_alternative_payees:
+                result['invoice_alternative_payees'][record.id] = [
+                    p.id for p in record.invoice.alternative_payees]
         return result
 
 
@@ -2976,6 +3035,22 @@
 class PayInvoiceStart(ModelView):
     'Pay Invoice'
     __name__ = 'account.invoice.pay.start'
+
+    payee = fields.Many2One(
+        'party.party', "Payee", required=True,
+        domain=[
+            ('id', 'in', Eval('payees', []))
+            ],
+        context={
+            'company': Eval('company', -1),
+            },
+        depends=['company'])
+    payees = fields.Many2Many(
+        'party.party', None, None, "Payees", readonly=True,
+        context={
+            'company': Eval('company', -1),
+            },
+        depends=['company'])
     amount = Monetary(
         "Amount", currency='currency', digits='currency', required=True)
     currency = fields.Many2One('currency.currency', 'Currency', required=True)
@@ -3104,11 +3179,21 @@
     def get_reconcile_lines_for_amount(self, invoice, amount):
         if invoice.type == 'in':
             amount *= -1
-        return invoice.get_reconcile_lines_for_amount(amount)
+        return invoice.get_reconcile_lines_for_amount(
+            amount, party=self.start.payee)
 
     def default_start(self, fields):
         default = {}
         invoice = self.record
+        if not invoice.alternative_payees:
+            default['payee'] = invoice.party.id
+        else:
+            try:
+                default['payee'], = invoice.alternative_payees
+            except ValueError:
+                pass
+        default['payees'] = (
+            [invoice.party.id] + [p.id for p in invoice.alternative_payees])
         default['company'] = invoice.company.id
         default['currency'] = invoice.currency.id
         default['amount'] = (invoice.amount_to_pay_today
@@ -3230,14 +3315,17 @@
             lines = invoice.pay_invoice(amount,
                 self.start.payment_method, self.start.date,
                 self.start.description, amount_second_currency,
-                second_currency, overpayment, overpayment_second_currency)
+                second_currency, overpayment, overpayment_second_currency,
+                party=self.start.payee)
 
         if remainder:
             if self.ask.type != 'partial':
                 to_reconcile = {l for l in self.ask.lines}
                 to_reconcile.update(
                     l for l in invoice.payment_lines
-                    if not l.reconciliation)
+                    if not l.reconciliation
+                    and (not invoice.account.party_required
+                        or l.party == self.start.payee))
                 if self.ask.type == 'writeoff':
                     to_reconcile.update(lines)
                 if to_reconcile:
@@ -3318,3 +3406,17 @@
                 if not l.reconciliation],
             'model': 'account.move.line',
             }
+
+
+class DelegateLinesToPay(Wizard):
+    "Delegate Lines to Pay"
+    __name__ = 'account.invoice.lines_to_pay.delegate'
+    start = StateAction('account.act_delegate_lines_wizard')
+
+    def do_start(self, action):
+        return action, {
+            'ids': [
+                l.id for l in self.record.lines_to_pay
+                if not l.reconciliation],
+            'model': 'account.move.line',
+            }
diff -r 9a2b0ae5129e -r 81860b110704 invoice.xml
--- a/invoice.xml       Mon Aug 08 22:25:23 2022 +0200
+++ b/invoice.xml       Sun Sep 11 17:46:09 2022 +0200
@@ -224,6 +224,18 @@
             <field name="button" ref="invoice_reschedule_lines_to_pay_button"/>
             <field name="group" ref="account.group_account"/>
         </record>
+
+        <record model="ir.model.button" 
id="invoice_delegate_lines_to_pay_button">
+            <field name="name">delegate_lines_to_pay</field>
+            <field name="string">Modify Payee</field>
+            <field name="model" search="[('model', '=', 'account.invoice')]"/>
+        </record>
+        <record model="ir.model.button-res.group"
+                id="invoice_delegate_lines_to_pay_button_group_account">
+            <field name="button" ref="invoice_delegate_lines_to_pay_button"/>
+            <field name="group" ref="account.group_account"/>
+        </record>
+
         <record model="ir.model.button" id="invoice_process_button">
             <field name="name">process</field>
             <field name="string">Process</field>
@@ -431,5 +443,11 @@
             <field 
name="wiz_name">account.invoice.lines_to_pay.reschedule</field>
             <field name="model">account.invoice</field>
         </record>
+
+        <record model="ir.action.wizard" id="act_delegate_lines_to_pay_wizard">
+            <field name="name">Delegate Lines to Pay</field>
+            <field 
name="wiz_name">account.invoice.lines_to_pay.delegate</field>
+            <field name="model">account.invoice</field>
+        </record>
     </data>
 </tryton>
diff -r 9a2b0ae5129e -r 81860b110704 party.py
--- a/party.py  Mon Aug 08 22:25:23 2022 +0200
+++ b/party.py  Sun Sep 11 17:46:09 2022 +0200
@@ -88,6 +88,7 @@
         return super().fields_to_replace() + [
             ('account.invoice', 'party'),
             ('account.invoice.line', 'party'),
+            ('account.invoice.alternative_payee', 'party'),
             ]
 
 
@@ -100,7 +101,10 @@
         super().check_erase_company(party, company)
 
         invoices = Invoice.search([
-                ('party', '=', party.id),
+                ['OR',
+                    ('party', '=', party.id),
+                    ('alternative_payees', '=', party.id),
+                    ],
                 ('company', '=', company.id),
                 ('state', 'not in', ['paid', 'cancelled']),
                 ])
diff -r 9a2b0ae5129e -r 81860b110704 
tests/scenario_invoice_alternative_payee.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_invoice_alternative_payee.rst      Sun Sep 11 17:46:09 
2022 +0200
@@ -0,0 +1,131 @@
+==================================
+Invoice Alternative Payee Scenario
+==================================
+
+Imports::
+
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model, Wizard
+    >>> from trytond.tests.tools import activate_modules
+    >>> from trytond.modules.company.tests.tools import (
+    ...     create_company, get_company)
+    >>> from trytond.modules.account.tests.tools import (
+    ...     create_fiscalyear, create_chart, get_accounts)
+    >>> from trytond.modules.account_invoice.tests.tools import (
+    ...     set_fiscalyear_invoice_sequences)
+
+Activate modules::
+
+    >>> config = activate_modules('account_invoice')
+
+    >>> Invoice = Model.get('account.invoice')
+    >>> Journal = Model.get('account.journal')
+    >>> PaymentMethod = Model.get('account.invoice.payment.method')
+
+Create company::
+
+    >>> _ = create_company()
+    >>> company = get_company()
+
+Create fiscal year::
+
+    >>> fiscalyear = set_fiscalyear_invoice_sequences(
+    ...     create_fiscalyear(company))
+    >>> fiscalyear.click('create_period')
+    >>> period = fiscalyear.periods[0]
+
+Create chart of accounts::
+
+    >>> _ = create_chart(company)
+    >>> accounts = get_accounts(company)
+
+    >>> journal_cash, = Journal.find([
+    ...         ('code', '=', 'CASH'),
+    ...         ])
+
+    >>> payment_method = PaymentMethod()
+    >>> payment_method.name = "Cash"
+    >>> payment_method.journal = journal_cash
+    >>> payment_method.credit_account = accounts['cash']
+    >>> payment_method.debit_account = accounts['cash']
+    >>> payment_method.save()
+
+Create parties::
+
+    >>> Party = Model.get('party.party')
+    >>> party1 = Party(name="Party 1")
+    >>> party1.save()
+    >>> party2 = Party(name="Party 2")
+    >>> party2.save()
+    >>> party3 = Party(name="Party 3")
+    >>> party3.save()
+
+Post customer invoice::
+
+    >>> invoice = Invoice()
+    >>> invoice.party = party1
+    >>> invoice.alternative_payees.append(Party(party2.id))
+    >>> line = invoice.lines.new()
+    >>> line.account = accounts['revenue']
+    >>> line.quantity = 1
+    >>> line.unit_price = Decimal(10)
+    >>> invoice.click('post')
+    >>> invoice.state
+    'posted'
+    >>> len(invoice.lines_to_pay)
+    1
+    >>> invoice.amount_to_pay
+    Decimal('10.00')
+
+    >>> party1.reload()
+    >>> party1.receivable
+    Decimal('0.0')
+    >>> party2.reload()
+    >>> party2.receivable
+    Decimal('10.00')
+    >>> party3.reload()
+    >>> party3.receivable
+    Decimal('0.0')
+
+Set another payee::
+
+    >>> delegate = Wizard(
+    ...     'account.invoice.lines_to_pay.delegate', [invoice])
+    >>> delegate_lines, = delegate.actions
+    >>> delegate_lines.form.party = party3
+    >>> delegate_lines.execute('delegate')
+
+    >>> invoice.reload()
+    >>> invoice.state
+    'posted'
+    >>> len(invoice.lines_to_pay)
+    3
+    >>> invoice.amount_to_pay
+    Decimal('10.00')
+
+    >>> party1.reload()
+    >>> party1.receivable
+    Decimal('0.0')
+    >>> party2.reload()
+    >>> party2.receivable
+    Decimal('0.0')
+    >>> party3.reload()
+    >>> party3.receivable
+    Decimal('10.00')
+
+Pay the invoice::
+
+    >>> pay = Wizard('account.invoice.pay', [invoice])
+    >>> pay.form.payee = party3
+    >>> pay.form.amount = Decimal('10.00')
+    >>> pay.form.payment_method = payment_method
+    >>> pay.execute('choice')
+    >>> pay.state
+    'end'
+    >>> invoice.state
+    'paid'
+    >>> len(invoice.payment_lines)
+    1
+    >>> len(invoice.reconciliation_lines)
+    1
diff -r 9a2b0ae5129e -r 81860b110704 view/invoice_form.xml
--- a/view/invoice_form.xml     Mon Aug 08 22:25:23 2022 +0200
+++ b/view/invoice_form.xml     Sun Sep 11 17:46:09 2022 +0200
@@ -57,7 +57,7 @@
             <label name="account"/>
             <field name="account"/>
             <label name="accounting_date"/>
-            <field name="accounting_date"/>
+            <field name="alternative_payees" colspan="4"/>
             <label name="move"/>
             <field name="move"/>
             <label name="cancel_move"/>
@@ -75,7 +75,10 @@
             <label name="amount_to_pay"/>
             <field name="amount_to_pay"/>
             <field name="lines_to_pay" colspan="4" 
view_ids="account_invoice.move_line_view_list_to_pay"/>
-            <button name="reschedule_lines_to_pay" colspan="4"/>
+            <group id="lines_to_pay_buttons" colspan="4" col="-1">
+                <button name="reschedule_lines_to_pay"/>
+                <button name="delegate_lines_to_pay"/>
+            </group>
             <field name="payment_lines" colspan="4"
                 view_ids="account_invoice.move_line_view_list_payment"/>
             <field name="reconciliation_lines" colspan="4" 
view_ids="account_invoice.move_line_view_list_payment"/>
diff -r 9a2b0ae5129e -r 81860b110704 view/move_line_list_payment.xml
--- a/view/move_line_list_payment.xml   Mon Aug 08 22:25:23 2022 +0200
+++ b/view/move_line_list_payment.xml   Sun Sep 11 17:46:09 2022 +0200
@@ -2,6 +2,7 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <tree>
+    <field name="party" expand="1" optional="1"/>
     <field name="date"/>
     <field name="debit" sum="Debit"/>
     <field name="credit" sum="Credit"/>
diff -r 9a2b0ae5129e -r 81860b110704 view/move_line_list_to_pay.xml
--- a/view/move_line_list_to_pay.xml    Mon Aug 08 22:25:23 2022 +0200
+++ b/view/move_line_list_to_pay.xml    Sun Sep 11 17:46:09 2022 +0200
@@ -2,6 +2,7 @@
 <!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
 this repository contains the full copyright notices and license terms. -->
 <tree>
+    <field name="party" expand="1" optional="1"/>
     <field name="maturity_date"/>
     <field name="amount"/>
     <field name="delegated_amount"/>
diff -r 9a2b0ae5129e -r 81860b110704 view/pay_start_form.xml
--- a/view/pay_start_form.xml   Mon Aug 08 22:25:23 2022 +0200
+++ b/view/pay_start_form.xml   Sun Sep 11 17:46:09 2022 +0200
@@ -1,7 +1,11 @@
 <?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>
+<form cursor="amount">
+    <label name="payee"/>
+    <field name="payee"/>
+    <newline/>
+
     <label name="amount"/>
     <field name="amount" symbol=""/>
     <label name="currency"/>

Reply via email to