Add multi client template management

This commit is contained in:
2026-04-06 14:56:37 +02:00
parent 05e68636ad
commit 1f62ae91dd
7 changed files with 141 additions and 6 deletions

View File

@@ -274,4 +274,7 @@ def register():
sale.SaleCreatePurchase,
sale.SaleAllocationsWizard,
module='sale', type_='wizard')
Pool.register(
invoice.InvoiceReport,
module='purchase_trade', type_='report')

View File

@@ -6,3 +6,6 @@ class Configuration(ModelSingleton, ModelSQL, ModelView):
__name__ = 'purchase_trade.configuration'
pricing_rule = fields.Text("Pricing Rule")
invoice_report_template = fields.Char("Invoice Template")
invoice_cndn_report_template = fields.Char("CN/DN Template")
invoice_prepayment_report_template = fields.Char("Prepayment Template")

View File

@@ -98,6 +98,9 @@ Derniere mise a jour: `2026-04-02`
- verifier si le probleme vient du cache avant de modifier le `.fodt`
- pour un report alternatif, ne pas reutiliser le cache du report standard `account_invoice/invoice.fodt`
- si besoin, bypasser la lecture/ecriture du cache pour les templates alternatifs
- pour les clients multi-templates, preferer une configuration metier qui
stocke le nom du template par action (`Invoice`, `CN/DN`, `Prepayment`)
plutot qu'une modification manuelle de `ir_action_report.report`
### TR-007 - Pour une facture trade, privilegier le lot physique comme chemin de navigation

View File

@@ -2,6 +2,9 @@ from decimal import Decimal, ROUND_HALF_UP
from trytond.pool import Pool, PoolMeta
from trytond.modules.purchase_trade.numbers_to_words import amount_to_currency_words
from trytond.exceptions import UserError
from trytond.modules.account_invoice.invoice import (
InvoiceReport as BaseInvoiceReport)
class Invoice(metaclass=PoolMeta):
@@ -665,3 +668,62 @@ class InvoiceLine(metaclass=PoolMeta):
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
class InvoiceReport(BaseInvoiceReport):
__name__ = 'account.invoice'
@classmethod
def _get_purchase_trade_configuration(cls):
Configuration = Pool().get('purchase_trade.configuration')
configurations = Configuration.search([], limit=1)
return configurations[0] if configurations else None
@classmethod
def _get_action_name(cls, action):
if isinstance(action, dict):
return action.get('name') or ''
return getattr(action, 'name', '') or ''
@classmethod
def _resolve_configured_report_path(cls, action):
config = cls._get_purchase_trade_configuration()
report_path = cls._get_action_report_path(action) or ''
action_name = cls._get_action_name(action)
if (report_path.endswith('/prepayment.fodt')
or action_name == 'Prepayment'):
template = (
getattr(config, 'invoice_prepayment_report_template', '')
if config else '')
elif (report_path.endswith('/invoice_ict_final.fodt')
or action_name == 'CN/DN'):
template = (
getattr(config, 'invoice_cndn_report_template', '')
if config else '')
else:
template = (
getattr(config, 'invoice_report_template', '')
if config else '')
template = (template or '').strip()
if not template:
raise UserError('No template found')
if '/' not in template:
return f'account_invoice/{template}'
return template
@classmethod
def _get_resolved_action(cls, action):
report_path = cls._resolve_configured_report_path(action)
if isinstance(action, dict):
resolved = dict(action)
resolved['report'] = report_path
return resolved
setattr(action, 'report', report_path)
return action
@classmethod
def _execute(cls, records, header, data, action):
resolved_action = cls._get_resolved_action(action)
return super()._execute(records, header, data, resolved_action)

View File

@@ -301,6 +301,64 @@ class PurchaseTradeTestCase(ModuleTestCase):
with self.assertRaises(UserError):
report.validate_remote_weight_report_context(shipment)
def test_invoice_report_uses_invoice_template_from_configuration(self):
'invoice report path is resolved from purchase_trade configuration'
report_class = Pool().get('account.invoice', type='report')
config_model = Mock()
config_model.search.return_value = [
Mock(
invoice_report_template='invoice_melya.fodt',
invoice_cndn_report_template='invoice_ict_final.fodt',
invoice_prepayment_report_template='prepayment.fodt',
)
]
with patch(
'trytond.modules.purchase_trade.invoice.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = config_model
self.assertEqual(
report_class._resolve_configured_report_path({
'name': 'Invoice',
'report': 'account_invoice/invoice.fodt',
}),
'account_invoice/invoice_melya.fodt')
self.assertEqual(
report_class._resolve_configured_report_path({
'name': 'Prepayment',
'report': 'account_invoice/prepayment.fodt',
}),
'account_invoice/prepayment.fodt')
self.assertEqual(
report_class._resolve_configured_report_path({
'name': 'CN/DN',
'report': 'account_invoice/invoice_ict_final.fodt',
}),
'account_invoice/invoice_ict_final.fodt')
def test_invoice_report_raises_when_template_is_missing(self):
'invoice report must fail clearly when no template is configured'
report_class = Pool().get('account.invoice', type='report')
config_model = Mock()
config_model.search.return_value = [
Mock(
invoice_report_template='',
invoice_cndn_report_template='',
invoice_prepayment_report_template='',
)
]
with patch(
'trytond.modules.purchase_trade.invoice.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = config_model
with self.assertRaises(UserError):
report_class._resolve_configured_report_path({
'name': 'Invoice',
'report': 'account_invoice/invoice.fodt',
})
def test_sale_report_multi_line_helpers_aggregate_all_lines(self):
'sale report helpers aggregate quantity, price lines and shipment periods'
Sale = Pool().get('sale.sale')

View File

@@ -2,4 +2,10 @@
<form col="4">
<label name="pricing_rule"/>
<field name="pricing_rule" colspan="3"/>
<label name="invoice_report_template"/>
<field name="invoice_report_template" colspan="3"/>
<label name="invoice_cndn_report_template"/>
<field name="invoice_cndn_report_template" colspan="3"/>
<label name="invoice_prepayment_report_template"/>
<field name="invoice_prepayment_report_template" colspan="3"/>
</form>

View File

@@ -1791,7 +1791,7 @@
<text:p text:style-name="P9"/>
<text:p text:style-name="P9"/>
<text:p text:style-name="P9"/>
<text:p text:style-name="P9"><text:span text:style-name="T51">SHIPMENT SCHEDULE</text:span>:<text:tab/><text:span text:style-name="T45"><text:placeholder text:placeholder-type="text">&lt;sale.lines[0].del_period.month_name if sale.lines and sale.lines[0].del_period else &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P9"><text:span text:style-name="T51">SHIPMENT SCHEDULE</text:span>:<text:tab/><text:span text:style-name="T45"><text:placeholder text:placeholder-type="text">&lt;sale.report_delivery_period_description or &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P9"/>
<text:p text:style-name="P6">TOLERANCE:<text:tab/><text:tab/>+/- <text:placeholder text:placeholder-type="text">&lt;sale.tol_min&gt;</text:placeholder><text:s/>%</text:p>
<text:p text:style-name="P2"/>