From d603d56f71db1b582c37d2cbf9456e68195aa77d Mon Sep 17 00:00:00 2001 From: Maxime Richez Date: Tue, 19 Sep 2023 14:12:42 +0200 Subject: [PATCH] Allow refreshing invoice report cache Closes #12091 --- modules/account_invoice/CHANGELOG | 1 + modules/account_invoice/__init__.py | 2 + modules/account_invoice/invoice.py | 117 ++++++++++++++++-- modules/account_invoice/invoice.xml | 33 +++++ .../scenario_invoice_report_revision.rst | 90 ++++++++++++++ modules/account_invoice/view/invoice_form.xml | 3 + .../view/invoice_report_revision_form.xml | 9 ++ .../view/invoice_report_revision_list.xml | 9 ++ 8 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 modules/account_invoice/tests/scenario_invoice_report_revision.rst create mode 100644 modules/account_invoice/view/invoice_report_revision_form.xml create mode 100644 modules/account_invoice/view/invoice_report_revision_list.xml diff --git a/modules/account_invoice/CHANGELOG b/modules/account_invoice/CHANGELOG index ac1a017512..050b42e963 100644 --- a/modules/account_invoice/CHANGELOG +++ b/modules/account_invoice/CHANGELOG @@ -1,3 +1,4 @@ +* Allow refreshing invoice report cache * Set employee who validates and posts invoices * Allow modification of invoice references when report is not cached * Add cache on invoice's amounts diff --git a/modules/account_invoice/__init__.py b/modules/account_invoice/__init__.py index 9920570489..3ef8607f94 100644 --- a/modules/account_invoice/__init__.py +++ b/modules/account_invoice/__init__.py @@ -23,6 +23,7 @@ def register(): invoice.PayInvoiceStart, invoice.PayInvoiceAsk, invoice.CreditInvoiceStart, + invoice.InvoiceReportRevision, party.Address, party.ContactMechanism, party.Party, @@ -46,6 +47,7 @@ def register(): invoice.CreditInvoice, invoice.RescheduleLinesToPay, invoice.DelegateLinesToPay, + invoice.RefreshInvoiceReport, party.Replace, party.Erase, account.RenewFiscalYear, diff --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py index 30132b4549..69a163debb 100644 --- a/modules/account_invoice/invoice.py +++ b/modules/account_invoice/invoice.py @@ -5,6 +5,7 @@ from collections import defaultdict, namedtuple from decimal import Decimal from itertools import chain, combinations, groupby +from genshi.template.text import TextTemplate from sql import Literal, Null from sql.aggregate import Sum from sql.conditionals import Coalesce @@ -27,7 +28,7 @@ from trytond.pool import Pool from trytond.pyson import Bool, Eval, If from trytond.report import Report from trytond.rpc import RPC -from trytond.tools import firstline, grouped_slice, reduce_ids +from trytond.tools import firstline, grouped_slice, reduce_ids, slugify from trytond.transaction import Transaction from trytond.wizard import ( Button, StateAction, StateTransition, StateView, Wizard) @@ -45,7 +46,17 @@ else: store_prefix = None -class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): +class InvoiceReportMixin: + __slots__ = () + + invoice_report_cache = fields.Binary( + "Invoice Report", readonly=True, + file_id=file_id, store_prefix=store_prefix) + invoice_report_cache_id = fields.Char("Invoice Report ID", readonly=True) + invoice_report_format = fields.Char("Invoice Report Format", readonly=True) + + +class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin): 'Invoice' __name__ = 'account.invoice' _rec_name = 'number' @@ -291,10 +302,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): amount_to_pay = fields.Function(Monetary( "Amount to Pay", currency='currency', digits='currency'), 'get_amount_to_pay') - invoice_report_cache = fields.Binary('Invoice Report', readonly=True, - file_id=file_id, store_prefix=store_prefix) - invoice_report_cache_id = fields.Char('Invoice Report ID', readonly=True) - invoice_report_format = fields.Char('Invoice Report Format', readonly=True) + invoice_report_revisions = fields.One2Many( + 'account.invoice.report.revision', 'invoice', + "Invoice Report Revisions", readonly=True, + states={ + 'invisible': ~Eval('invoice_report_revisions'), + }) allow_cancel = fields.Function( fields.Boolean("Allow Cancel Invoice"), 'get_allow_cancel') has_payment_method = fields.Function( @@ -342,7 +355,8 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): 'move', 'cancel_move', 'additional_moves', 'invoice_report_cache', 'invoice_report_format', 'total_amount_cache', 'tax_amount_cache', 'untaxed_amount_cache', - 'lines', 'reference'} + 'lines', 'reference', 'invoice_report_cache_id', + 'invoice_report_revisions'} cls._order = [ ('number', 'DESC NULLS FIRST'), ('id', 'DESC'), @@ -1249,6 +1263,26 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): "Return the default computed tax identifier" return self.company.party.tax_identifier + @property + def invoice_report_versioned(self): + return self.state in {'posted', 'paid'} and self.type == 'out' + + def create_invoice_report_revision(self): + pool = Pool() + InvoiceReportRevision = pool.get('account.invoice.report.revision') + if not self.invoice_report_versioned: + return + invoice_report_revision = InvoiceReportRevision( + invoice=self, + invoice_report_cache=self.invoice_report_cache, + invoice_report_cache_id=self.invoice_report_cache_id, + invoice_report_format=self.invoice_report_format) + self.invoice_report_revisions += (invoice_report_revision,) + self.invoice_report_cache = None + self.invoice_report_cache_id = None + self.invoice_report_format = None + return invoice_report_revision + @property def is_modifiable(self): return not (self.state in {'posted', 'paid'} @@ -3066,6 +3100,70 @@ class PaymentMethod(DeactivableMixin, ModelSQL, ModelView): return Transaction().context.get('company') +class InvoiceReportRevision(ModelSQL, ModelView, InvoiceReportMixin): + "Invoice Report Revision" + __name__ = 'account.invoice.report.revision' + invoice = fields.Many2One( + 'account.invoice', "Invoice", required=True, ondelete='CASCADE') + date = fields.DateTime("Date", required=True, readonly=True) + filename = fields.Function(fields.Char("File Name"), 'get_filename') + + @classmethod + def __setup__(cls): + super().__setup__() + cls.__access__.add('invoice') + cls._order.insert(0, ('date', 'DESC')) + cls.invoice_report_cache.filename = 'filename' + + @classmethod + def default_date(cls): + return dt.datetime.now() + + @classmethod + def get_filename(cls, revisions, name): + pool = Pool() + ActionReport = pool.get('ir.action.report') + + action_report, = ActionReport.search([ + ('report_name', '=', 'account.invoice'), + ], limit=1) + + action_report_name = action_report.name[:100] + if action_report.record_name: + template = TextTemplate(action_report.record_name) + else: + template = None + filenames = {} + for revision in revisions: + invoice = revision.invoice + if template: + record_name = template.generate(record=invoice).render() + else: + record_name = invoice.rec_name + filename = '-'.join([action_report_name, record_name]) + filenames[revision.id] = ( + f'{slugify(filename)}.{revision.invoice_report_format}') + return filenames + + +class RefreshInvoiceReport(Wizard): + "Refresh Invoice Report" + __name__ = 'account.invoice.refresh_invoice_report' + start_state = 'archive' + archive = StateTransition() + print_ = StateAction('account_invoice.report_invoice') + + def transition_archive(self): + for record in self.records: + record.create_invoice_report_revision() + self.model.save(self.records) + return 'print_' + + def do_print_(self, action): + ids = [r.id for r in self.records] + return action, {'ids': ids} + + class InvoiceReport(Report): __name__ = 'account.invoice' @@ -3086,10 +3184,7 @@ class InvoiceReport(Report): invoice.invoice_report_cache) else: result = super()._execute(records, header, data, action) - # If the invoice is posted or paid and the report not saved in - # invoice_report_cache there was an error somewhere. So we save it - # now in invoice_report_cache - if invoice.state in {'posted', 'paid'} and invoice.type == 'out': + if invoice.invoice_report_versioned: format_, data = result if isinstance(data, str): data = bytes(data, 'utf-8') diff --git a/modules/account_invoice/invoice.xml b/modules/account_invoice/invoice.xml index 6ad728fd32..ab5f30dcaf 100644 --- a/modules/account_invoice/invoice.xml +++ b/modules/account_invoice/invoice.xml @@ -292,6 +292,39 @@ this repository contains the full copyright notices and license terms. --> + + Invoice (revised) + account.invoice.refresh_invoice_report + account.invoice + + + form_print + account.invoice,-1 + + + + + + + + + account.invoice.report.revision + tree + invoice_report_revision_list + + + account.invoice.report.revision + form + invoice_report_revision_form + + + + + + + + + Invoice account.invoice diff --git a/modules/account_invoice/tests/scenario_invoice_report_revision.rst b/modules/account_invoice/tests/scenario_invoice_report_revision.rst new file mode 100644 index 0000000000..1312264c5c --- /dev/null +++ b/modules/account_invoice/tests/scenario_invoice_report_revision.rst @@ -0,0 +1,90 @@ +================================ +Invoice Report Revision Scenario +================================ + +Imports:: + + >>> import datetime as dt + + >>> 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, create_tax) + >>> from trytond.modules.account_invoice.tests.tools import ( + ... set_fiscalyear_invoice_sequences) + + >>> today = dt.date.today() + +Activate modules:: + + >>> config = activate_modules('account_invoice') + + >>> Invoice = Model.get('account.invoice') + >>> Journal = Model.get('account.journal') + >>> Move = Model.get('account.move') + >>> Party = Model.get('party.party') + >>> PaymentMethod = Model.get('account.invoice.payment.method') + >>> ProductCategory = Model.get('product.category') + >>> ProductTemplate = Model.get('product.template') + >>> ProductUom = Model.get('product.uom') + >>> Sequence = Model.get('ir.sequence') + +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) + +Create party:: + + >>> party = Party(name="Party") + >>> party.save() + +Post an invoice:: + + >>> invoice = Invoice() + >>> invoice.type = 'out' + >>> invoice.party = party + >>> invoice.invoice_date = today + >>> line = invoice.lines.new() + >>> line.account = accounts['revenue'] + >>> line.quantity = 5 + >>> line.unit_price = Decimal('20') + >>> invoice.click('post') + >>> invoice.state + 'posted' + +Check invoice report:: + + >>> bool(invoice.invoice_report_cache) + True + >>> len(invoice.invoice_report_revisions) + 0 + >>> invoice.invoice_report_format + 'odt' + +Execute update invoice report wizard:: + + >>> refresh_invoice_report = Wizard( + ... 'account.invoice.refresh_invoice_report', [invoice]) + >>> revision, = invoice.invoice_report_revisions + >>> bool(revision.invoice_report_cache) + True + >>> revision.invoice_report_format + 'odt' + >>> revision.filename + 'Invoice-1.odt' diff --git a/modules/account_invoice/view/invoice_form.xml b/modules/account_invoice/view/invoice_form.xml index b06cf84d63..f513f7b8da 100644 --- a/modules/account_invoice/view/invoice_form.xml +++ b/modules/account_invoice/view/invoice_form.xml @@ -84,6 +84,9 @@ this repository contains the full copyright notices and license terms. --> view_ids="account_invoice.move_line_view_list_payment"/> + + +