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"/>