Support alternative payees on invoice

issue11646
review435641003
This commit is contained in:
Cédric Krier
2022-09-11 17:46:09 +02:00
parent 01d1dd40f0
commit 9df92ed165
11 changed files with 308 additions and 18 deletions
+2
View File
@@ -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
+3
View File
@@ -15,6 +15,7 @@ def register():
payment_term.TestPaymentTermViewResult,
invoice.Invoice,
invoice.InvoiceAdditionalMove,
invoice.AlternativePayee,
invoice.InvoicePaymentLine,
invoice.InvoiceLine,
invoice.InvoiceLineTax,
@@ -44,10 +45,12 @@ def register():
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,
+21
View File
@@ -440,3 +440,24 @@ class RescheduleLines(metaclass=PoolMeta):
'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
+116 -14
View File
@@ -166,6 +166,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
})
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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
'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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
~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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
'.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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
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 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
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)
@@ -1685,6 +1716,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
def reschedule_lines_to_pay(cls, invoices):
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):
@@ -1793,6 +1830,17 @@ class InvoiceAdditionalMove(ModelSQL):
'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 @@ class InvoicePaymentLine(ModelSQL):
'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 @@ class InvoicePaymentLine(ModelSQL):
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 @@ class InvoicePaymentLine(ModelSQL):
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 InvoiceReport(Report):
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 @@ class PayInvoice(Wizard):
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 @@ class PayInvoice(Wizard):
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 @@ class RescheduleLinesToPay(Wizard):
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',
}
+18
View File
@@ -224,6 +224,18 @@ this repository contains the full copyright notices and license terms. -->
<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 @@ this repository contains the full copyright notices and license terms. -->
<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>
+5 -1
View File
@@ -88,6 +88,7 @@ class Replace(metaclass=PoolMeta):
return super().fields_to_replace() + [
('account.invoice', 'party'),
('account.invoice.line', 'party'),
('account.invoice.alternative_payee', 'party'),
]
@@ -100,7 +101,10 @@ class Erase(metaclass=PoolMeta):
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']),
])
@@ -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
+5 -2
View File
@@ -57,7 +57,7 @@ this repository contains the full copyright notices and license terms. -->
<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 @@ this repository contains the full copyright notices and license terms. -->
<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"/>
+1
View File
@@ -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"/>
+1
View File
@@ -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"/>
+5 -1
View File
@@ -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"/>