Add split lines wizard

issue10424
review349911002
This commit is contained in:
Cédric Krier committed 2021-08-20 23:45:02 +02:00
1 parent e3ba424efe
commit 697f2a857b
9 files changed
+263 -18

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Add wizard to reschedule lines to pay of invoices
* Create base tax line for manual taxes
Version 6.0.0 - 2021-05-03
+3
View File
@@ -17,6 +17,7 @@ def register():
payment_term.TestPaymentTermView,
payment_term.TestPaymentTermViewResult,
invoice.Invoice,
invoice.InvoiceAdditionalMove,
invoice.InvoicePaymentLine,
invoice.InvoiceLine,
invoice.InvoiceLineTax,
@@ -45,9 +46,11 @@ def register():
payment_term.TestPaymentTerm,
invoice.PayInvoice,
invoice.CreditInvoice,
invoice.RescheduleLinesToPay,
party.Replace,
party.Erase,
account.RenewFiscalYear,
account.SplitLines,
module='account_invoice', type_='wizard')
Pool.register(
invoice.InvoiceReport,
+24 -2
View File
@@ -311,8 +311,10 @@ def _invoices_to_process(reconciliations):
invoices = set()
for sub_ids in grouped_slice(move_ids):
invoices.update(Invoice.search([
('move', 'in', list(sub_ids)),
sub_ids = list(sub_ids)
invoices.update(Invoice.search(['OR',
('move', 'in', sub_ids),
('additional_moves', 'in', sub_ids),
]))
if others:
invoices.update(_invoices_to_process(Reconciliation.browse(others)))
@@ -404,3 +406,23 @@ class RenewFiscalYear(metaclass=PoolMeta):
if to_write:
InvoiceSequence.write(*to_write)
return fiscalyear
class SplitLines(metaclass=PoolMeta):
__name__ = 'account.move.line.split'
@classmethod
def split_lines(cls, lines, journal, terms):
pool = Pool()
Invoice = pool.get('account.invoice')
move, balance_line = super().split_lines(lines, journal, terms)
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, {
'additional_moves': [('add', [move.id])],
})
return move, balance_line
+9
View File
@@ -102,6 +102,15 @@ credited with a refund.
When this is done the credit note is automatically posted and the invoice is
cancelled.
.. _wizard-account.invoice.lines_to_pay.reschedule:
Reschedule Lines to Pay
^^^^^^^^^^^^^^^^^^^^^^^
The *Reschedule Lines to Pay* wizard allows to modify the payment terms of the
remaining lines to pay using the `Split Lines <wizard-account.move.line.split>`
wizard.
Reports
-------
+94 -16
View File
@@ -2,7 +2,7 @@
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from collections import defaultdict, namedtuple
from itertools import combinations
from itertools import combinations, chain
from sql import Null
from sql.aggregate import Sum
@@ -144,6 +144,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
('company', '=', Eval('company', -1)),
],
depends=['company'])
additional_moves = fields.Many2Many(
'account.invoice-additional-account.move', 'invoice', 'move',
"Additional Moves", readonly=True,
domain=[
('company', '=', Eval('company', -1)),
],
states={
'invisible': ~Eval('additional_moves'),
},
depends=['company'])
cancel_move = fields.Many2One('account.move', 'Cancel Move', readonly=True,
domain=[
('company', '=', Eval('company', -1)),
@@ -259,7 +269,8 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
super(Invoice, cls).__setup__()
cls._check_modify_exclude = {
'state', 'payment_lines', 'move', 'cancel_move',
'invoice_report_cache', 'invoice_report_format', 'lines'}
'additional_moves', 'invoice_report_cache',
'invoice_report_format', 'lines'}
cls._order = [
('number', 'DESC NULLS FIRST'),
('id', 'DESC'),
@@ -325,6 +336,11 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
'invisible': Eval('state') != 'posted',
'depends': ['state'],
},
'reschedule_lines_to_pay': {
'invisible': (
~Eval('lines_to_pay') | Eval('reconciled', False)),
'depends': ['lines_to_pay', 'reconciled'],
},
'process': {
'invisible': ~Eval('state').in_(
['posted', 'paid']),
@@ -696,19 +712,38 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
def get_lines_to_pay(cls, invoices, name):
pool = Pool()
MoveLine = pool.get('account.move.line')
AdditionalMove = pool.get('account.invoice-additional-account.move')
line = MoveLine.__table__()
invoice = cls.__table__()
additional_move = AdditionalMove.__table__()
cursor = Transaction().connection.cursor()
lines = defaultdict(list)
for sub_ids in grouped_slice(invoices):
red_sql = reduce_ids(invoice.id, sub_ids)
cursor.execute(*invoice.join(line,
condition=((invoice.move == line.move)
& (invoice.account == line.account))).select(
invoice.id, line.id,
where=red_sql,
order_by=(invoice.id, line.maturity_date.nulls_last)))
query = (invoice
.join(line,
condition=((invoice.move == line.move)
& (invoice.account == line.account)))
.select(
invoice.id.as_('invoice'),
line.id.as_('line'),
line.maturity_date.as_('maturity_date'),
where=red_sql))
query |= (invoice
.join(additional_move,
condition=additional_move.invoice == invoice.id)
.join(line,
condition=((additional_move.move == line.move)
& (invoice.account == line.account)))
.select(
invoice.id.as_('invoice'),
line.id.as_('line'),
line.maturity_date.as_('maturity_date'),
where=red_sql))
cursor.execute(*query.select(
query.invoice, query.line,
order_by=query.maturity_date.nulls_last))
for invoice_id, line_id in cursor:
lines[invoice_id].append(line_id)
return lines
@@ -717,11 +752,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
if self.state != 'paid':
return
lines = set()
for line in self.move.lines:
if line.account == self.account and line.reconciliation:
for line in line.reconciliation.lines:
if line not in self.move.lines:
lines.add(line)
for move in chain([self.move], self.additional_moves):
for line in move.lines:
if line.account == self.account and line.reconciliation:
for line in line.reconciliation.lines:
if line not in self.lines_to_pay:
lines.add(line)
return [l.id for l in sorted(lines, key=lambda l: l.date)]
@classmethod
@@ -1239,6 +1275,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
default.setdefault('number', None)
default.setdefault('sequence')
default.setdefault('move', None)
default.setdefault('additional_moves', None)
default.setdefault('cancel_move', None)
default.setdefault('invoice_report_cache', None)
default.setdefault('invoice_report_cache_id', None)
@@ -1465,6 +1502,8 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
for invoice in invoices:
if invoice.move:
moves.append(invoice.move)
if invoice.additional_moves:
moves.extend(invoice.additional_moves)
if moves:
Move.delete(moves)
@@ -1539,6 +1578,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
def pay(cls, invoices):
pass
@classmethod
@ModelView.button_action(
'account_invoice.act_reschedule_lines_to_pay_wizard')
def reschedule_lines_to_pay(cls, invoices):
pass
@classmethod
@ModelView.button
def process(cls, invoices):
@@ -1575,6 +1620,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
if invoice.move or invoice.number:
if invoice.move.state == 'draft':
delete_moves.append(invoice.move)
delete_moves.extend(invoice.additional_moves)
elif not invoice.cancel_move:
if (invoice.type == 'out'
and not invoice.company.cancel_invoice_out):
@@ -1583,8 +1629,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
'.msg_invoice_customer_cancel_move',
invoice=invoice.rec_name))
invoice.cancel_move = invoice.move.cancel()
additional_cancel_moves = [
m.cancel() for m in invoice.additional_moves]
invoice.additional_moves += tuple(additional_cancel_moves)
to_save.append(invoice)
cancel_moves.append(invoice.cancel_move)
cancel_moves.extend(additional_cancel_moves)
if cancel_moves:
Move.save(cancel_moves)
cls.save(to_save)
@@ -1601,9 +1651,13 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
if not invoice.move or not invoice.cancel_move:
continue
to_reconcile = []
for line in invoice.move.lines + invoice.cancel_move.lines:
if line.account == invoice.account:
to_reconcile.append(line)
for move in chain(
[invoice.move, invoice.cancel_move],
invoice.additional_moves):
for line in move.lines:
if (not line.reconciliation
and line.account == invoice.account):
to_reconcile.append(line)
Line.reconcile(to_reconcile)
cls._clean_payments(invoices)
@@ -1626,6 +1680,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin):
cls.write(*to_write)
class InvoiceAdditionalMove(ModelSQL):
"Invoice Additional Move"
__name__ = 'account.invoice-additional-account.move'
invoice = fields.Many2One(
'account.invoice', "Invoice", ondelete='CASCADE',
select=True, required=True)
move = fields.Many2One(
'account.move', "Additional Move", ondelete='CASCADE')
class InvoicePaymentLine(ModelSQL):
'Invoice - Payment Line'
__name__ = 'account.invoice-account.move.line'
@@ -3141,3 +3205,17 @@ class CreditInvoice(Wizard):
if len(credit_invoices) == 1:
action['views'].reverse()
return action, data
class RescheduleLinesToPay(Wizard):
"Reschedule Lines to Pay"
__name__ = 'account.invoice.lines_to_pay.reschedule'
start = StateAction('account.act_split_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',
}
+16
View File
@@ -214,6 +214,16 @@ this repository contains the full copyright notices and license terms. -->
<field name="button" ref="invoice_pay_button"/>
<field name="group" ref="account.group_account"/>
</record>
<record model="ir.model.button" id="invoice_reschedule_lines_to_pay_button">
<field name="name">reschedule_lines_to_pay</field>
<field name="string">Reschedule</field>
<field name="model" search="[('model', '=', 'account.invoice')]"/>
</record>
<record model="ir.model.button-res.group"
id="invoice_reschedule_lines_to_pay_button_group_account">
<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_process_button">
<field name="name">process</field>
<field name="string">Process</field>
@@ -415,5 +425,11 @@ this repository contains the full copyright notices and license terms. -->
pyson="1"/>
<field name="rule_group" ref="rule_group_invoice_line_companies"/>
</record>
<record model="ir.action.wizard" id="act_reschedule_lines_to_pay_wizard">
<field name="name">Reschedule Lines to Pay</field>
<field name="wiz_name">account.invoice.lines_to_pay.reschedule</field>
<field name="model">account.invoice</field>
</record>
</data>
</tryton>
+109
View File
@@ -0,0 +1,109 @@
============================
Invoice Split Lines 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 party::
>>> Party = Model.get('party.party')
>>> party = Party(name='Party')
>>> party.save()
Post customer invoice::
>>> invoice = Invoice()
>>> invoice.party = party
>>> 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')
Reschedule line::
>>> reschedule = Wizard('account.invoice.lines_to_pay.reschedule', [invoice])
>>> split_lines, = reschedule.actions
>>> split_lines.form.total_amount
Decimal('10.00')
>>> split_lines.form.start_date = period.end_date
>>> split_lines.form.frequency ='monthly'
>>> split_lines.form.number = 2
>>> split_lines.execute('preview')
>>> split_lines.execute('split')
>>> invoice.reload()
>>> invoice.state
'posted'
>>> len(invoice.lines_to_pay)
4
>>> len([l for l in invoice.lines_to_pay if not l.reconciliation])
2
>>> invoice.amount_to_pay
Decimal('10.00')
Pay the invoice::
>>> pay = Wizard('account.invoice.pay', [invoice])
>>> pay.form.amount
Decimal('10.00')
>>> pay.form.payment_method = payment_method
>>> pay.execute('choice')
>>> pay.state
'end'
>>> invoice.state
'paid'
>>> len(invoice.reconciliation_lines)
1
+5
View File
@@ -310,6 +310,11 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_invoice_split_lines.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_renew_fiscalyear.rst',
tearDown=doctest_teardown, encoding='utf-8',
+2
View File
@@ -62,6 +62,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="move"/>
<label name="cancel_move"/>
<field name="cancel_move"/>
<field name="additional_moves" colspan="4"/>
<separator name="comment" colspan="4"/>
<field name="comment" colspan="4"/>
</page>
@@ -74,6 +75,7 @@ this repository contains the full copyright notices and license terms. -->
<label name="amount_to_pay"/>
<field name="amount_to_pay" symbol="currency"/>
<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"/>
<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"/>