Add split lines wizard
issue10424 review349911002
This commit is contained in:
1 parent
e3ba424efe
commit
697f2a857b
9 files changed
+263
-18
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in new issue
Block a user