@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -292,6 +292,39 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="group" ref="account.group_account_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="refresh_invoice_report_wizard">
|
||||
<field name="name">Invoice (revised)</field>
|
||||
<field name="wiz_name">account.invoice.refresh_invoice_report</field>
|
||||
<field name="model">account.invoice</field>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="refresh_invoice_report_keyword">
|
||||
<field name="keyword">form_print</field>
|
||||
<field name="model">account.invoice,-1</field>
|
||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
||||
</record>
|
||||
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
|
||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
||||
<field name="group" ref="account.group_account_admin"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="invoice_report_revision_view_list">
|
||||
<field name="model">account.invoice.report.revision</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">invoice_report_revision_list</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="invoice_report_revision_view_form">
|
||||
<field name="model">account.invoice.report.revision</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">invoice_report_revision_form</field>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_invoice_report_revision">
|
||||
<field name="model" search="[('model', '=', 'account.invoice.report.revision')]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.report" id="report_invoice">
|
||||
<field name="name">Invoice</field>
|
||||
<field name="model">account.invoice</field>
|
||||
|
||||
@@ -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'
|
||||
@@ -84,6 +84,9 @@ this repository contains the full copyright notices and license terms. -->
|
||||
view_ids="account_invoice.move_line_view_list_payment"/>
|
||||
<field name="reconciliation_lines" colspan="4" view_ids="account_invoice.move_line_view_list_payment"/>
|
||||
</page>
|
||||
<page name="invoice_report_revisions">
|
||||
<field name="invoice_report_revisions" colspan="4"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<label id="empty" colspan="3"/>
|
||||
<group col="-1" colspan="3" id="buttons">
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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>
|
||||
<label name="date"/>
|
||||
<field name="date"/>
|
||||
<label name="invoice_report_cache"/>
|
||||
<field name="invoice_report_cache"/>
|
||||
</form>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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. -->
|
||||
<tree>
|
||||
<field name="invoice" expand="2"/>
|
||||
<field name="date" widget="date"/>
|
||||
<field name="date" widget="time" string="Time"/>
|
||||
<field name="invoice_report_cache" widget="binary" expand="1"/>
|
||||
</tree>
|
||||
Reference in New Issue
Block a user