Compare commits

..

20 Commits

Author SHA1 Message Date
AzureAD\SylvainDUVERNAY
1c8abc7c1e Trade Finance 2026-04-12 17:22:25 +02:00
AzureAD\SylvainDUVERNAY
37bf6ba23b Trade Finance 2026-04-12 17:15:08 +02:00
AzureAD\SylvainDUVERNAY
2e649aa61b Commit 2026-04-12 17:06:26 +02:00
AzureAD\SylvainDUVERNAY
735a72d23e Trade Finance 2026-04-12 16:58:45 +02:00
AzureAD\SylvainDUVERNAY
ebf9b6c495 Trade Finance 2026-04-12 16:47:59 +02:00
AzureAD\SylvainDUVERNAY
13d26ac41b Trade Finance 2026-04-12 16:36:08 +02:00
AzureAD\SylvainDUVERNAY
b829b11791 Trade Finance - Commit 2026-04-12 14:41:12 +02:00
AzureAD\SylvainDUVERNAY
4a056ef402 Change on Facility definition 2026-04-07 21:53:10 +02:00
AzureAD\SylvainDUVERNAY
da65da79c0 Trade Finance - Facility - Limit ordering 2026-03-31 22:06:58 +02:00
AzureAD\SylvainDUVERNAY
aa6b3fb9ad Trade Finance - TF Facility - Order limits 2026-03-31 21:55:34 +02:00
AzureAD\SylvainDUVERNAY
b1dd118628 Trade Finance - Facility - Improve Limits table layout 2026-03-31 21:39:56 +02:00
AzureAD\SylvainDUVERNAY
f1f9d157cc Trade Finance - Facility - Fix error on Save 2026-03-31 16:50:45 +02:00
AzureAD\SylvainDUVERNAY
283b71fda9 Trade Finance - Sub Limit 2026-03-31 14:14:05 +02:00
AzureAD\SylvainDUVERNAY
f27dd5620e Trade Finance - Adjustment on sub-limit 2026-03-31 14:10:43 +02:00
AzureAD\SylvainDUVERNAY
143f59c62e Trade Finance - Facility Fix 3 2026-03-31 13:37:42 +02:00
AzureAD\SylvainDUVERNAY
be6b6517a5 Trade Finance - Facility Fix 2 2026-03-31 13:34:31 +02:00
AzureAD\SylvainDUVERNAY
d96973310b Trade Finance Facility - Fix 1 2026-03-31 13:31:33 +02:00
AzureAD\SylvainDUVERNAY
39278c4483 Trade Finance - Facility 2026-03-31 13:22:18 +02:00
AzureAD\SylvainDUVERNAY
4534ad86f1 Trade Finance - Fix 2026-03-31 12:24:17 +02:00
AzureAD\SylvainDUVERNAY
10e8e5be9b Trade Finance - Initial Commit 2026-03-31 12:14:51 +02:00
126 changed files with 3670 additions and 9799 deletions

View File

@@ -0,0 +1,17 @@
{
"permissions": {
"allow": [
"Bash(cd /c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/modules)",
"Bash(ls -d */)",
"Bash(cd /c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/modules/purchase_trade)",
"Bash(ls -1d */)",
"Bash(for f:*)",
"Bash(do echo:*)",
"Read(//c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/**)",
"Bash(done)",
"Bash(cd /c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/modules/purchase_trade/view)",
"Bash(ls -1 *.xml)",
"Bash(py --version)"
]
}
}

3
.gitignore vendored
View File

@@ -1 +1,2 @@
*.pyc
deployment/vps-TradonDev_Instructions.md
deployment/vps/46.202.173.47-credentials.md

View File

@@ -38,18 +38,13 @@ Guide rapide pour les agents qui codent dans ce repository.
- Lire `wsgi.py`, `rpc.py`, `protocols/*`, `tests/test_rpc.py`, `tests/test_wsgi.py`.
- Si bug metier:
- Modifier uniquement `modules/<module>/` + ses tests.
- Conventions de champs dates:
- Dans ce projet, ne pas introduire de `fields.DateTime`.
- Utiliser `fields.Date` pour les dates metier et les champs de suivi UI, sauf demande explicite deja existante dans le module cible.
- Si bug template Relatorio (`.fodt`):
- Lire d'abord le template standard voisin du meme domaine (`invoice.fodt`, `sale.fodt`, etc.).
- Preferer des proprietes Python simples exposees par le modele plutot que des expressions Genshi complexes dans le template.
- Dans les placeholders XML, utiliser `&quot;` et `&apos;` plutot que des antislashs type `\'`.
- Si un document facture depend fortement d'une vente/achat, ajouter au besoin un petit pont Python pour exposer des `report_*` stables au template.
- Pour les templates `stock.shipment.in`, preferer aussi des proprietes `report_*` sur le shipment plutot que des contextes ad hoc (`si_*`) quand le document devient metier ou client-specifique.
- Si plusieurs actions de report pointent vers `report_name = 'account.invoice'`, verifier aussi le cache `invoice_report_cache` dans `modules/account_invoice/invoice.py`: un mauvais cache peut faire croire que plusieurs actions utilisent le meme `.fodt`.
- Avant de conclure qu'un template ou une action est faux, verifier si le report alternatif doit bypasser le cache standard.
- Pour les templates shipment, ne pas supposer qu'une variable locale comme `shipment` sera definie partout dans Genshi, surtout dans les headers/footers; preferer `records[0]....` ou des placeholders alignes sur le scope reel du report.
- Dans `purchase_trade`, pour remonter d'une facture vers shipment, pro forma, freight ou autres donnees logistiques, privilegier le lot physique comme pont entre `purchase.line`, `sale.line` et shipment.
- Pour `FREIGHT VALUE`, ne pas lire un champ direct sur la facture: retrouver le fee de shipment (`shipment_in`) dont le produit est `Maritime freight`, puis utiliser `fee.get_amount()`.

View File

@@ -1 +0,0 @@
[0407/143111.471:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: Accès refusé. (0x5)

View File

@@ -1,36 +0,0 @@
# Fiche VPS - 46.202.173.47
Date de reference: 2026-04-07
## Identite serveur
- IP: `46.202.173.47`
- Hostname alias conseille: `vps3`
## Acces
- Cle publique Laurent Barontini (vps-deploy):
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEm8JMCYsk6I1IoYhIHXNrdyERHdh+eeDCJagOHaRAEK vps-deploy`
- Cle publique Sylvain Duvernay (s.duvernay@singa-associates.com):
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG6Xsp/v6q6JO04ETv1880qoSPptUMxlWQvgcBz67o63 s.duvernay@singa-associates.com`
- Fichier local: `$env:USERPROFILE\.ssh\id_ed25519`
- Mot de passe fourni:
`!!OpenSquared!!`
- Utilisateur SSH:
'root'
- Port SSH:
'22'
## Commande de connexion type
- Laurent Barontini (cle vps-deploy):
`ssh -i $env:USERPROFILE\.ssh\vps_deploy_key <user>@46.202.173.47`
- Sylvain Duvernay (cle id_ed25519):
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47`
- Avec port custom:
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 -p <port> <user>@46.202.173.47`

View File

@@ -0,0 +1,3 @@
Serveur: 'VPS-62.72.36.116'
Alias Name: 'VPS DEV'
IP Address:'62.72.36.116'

View File

@@ -288,7 +288,7 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.action.report" id="report_invoice">
<field name="name">Invoice</field>
<field name="name">Provisional Invoice</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/invoice.fodt</field>
@@ -314,7 +314,7 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.action.report" id="report_invoice_ict_final">
<field name="name">CN/DN</field>
<field name="name">Final Invoice</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/invoice_ict_final.fodt</field>

View File

@@ -2,7 +2,7 @@
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:rpt="http://openoffice.org/2005/report" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:officeooo="http://openoffice.org/2009/office" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<dc:title>Invoice</dc:title>
<dc:title>Provisional Invoice</dc:title>
<meta:initial-creator>willen</meta:initial-creator>
@@ -3866,7 +3866,7 @@
<table:table-row table:style-name="Tableau3.1">
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P22"/>
<text:p text:style-name="P22">Invoice</text:p>
<text:p text:style-name="P22">Provisional Invoice</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P22"/>
@@ -3930,20 +3930,15 @@
<text:p text:style-name="P25">Goods description</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau5.A1" office:value-type="string">
<text:p text:style-name="P26">QUANTITY: <text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_lbs, invoice.party.lang) if invoice.report_lbs != &apos;&apos; else &apos;&apos;&gt;</text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_net, invoice.party.lang) if invoice.report_net != &apos;&apos; else &apos;&apos;&gt;</text:placeholder> <text:placeholder text:placeholder-type="text">&lt;invoice.report_weight_unit_upper&gt;</text:placeholder>)</text:p>
<text:p text:style-name="P26"/>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;invoice.report_description_upper or invoice.report_product_description&gt;</text:placeholder><text:placeholder text:placeholder-type="text">&lt;&apos; CROP &apos; + invoice.report_crop_name if invoice.report_crop_name else &apos;&apos;&gt;</text:placeholder></text:p>
<text:p text:style-name="P26">QUANTITY: <text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_lbs, invoice.party.lang) if invoice.report_lbs != &apos;&apos; else &apos;&apos;&gt;</text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_net, invoice.party.lang) if invoice.report_net != &apos;&apos; else &apos;&apos;&gt;</text:placeholder> MTS)</text:p>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;invoice.report_description_upper or invoice.report_product_description&gt;</text:placeholder><text:s/>CROP <text:placeholder text:placeholder-type="text">&lt;invoice.report_crop_name&gt;</text:placeholder></text:p>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;invoice.report_attributes_name&gt;</text:placeholder></text:p>
<text:p text:style-name="P26"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;block in invoice.report_trade_blocks&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P26"><text:placeholder text:placeholder-type="text">&lt;block[0]&gt;</text:placeholder></text:p>
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text">&lt;block[1]&gt;</text:placeholder></text:p>
<text:p text:style-name="P18"/>
<text:p text:style-name="P26"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_currency_upper&gt;</text:placeholder><text:s/><text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_value&gt;</text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_unit_upper&gt;</text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_price_words&gt;</text:placeholder>) <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_pricing_text&gt;</text:placeholder></text:p>
<text:p text:style-name="P18"/>
<text:p text:style-name="P18"/>
<text:p text:style-name="P32"><text:placeholder text:placeholder-type="text">&lt;invoice.report_incoterm&gt;</text:placeholder></text:p>
<text:p text:style-name="P29"><text:span text:style-name="T1">ALL DETAILS AND SPECIFICATIONS AS PER</text:span> <text:span text:style-name="T3">BENEFICIARY </text:span></text:p>
<text:p text:style-name="P26"/>
<text:p text:style-name="P26">PROFORMA INVOICE NO. <text:placeholder text:placeholder-type="text">&lt;invoice.report_proforma_invoice_number&gt;</text:placeholder><text:s/>DATED <text:placeholder text:placeholder-type="text">&lt;format_date(invoice.report_proforma_invoice_date, invoice.party.lang) if invoice.report_proforma_invoice_date else &apos;&apos;&gt;</text:placeholder>.</text:p>
<text:p text:style-name="P26"/>
<text:p text:style-name="P12"/>
</table:table-cell>
@@ -3957,10 +3952,10 @@
<text:p text:style-name="P15"><text:s text:c="19"/>BALES</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau6.A1" office:value-type="string">
<text:p text:style-name="P41"><text:s text:c="13"/>Gross <text:placeholder text:placeholder-type="text">&lt;invoice.report_weight_unit_upper&gt;</text:placeholder></text:p>
<text:p text:style-name="P41"><text:s text:c="13"/>Gross KGS</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau6.A1" office:value-type="string">
<text:p text:style-name="P15"><text:s text:c="13"/>NET <text:placeholder text:placeholder-type="text">&lt;invoice.report_weight_unit_upper&gt;</text:placeholder></text:p>
<text:p text:style-name="P15"><text:s text:c="13"/>NET KGS</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau6.A1" office:value-type="string">
<text:p text:style-name="P41"><text:s text:c="10"/></text:p>
@@ -4014,9 +4009,7 @@
<table:table-column table:style-name="Tableau8.B"/>
<table:table-row table:style-name="Tableau8.1">
<table:table-cell table:style-name="Tableau8.A1" office:value-type="string">
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in invoice.report_rate_lines.splitlines()&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P14">At <text:placeholder text:placeholder-type="text">&lt;line&gt;</text:placeholder></text:p>
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P14">At <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_currency_upper&gt;</text:placeholder><text:s/><text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_value&gt;</text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_unit_upper&gt;</text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_price_words&gt;</text:placeholder>) <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_pricing_text&gt;</text:placeholder></text:p>
<text:p text:style-name="P14"/>
<text:p text:style-name="P14">FREIGHT VALUE: <text:placeholder text:placeholder-type="text">&lt;invoice.report_freight_currency_symbol&gt;</text:placeholder><text:s/><text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_freight_amount, invoice.party.lang) if invoice.report_freight_amount != &apos;&apos; else &apos;&apos;&gt;</text:placeholder></text:p>
<text:p text:style-name="P14"/>

View File

@@ -2,7 +2,7 @@
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:rpt="http://openoffice.org/2005/report" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:tableooo="http://openoffice.org/2009/table" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:drawooo="http://openoffice.org/2010/draw" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:formx="urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:grddl="http://www.w3.org/2003/g/data-view#" xmlns:css3t="http://www.w3.org/TR/css3-text/" xmlns:officeooo="http://openoffice.org/2009/office" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<dc:title>Credit / Debit Note</dc:title>
<dc:title>Final Invoice</dc:title>
<meta:initial-creator>willen</meta:initial-creator>
<meta:creation-date>2018-12-09T16:20:00</meta:creation-date>
<dc:date>2026-03-27T08:01:16.333000000</dc:date>
@@ -3852,7 +3852,7 @@
<table:table-row table:style-name="Tableau3.1">
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P23"/>
<text:p text:style-name="P23"><text:placeholder text:placeholder-type="text">&lt;invoice.report_note_title&gt;</text:placeholder></text:p>
<text:p text:style-name="P23">Final Invoice</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
<text:p text:style-name="P23"/>
@@ -3916,19 +3916,23 @@
<text:p text:style-name="P26">Goods description</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau5.A1" office:value-type="string">
<text:p text:style-name="P27">QUANTITY: <text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_lbs, invoice.party.lang) if invoice.report_lbs != &apos;&apos; else &apos;&apos;&gt;</text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_net, invoice.party.lang) if invoice.report_net != &apos;&apos; else &apos;&apos;&gt;</text:placeholder> <text:placeholder text:placeholder-type="text">&lt;invoice.report_weight_unit_upper&gt;</text:placeholder>)</text:p>
<text:p text:style-name="P27"/>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;invoice.report_description_upper or invoice.report_product_description&gt;</text:placeholder><text:placeholder text:placeholder-type="text">&lt;&apos; CROP &apos; + invoice.report_crop_name if invoice.report_crop_name else &apos;&apos;&gt;</text:placeholder></text:p>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;invoice.report_attributes_name&gt;</text:placeholder></text:p>
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;block in invoice.report_trade_blocks&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text">&lt;block[0]&gt;</text:placeholder></text:p>
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text">&lt;block[1]&gt;</text:placeholder></text:p>
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in invoice.lines&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P28"><text:placeholder text:placeholder-type="text">&lt;if test=&quot;line.type == &apos;line&apos;&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P22"><text:placeholder text:placeholder-type="text">&lt;if test=&quot;line.report_description_upper&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P22"><text:placeholder text:placeholder-type="text">&lt;line.report_description_upper&gt;</text:placeholder></text:p>
<text:p text:style-name="P22"><text:placeholder text:placeholder-type="text">&lt;/if&gt;</text:placeholder></text:p>
<text:p text:style-name="P27">QUANTITY <text:placeholder text:placeholder-type="text">&lt;format_number(line.report_lbs, invoice.party.lang) if line.report_lbs != &apos;&apos; else &apos;&apos;&gt;</text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text">&lt;format_number(line.report_net, invoice.party.lang) if line.report_net != &apos;&apos; else &apos;&apos;&gt;</text:placeholder> MTS)</text:p>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;line.report_product_description or line.product_name or &apos;&apos;&gt;</text:placeholder><text:s/>CROP <text:placeholder text:placeholder-type="text">&lt;line.report_crop_name&gt;</text:placeholder></text:p>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;line.report_attributes_name&gt;</text:placeholder></text:p>
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text">&lt;line.report_rate_currency_upper&gt;</text:placeholder><text:s/><text:placeholder text:placeholder-type="text">&lt;line.report_rate_value&gt;</text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text">&lt;line.report_rate_unit_upper&gt;</text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text">&lt;line.report_rate_price_words&gt;</text:placeholder>) <text:placeholder text:placeholder-type="text">&lt;line.report_rate_pricing_text&gt;</text:placeholder></text:p>
<text:p text:style-name="P18"/>
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;/if&gt;</text:placeholder></text:p>
<text:p text:style-name="P18"/>
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P21"/>
<text:p text:style-name="P34"><text:placeholder text:placeholder-type="text">&lt;invoice.report_incoterm&gt;</text:placeholder></text:p>
<text:p text:style-name="P31"><text:span text:style-name="T1">ALL DETAILS AND SPECIFICATIONS AS PER</text:span> <text:span text:style-name="T3">BENEFICIARY </text:span></text:p>
<text:p text:style-name="P27"/>
<text:p text:style-name="P27">PROFORMA INVOICE NO. <text:placeholder text:placeholder-type="text">&lt;invoice.report_proforma_invoice_number&gt;</text:placeholder><text:s/>DATED <text:placeholder text:placeholder-type="text">&lt;format_date(invoice.report_proforma_invoice_date, invoice.party.lang) if invoice.report_proforma_invoice_date else &apos;&apos;&gt;</text:placeholder>.</text:p>
<text:p text:style-name="P27"/>
<text:p text:style-name="P12"/>
</table:table-cell>
@@ -3942,10 +3946,10 @@
<text:p text:style-name="P15"><text:s text:c="19"/>BALES</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau6.A1" office:value-type="string">
<text:p text:style-name="P43"><text:s text:c="13"/>Gross <text:placeholder text:placeholder-type="text">&lt;invoice.report_weight_unit_upper&gt;</text:placeholder></text:p>
<text:p text:style-name="P43"><text:s text:c="13"/>Gross KGS</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau6.A1" office:value-type="string">
<text:p text:style-name="P15"><text:s text:c="13"/>NET <text:placeholder text:placeholder-type="text">&lt;invoice.report_weight_unit_upper&gt;</text:placeholder></text:p>
<text:p text:style-name="P15"><text:s text:c="13"/>NET KGS</text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau6.A1" office:value-type="string">
<text:p text:style-name="P43"><text:s text:c="10"/></text:p>
@@ -3999,9 +4003,7 @@
<table:table-column table:style-name="Tableau8.B"/>
<table:table-row table:style-name="Tableau8.1">
<table:table-cell table:style-name="Tableau8.A1" office:value-type="string">
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in invoice.report_positive_rate_lines.splitlines()&quot;&gt;</text:placeholder></text:p>
<text:p text:style-name="P14">At <text:placeholder text:placeholder-type="text">&lt;line&gt;</text:placeholder></text:p>
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
<text:p text:style-name="P14">At <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_currency_upper&gt;</text:placeholder><text:s/><text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_value&gt;</text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_unit_upper&gt;</text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_price_words&gt;</text:placeholder>) <text:placeholder text:placeholder-type="text">&lt;invoice.report_rate_pricing_text&gt;</text:placeholder></text:p>
<text:p text:style-name="P14"/>
<text:p text:style-name="P14"><text:soft-page-break/>FREIGHT VALUE: <text:placeholder text:placeholder-type="text">&lt;invoice.report_freight_currency_symbol&gt;</text:placeholder><text:s/><text:placeholder text:placeholder-type="text">&lt;format_number(invoice.report_freight_amount, invoice.party.lang) if invoice.report_freight_amount != &apos;&apos; else &apos;&apos;&gt;</text:placeholder></text:p>
<text:p text:style-name="P14"/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -248,10 +248,29 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
ShipmentWR.save([swr])
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
logger.info("BL_NUMBER:%s",sh[0].bl_number)
doc.notes = (
(doc.notes or "")
+ "Global WR linked to shipment. "
+ "Create remote lot WRs from the weight report form.\n")
if sh[0].incoming_moves:
factor_net = wr.net_landed_kg / wr.bales if wr.bales else 1
factor_gross = wr.gross_landed_kg / wr.bales if wr.bales else 1
for move in sh[0].incoming_moves:
lot = move.lot
if lot.lot_type == 'physic':
wr_payload = {
"chunk_key": lot.lot_chunk_key,
"gross_weight": float(round(Decimal(lot.lot_qt) * factor_gross,5)),
"net_weight": float(round(Decimal(lot.lot_qt) * factor_net,5)),
"tare_total": float(round(wr.tare_kg * (Decimal(lot.lot_qt) / wr.bales),5)) ,
"bags": int(lot.lot_qt),
"surveyor_code": sh[0].controller.get_alf(),
"place_key": sh[0].to_location.get_places(),
"report_date": int(wr.report_date.strftime("%Y%m%d")),#wr.report_date.isoformat() if wr.report_date else None,
"weight_date": int(wr.weight_date.strftime("%Y%m%d")),#wr.weight_date.isoformat() if wr.weight_date else None,
"agent": sh[0].agent.get_alf(),
"forwarder_ref": sh[0].returned_id
}
logger.info("PAYLOAD:%s",wr_payload)
data = doc.create_weight_report(wr_payload)
doc.notes = (doc.notes or "") + f"WR created in Fintrade: {data.get('success')}\n"
doc.notes = (doc.notes or "") + f"WR key: {data.get('weight_report_key')}\n"
# if cls.rule_set.ocr_required:[]
# cls.run_ocr([doc])
@@ -274,4 +293,4 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
# except Exception as e:
# doc.state = "error"
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
doc.save()
doc.save()

View File

@@ -1,168 +0,0 @@
# AGENTS.md - Module `purchase_trade`
Ce guide complete le `AGENTS.md` racine.
Pour ce module, les regles locales ci-dessous priment.
## 1) Perimetre metier
Le module `purchase_trade` etend les flux achat/vente Tryton avec une logique
de negoce physique:
- contrats d'achat (`purchase.purchase`, `purchase.line`)
- contrats de vente (`sale.sale`, `sale.line`)
- lots physiques et virtuels
- matching achat/vente
- shipments et execution logistique
- frais (`fee.fee`)
- templates de documents metier et facture
## 2) Fichiers pivots
- Contrats achat:
- `modules/purchase_trade/purchase.py`
- Contrats vente:
- `modules/purchase_trade/sale.py`
- Lots / matching / invoicing:
- `modules/purchase_trade/lot.py`
- Shipments / lien facture-lot:
- `modules/purchase_trade/stock.py`
- Fees:
- `modules/purchase_trade/fee.py`
- Bridge facture / templates:
- `modules/purchase_trade/invoice.py`
- Vues:
- `modules/purchase_trade/view/*.xml`
- Actions module:
- `modules/purchase_trade/*.xml`
- Manifest:
- `modules/purchase_trade/tryton.cfg`
## 3) Documentation locale a lire en priorite
- Regles metier:
- `modules/purchase_trade/docs/business-rules.md`
- Regles templates:
- `modules/purchase_trade/docs/template-rules.md`
- Catalogue des proprietes templates:
- `modules/purchase_trade/docs/template-properties.md`
## 4) Invariants metier a preserver
- Un lot `virtual` est la reference d'ouverture de quantite pour une `purchase.line`.
- Une `sale.line` doit aussi avoir au minimum un lot `virtual`; une valuation
cote sale ne doit donc pas disparaitre juste parce que le lot est `open`.
- Le lot physique est le pont principal entre:
- `purchase.line`
- `sale.line`
- shipment
- facture
- Pour remonter d'une facture vers shipment / BL / controller / fret:
- privilegier le lot physique
- ne pas multiplier des chemins d'acces concurrents
- Pour les champs de colis (`NB BALES`) dans les templates facture:
- la source de verite est `line.lot.lot_qt`
- sur une facture, sommer les `lot_qt` des lignes de facture
- tenir compte du signe de la ligne de facture pour les notes finales
- ne pas proratiser depuis le poids (`net` / `gross`)
- Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment
dont le produit est `Maritime freight`.
- Pour `stock/insurance.fodt`, le `Amount insured` doit venir en priorite de
`110%` du total des `incoming_moves` (fallback fee `Insurance` si aucun
montant incoming calculable).
- Pour le surveyor du certificat d'assurance shipment, la priorite est:
`shipment.surveyor` -> `shipment.controller` -> fournisseur du fee
`Insurance`.
- Pour `payment_order.fodt`, utiliser des proprietes
`invoice.report_payment_order_*` plutot que des tokens legacy `<...>`.
- Ajouter un champ de template dans `Document Templates` ne rend pas le report
visible dans la fiche: il faut aussi l'action `ir.action.report` +
`ir.action.keyword` (`form_print`) cote `account.invoice`.
- Le wizard `Create contracts` en mode `matched` peut maintenant partir de
plusieurs `lot.qt`, mais doit conserver un matching par lot source et laisser
`created_by_code = True` sur les lignes creees pour ne pas declencher les
creations automatiques de lots dans les validations.
- En valuation / PnL:
- la valeur stockee dans `type` est la cle technique (`pur. priced`,
`sale priced`, `pur. fee`, etc.), pas le label affiche dans l'UI
- les references doivent rester coherentes avec le type de lot:
`Purchase/Open`, `Purchase/Physic`, `Sale/Open`, `Sale/Physic`
- pour une sale matchee, les lignes de valuation purchase generees sur un lot
physique doivent aussi renseigner `sale` et `sale_line` afin de remonter
dans l'onglet PnL de la sale
- une sale non matchee doit etre valorisable "sale-first" et alimenter
`valuation.valuation` / `valuation.valuation.line`
- si une `sale.line` `basis` n'a ni `price_summary` ni `lot_price_sale`,
creer quand meme une ligne `sale priced` avec `price = 0` et `amount = 0`
plutot que de ne rien generer
- le MTM ne doit etre renseigne que pour `pur. priced`, `sale priced` et
`derivative`; jamais pour les fees
- `mtm_price` doit afficher le prix brut de valorisation (sans ratio), alors
que `mtm` reste le montant calcule selon la logique de strategie
- En pricing:
- le `unit_price` doit rester un prix de base, hors `premium`
- le `premium` doit impacter le prix total economique et donc le `amount`,
aussi bien en `priced` qu'en `basis`
- pour les documents commerciaux / facture, une ligne `basis` affiche le
`premium` comme prix visible, pas le prix economique total
- si `linked currency` est active, le `premium` est saisi dans la devise /
unite liee (ex: `USC/LB`) puis converti vers le repere de la ligne pour le
calcul du `amount`
- en `basis + linked currency`, le `linked_price` doit representer le prix
basis brut (hors premium) dans la devise liee; le `unit_price` reste ce
prix brut converti, et le `premium` converti est ajoute seulement dans
l'`amount`
- si `linked currency` est cochee, `linked_price`, `linked_currency` et
`linked_unit` sont requis
- dans les forms, presenter le bloc prix dans l'ordre:
`price_type` -> linked fields -> `premium` -> `unit_price` -> `amount`
- en valuation `basis`, le premium s'applique a chaque composant, pas
uniquement a une ligne de resume
- pour une ligne `basis` sans `price_summary`, la valuation fallback doit
utiliser `unit_price + premium` (et pas `unit_price` seul)
- a la validation d'une `sale.line`, si un lot virtuel est cree et qu'aucun
matching purchase n'existe, il faut lancer `generate_from_sale_line()` pour
alimenter le PnL sale-first
## 5) Conventions de modification
1. Modifier la logique metier dans le fichier pivot le plus proche.
2. Si un template `.fodt` devient complexe, deplacer la logique dans une
propriete Python `report_*`.
3. Pour une facture trade, preferer enrichir `modules/purchase_trade/invoice.py`
plutot que surcharger lourdement le `.fodt`.
4. Si une regle metier durable change, mettre a jour
`docs/business-rules.md`.
5. Si une convention de template change, mettre a jour
`docs/template-rules.md`.
6. Pour les vues XML Tryton de ce module, utiliser `editable="1"` sur les
`<tree>` editables; ne pas utiliser `editable="bottom"`.
7. Si une regle de texte par defaut durable est demandee sur achat/vente,
preferer un singleton de configuration expose dans un menu fonctionnel
existant plutot qu'un menu technique `purchase_trade`.
## 6) Pieges connus
- Plusieurs actions de report `account.invoice` peuvent sembler rendre le meme
document a cause du cache `invoice_report_cache`.
- Les reports alternatifs (`Final Invoice`, `Prepayment`, etc.) ne doivent pas
reutiliser le cache du report standard sans verification.
- Pour les donnees achat/vente partagees, ne pas supposer qu'une facture de
vente doit lire directement sur la `sale.line`: souvent, la verite metier
passe par le lot physique et/ou la `account.invoice.line`.
- Les templates `invoice_ict*` peuvent partager les memes proprietes Python;
si une regle doit valoir pour provisional et final, la mettre dans
`modules/purchase_trade/invoice.py` plutot que dupliquer dans les `.fodt`.
- Dans les ecrans PnL, le label `Sale price` correspond au type stocke
`sale priced`; idem pour `Pur. price` / `pur. priced`.
- Une ligne `basis` sans resume de pricing peut sinon disparaitre de la
valuation si aucun fallback explicite a `0` n'est prevu.
- Le calcul du prix peut diverger entre `unit_price`, `linked_price`,
`lot_price` et valuation si le premium n'est pas traite explicitement dans
chaque maillon.
## 7) Definition of done (module `purchase_trade`)
- Le flux achat/vente/lot cible reste coherent.
- Les impacts templates/facture ont ete verifies conceptuellement.
- Les docs locales ont ete mises a jour si une nouvelle regle durable a emerge.
- Le patch reste minimal et local au domaine demande.

View File

@@ -3,10 +3,9 @@
from trytond.pool import Pool
from . import (
account,
configuration,
purchase,
from . import (
account,
purchase,
sale,
global_reporting,
stock,
@@ -55,10 +54,9 @@ def register():
incoming.ImportSwift,
lc.LCMT700,
lc.LCMessage,
lc.CreateLCStart,
global_reporting.GRConfiguration,
configuration.Configuration,
module='purchase_trade', type_='model')
lc.CreateLCStart,
global_reporting.GRConfiguration,
module='purchase_trade', type_='model')
Pool.register(
incoming.ImportSwift,
incoming.PrepareDocuments,
@@ -107,13 +105,10 @@ def register():
purchase.FeeLots,
valuation.Valuation,
valuation.ValuationLine,
valuation.ValuationDyn,
valuation.ValuationReport,
valuation.ValuationReportContext,
valuation.ValuationProcessDimension,
valuation.ValuationProcessStart,
valuation.ValuationProcessResult,
derivative.Derivative,
valuation.ValuationDyn,
valuation.ValuationReport,
valuation.ValuationReportContext,
derivative.Derivative,
derivative.DerivativeMatch,
derivative.MatchWizardStart,
derivative.DerivativeReport,
@@ -263,22 +258,14 @@ def register():
purchase.InvoicePayment,
stock.ImportSoFWizard,
dashboard.BotWizard,
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
purchase.PositionReport,
valuation.ValuationProcess,
derivative.DerivativeMatchWizard,
module='purchase', type_='wizard')
Pool.register(
sale.SaleCreatePurchase,
sale.SaleAllocationsWizard,
module='sale', type_='wizard')
Pool.register(
invoice.InvoiceReport,
invoice.SaleReport,
invoice.PurchaseReport,
stock.ShipmentShippingReport,
stock.ShipmentInsuranceReport,
module='purchase_trade', type_='report')
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
purchase.PositionReport,
derivative.DerivativeMatchWizard,
module='purchase', type_='wizard')
Pool.register(
sale.SaleCreatePurchase,
sale.SaleAllocationsWizard,
module='sale', type_='wizard')

View File

@@ -1,18 +0,0 @@
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
class Configuration(ModelSingleton, ModelSQL, ModelView):
"Purchase Trade Configuration"
__name__ = 'purchase_trade.configuration'
pricing_rule = fields.Text("Pricing Rule")
sale_report_template = fields.Char("Sale Template")
sale_bill_report_template = fields.Char("Sale Bill Template")
sale_final_report_template = fields.Char("Sale Final Template")
invoice_report_template = fields.Char("Invoice Template")
invoice_cndn_report_template = fields.Char("CN/DN Template")
invoice_prepayment_report_template = fields.Char("Prepayment Template")
invoice_payment_order_report_template = fields.Char("Payment Order Template")
purchase_report_template = fields.Char("Purchase Template")
shipment_shipping_report_template = fields.Char("Shipping Template")
shipment_insurance_report_template = fields.Char("Insurance Template")

View File

@@ -1,48 +0,0 @@
<tryton>
<data>
<record model="ir.ui.view" id="purchase_trade_configuration_view_form">
<field name="model">purchase_trade.configuration</field>
<field name="type">form</field>
<field name="name">configuration_form</field>
</record>
<record model="ir.ui.view" id="purchase_trade_template_configuration_view_form">
<field name="model">purchase_trade.configuration</field>
<field name="type">form</field>
<field name="name">template_configuration_form</field>
</record>
<record model="ir.action.act_window" id="act_purchase_trade_configuration_form">
<field name="name">Pricing Configuration</field>
<field name="res_model">purchase_trade.configuration</field>
</record>
<record model="ir.action.act_window.view" id="act_purchase_trade_configuration_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="purchase_trade_configuration_view_form"/>
<field name="act_window" ref="act_purchase_trade_configuration_form"/>
</record>
<record model="ir.action.act_window" id="act_purchase_trade_template_configuration_form">
<field name="name">Document Templates</field>
<field name="res_model">purchase_trade.configuration</field>
</record>
<record model="ir.action.act_window.view" id="act_purchase_trade_template_configuration_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="purchase_trade_template_configuration_view_form"/>
<field name="act_window" ref="act_purchase_trade_template_configuration_form"/>
</record>
<menuitem
name="Configuration"
parent="price.menu_price"
action="act_purchase_trade_configuration_form"
sequence="10"
id="menu_purchase_trade_configuration"
icon="tryton-settings"/>
<menuitem
name="Document Templates"
parent="document_incoming.menu_configuration"
action="act_purchase_trade_template_configuration_form"
sequence="20"
id="menu_purchase_trade_template_configuration"
icon="tryton-settings"/>
</data>
</tryton>

View File

@@ -1,8 +1,8 @@
# Business Rules - Purchase Trade
Statut: `draft`
Version: `v0.4`
Derniere mise a jour: `2026-04-02`
Version: `v0.2`
Derniere mise a jour: `2026-03-27`
Owner metier: `a completer`
Owner technique: `a completer`
@@ -100,290 +100,6 @@ Owner technique: `a completer`
- Priorite:
- `importante`
### BR-PT-004 - La valuation doit couvrir les flux purchase et sale, y compris les sales non matchees
- Intent: obtenir un PnL coherent cote achat et cote vente, meme lorsqu'une
sale n'est pas encore matchee a une purchase.
- Description:
- Le flux historique de valuation part de `purchase.line` puis remonte vers
les ventes via les lots/lots matchants.
- Le systeme doit egalement savoir valoriser directement une `sale.line`
non matchee ("sale-first").
- Une sale non matchee doit creer des lignes dans
`valuation.valuation` et `valuation.valuation.line` afin d'apparaitre dans
l'onglet PnL de la sale.
- Resultat attendu:
- pour une `sale.line` non matchee, generer au minimum les types:
- `sale priced`
- `sale fee`
- `derivative` si la ligne porte des derives
- si la sale est matchee via un lot physique, les lignes purchase portees par
ce lot physique doivent aussi renseigner `sale` et `sale_line`
- une sale matchee doit donc voir:
- ses lignes `sale *`
- les lignes purchase portees par le lot physique partage
- Priorite:
- `structurante`
### BR-PT-005 - Les references de valuation doivent decrire la nature du lot de la ligne
- Intent: eviter les ambiguïtes dans les ecrans PnL entre lots `open` et lots
`physic`.
- Description:
- La reference affichee dans la valuation doit decrire la ligne elle-meme,
pas son vis-a-vis.
- Les references autorisees pour les lignes de prix sont:
- `Purchase/Open`
- `Purchase/Physic`
- `Sale/Open`
- `Sale/Physic`
- Resultat attendu:
- un lot `virtual` cote purchase ne doit jamais sortir avec la reference
`Purchase/Physic`
- un lot `virtual` cote sale ne doit jamais sortir avec la reference
`Sale/Physic`
- un lot physique matche peut produire:
- une ligne purchase en `Purchase/Physic`
- une ligne sale en `Sale/Physic`
- un open sale matche a un open purchase peut produire des quantites egales
tout en gardant des references differentes (`Purchase/Open` vs `Sale/Open`)
- Priorite:
- `importante`
### BR-PT-006 - Une sale basis sans prix detaille doit quand meme apparaitre en valuation
- Intent: ne pas perdre les lignes de PnL lorsque le detail de pricing n'est
pas encore renseigne.
- Description:
- Une `sale.line` de type `basis` peut exister avec un lot `virtual`, sans
`price_summary` et sans `lot_price_sale`.
- Dans ce cas, la valuation doit quand meme creer une ligne `sale priced`.
- Resultat attendu:
- si `price_summary` est vide:
- creer une ligne `sale priced`
- avec `price = 0`
- avec `amount = 0`
- avec un `state` de type `unfixed`
- si `lot_price_sale` est vide sur un lot sale, utiliser `sale_line.unit_price`
comme fallback quand il existe
- Priorite:
- `importante`
### BR-PT-007 - Le MTM de valuation ne s'applique pas aux fees
- Intent: distinguer les lignes de prix marquables au marche des lignes de
frais qui ne doivent pas etre mark-to-market.
- Description:
- Le systeme peut renseigner `mtm_price`, `mtm` et `strategy` uniquement pour:
- `pur. priced`
- `sale priced`
- `derivative`
- Les fees (`pur. fee`, `sale fee`, `shipment fee`, `line fee`) ne doivent
jamais porter de valorisation MTM.
- Resultat attendu:
- les lignes de fee doivent conserver:
- `mtm_price = NULL`
- `mtm = NULL`
- `strategy = NULL`
- `mtm_price` doit representer le prix brut de valorisation sans appliquer le
ratio de composant
- `mtm` reste le montant calcule selon la logique de strategie
- Priorite:
- `structurante`
### BR-PT-008 - Le premium fait partie du prix contractuel en `priced` et en `basis`
- Intent: garantir que le montant total valorise et facture reflete toujours le
premium/discount saisi sur la ligne.
- Description:
- Le `premium` d'une `purchase.line` ou `sale.line` doit impacter le prix
total quelle que soit la `price_type`.
- Cette regle vaut pour:
- les calculs de `amount`
- la valuation / PnL
- Resultat attendu:
- le `unit_price` reste le prix de base, hors premium
- en `priced`, le montant economique = `unit_price + premium`
- en `basis`, le premium s'ajoute aussi au prix total economique
- en valuation `basis`, le premium s'applique a chaque composant valorise
(ex: meme premium repete sur chaque bloc ICE)
- Exemple metier:
- `8.30 USC/LB 500 TONS ON ICE MCH'26`
- `8.30 USC/LB 500 TONS ON ICE MAY 26`
- le premium `8.30 USC/LB` s'applique a chaque composant
- Priorite:
- `structurante`
### BR-PT-009 - En linked currency, le premium est exprime dans la devise/unite liee
- Intent: respecter la facon dont les traders saisissent les prix sur certains
produits (ex: coton en `USC/LB`).
- Description:
- Quand `enable_linked_currency` est coche, le `premium` est saisi dans la
devise / unite liee, pas dans la devise / unite native de la ligne.
- Le systeme doit convertir ce premium vers le repere de la ligne pour les
calculs internes de montant et de valuation.
- Resultat attendu:
- `premium` est interprete dans le repere `linked_currency` / `linked_unit`
- le `unit_price` ne doit pas absorber ce premium
- les `amount` et valuations doivent refleter ce premium converti
- si `linked currency` est cochee, `linked_price`, `linked_currency` et
`linked_unit` sont obligatoires
- Priorite:
- `structurante`
### BR-PT-010 - En `basis + linked currency`, le linked price suit le basis brut
- Intent: rendre lisible la decomposition entre prix basis de marche et premium.
- Description:
- Quand une ligne est en `basis` et `linked currency`, le bloc
`linked_price` doit etre recalcule automatiquement.
- Ce `linked_price` doit representer le prix basis brut, hors premium.
- Le `unit_price` de la ligne doit rester ce prix brut converti.
- Le premium converti n'est ajoute qu'au niveau du `amount`.
- Resultat attendu:
- modification du basis -> mise a jour automatique du `linked_price`
- `linked_price` = base market / basis
- `unit_price` = `linked_price` converti
- `amount` = quantite * (`unit_price` + premium converti)
- Priorite:
- `importante`
### BR-PT-011 - Une sale line non matchee avec lot virtuel doit generer une valuation sale-first des la validation
- Intent: ne pas attendre un matching purchase pour afficher le PnL d'une sale
ouverte.
- Description:
- Lors de la validation d'une `sale.line`, le systeme peut creer un lot
`virtual`.
- Si aucun `lot.qt` ne relie ce lot a une `purchase.line`, il faut tout de
meme generer la valuation cote sale.
### BR-PT-012 - Le wizard Create contracts peut creer un seul achat matche a plusieurs open sales
- Intent: permettre la creation d'un contrat achat unique a partir de plusieurs
`lot.qt` de vente selectionnes.
- Description:
- En mode `matched`, le wizard `Create contracts` peut recevoir plusieurs
`lot.qt` selectionnes.
- Il doit creer un seul contrat, avec une ligne par lot source selectionne.
- Chaque ligne doit conserver son lot d'origine pour le matching.
- Resultat attendu:
- le wizard agrege les quantites de la selection
- il refuse une quantite saisie differente du total selectionne
- il conserve `created_by_code = True` sur les lignes creees pour ne pas
declencher les creations automatiques parasites lors des validations
- Priorite:
- `importante`
### BR-PT-013 - Le texte par defaut de pricing_rule est configure globalement
- Intent: centraliser un texte metier recurrent reutilise a la creation des
lignes achat et vente.
- Description:
- Le module expose un singleton `purchase_trade.configuration` avec un champ
texte `pricing_rule`.
- Toute nouvelle `purchase.line` et `sale.line` doit prendre ce texte comme
valeur par defaut de `pricing_rule`.
- Resultat attendu:
- la configuration est accessible depuis le menu `Prices`
- la valeur sert de defaut a la creation des lignes
- les lignes existantes ne sont pas modifiees retroactivement
- Priorite:
- `importante`
### BR-PT-014 - L'affectation d'un controller doit suivre l'ecart a l'objectif regional
- Intent: repartir les controllers selon les cibles definies dans l'onglet
`Execution` des `party.party`.
- Description:
- chaque ligne `party.execution` fixe une cible `% targeted` pour un
controller sur une `country.region`
- le `% achieved` est calcule a partir des `stock.shipment.in` deja affectes
a un controller dans cette zone
- la zone d'un shipment est determinee par `shipment.to_location.country`
- une region parente couvre aussi ses sous-regions
- Resultat attendu:
- pour une ligne `party.execution`, `achieved_percent` =
`shipments de la zone avec ce controller / shipments controles de la zone`
- le denominateur ne compte que les `stock.shipment.in` qui ont deja un
`controller`; les shipments encore non affectes ne biaisent donc pas la
statistique affichee
- lors d'un choix automatique de controller, la priorite va a la regle dont
l'ecart `targeted - achieved` est le plus eleve
- un controller a `80%` cible et `40%` reel doit donc passer avant un
controller a `50%` cible et `45%` reel sur la meme zone
- l'appartenance a la zone se lit depuis `shipment.to_location.country`, et
une region parente couvre aussi ses sous-regions
- Priorite:
- `importante`
### BR-PT-015 - Les weight reports distants par lot partent du weight report global attache au shipment
- Intent: separer la creation du `weight.report` global et l'export detaille
par lot vers le systeme distant.
- Description:
- l'automation cree le `weight.report` global et l'attache au
`stock.shipment.in`
- l'export FastAPI par lot ne part plus directement de l'automation
- l'utilisateur ouvre le `weight.report` voulu depuis le shipment et lance
l'action d'export depuis ce rapport
- Resultat attendu:
- le rapport choisi sert de base unique pour calculer les payloads par lot
- seuls les lots physiques des `incoming_moves` du shipment sont exportes
- l'action exige au minimum un `controller` et un `returned_id` sur le
shipment
- les cles renvoyees par le systeme distant et la date d'envoi sont
conservees sur le `weight.report` local
- Priorite:
- `importante`
- Resultat attendu:
- apres creation du lot virtuel, si aucun matching purchase n'existe:
- appeler `Valuation.generate_from_sale_line(line)`
- creer au moins la ligne `sale priced` fallback si la ligne porte un prix
economique via le premium
- Priorite:
- `importante`
### BR-PT-012 - Fallback valuation basis sans summary: utiliser le prix economique de la ligne
- Intent: eviter qu'une valuation `basis` ouverte sorte a zero alors que la
ligne a bien une valeur economique via le premium.
- Description:
- Une ligne `basis` peut ne pas avoir encore de `price_summary`.
- Dans ce cas, la valuation fallback ne doit pas prendre `unit_price` seul si
celui-ci est brut et hors premium.
- Resultat attendu:
- le fallback valuation `basis` doit utiliser:
- `unit_price + premium converti`
- cette regle vaut au minimum pour:
- `sale.line` non matchee
- `purchase.line` sans summary
- Priorite:
- `importante`
### BR-PT-013 - Create Contracts multi-lots doit conserver un matching par lot source
- Intent: permettre la creation d'un seul contrat mirror a partir de plusieurs
open quantities sans perdre le lien lot-a-lot.
- Description:
- Le wizard `Create contracts` peut etre lance avec plusieurs `lot.qt`
selectionnes.
- En creation `matched`, le systeme doit creer un seul contrat avec une ligne
par lot source selectionne, et chaque ligne doit etre matchee avec son lot
d'origine.
- Resultat attendu:
- la quantite totale du wizard = somme des open quantities selectionnees
- le contrat cree porte plusieurs lignes si plusieurs lots source sont
selectionnes
- chaque ligne creee reutilise le `shipment_origin` et le lot source qui lui
correspondent
- `created_by_code` doit rester positionne sur les lignes creees par wizard
pour eviter la recreation automatique de lots virtuels dans les `validate`
de `purchase.line`, `sale.line` et `lot.lot`
- Priorite:
- `importante`
## 4) Exemples concrets
### Exemple E1 - Augmentation simple
@@ -430,8 +146,6 @@ Owner technique: `a completer`
- Fichiers Python concernes:
- `modules/purchase_trade/purchase.py`
- `modules/purchase_trade/lot.py`
- `modules/purchase_trade/valuation.py`
- `modules/purchase_trade/sale.py`
## 6) Strategie de tests
@@ -441,11 +155,3 @@ Pour cette regle, couvrir au minimum:
- augmentation sans `lot.qt` ouvert
- diminution possible
- diminution impossible avec erreur
- valuation purchase/sale sur lot physique matche
- valuation sale-first sur sale non matchee avec lot virtual
- valuation sale `basis` sans `price_summary`
- absence de MTM sur les fees
- premium en `priced`
- premium en `basis`
- premium en `linked currency`
- synchro `basis` -> `linked_price` -> `unit_price`

View File

@@ -1,417 +0,0 @@
# Template Properties - Purchase Trade
Statut: `draft`
Version: `v0.2`
Derniere mise a jour: `2026-04-07`
## 1) Objectif
- Lister les proprietes Python exposees pour alimenter les templates Relatorio.
- Donner un point d'entree rapide aux createurs de templates.
- Eviter de reparser tout `modules/purchase_trade/invoice.py`, `sale.py` ou `purchase.py`.
## 2) Fichiers sources
- Bridge facture:
- `modules/purchase_trade/invoice.py`
- Proprietes de vente reutilisables:
- `modules/purchase_trade/sale.py`
- Proprietes d'achat reutilisables:
- `modules/purchase_trade/purchase.py`
## 3) Principes de lecture
- Pour une facture:
- preferer les proprietes `report_*` exposees sur `account.invoice`
- pour une facture finale detaillee, utiliser aussi les proprietes `report_*`
exposees sur `account.invoice.line`
- Pour une vente:
- reutiliser si possible les proprietes `report_*` deja presentes sur `sale.sale`
- Pour un achat:
- reutiliser si possible les proprietes `report_*` deja presentes sur `purchase.purchase`
- Pour un shipment entrant:
- reutiliser si possible les proprietes `report_*` exposees sur `stock.shipment.in`
## 4) Propriete disponibles sur `account.invoice`
Source code: `modules/purchase_trade/invoice.py`
### Identite du document / parties
- `report_address`
- Usage: adresse d'affichage de la facture
- Source de verite: `sale.report_address` ou `purchase.report_address`, fallback `invoice.invoice_address.full_address`
- `report_contract_number`
- Usage: numero de contrat
- Source de verite: `sale.full_number` ou `purchase.full_number`
- `report_trader_initial`
- Usage: initiales trader dans les templates
- Source de verite: contrat lie
- `report_operator_initial`
- Usage: initiales operator dans les templates
- Source de verite: contrat lie
### Produit / contrat / quantites
- `report_origin`
- Usage: origine produit
- Source de verite: `sale.product_origin` ou `purchase.product_origin`
- `report_product_description`
- Usage: description produit principale
- Source de verite: premiere ligne metier liee a la facture
- `report_product_name`
- Usage: nom produit principal
- Source de verite: premiere ligne metier liee a la facture
- `report_description_upper`
- Usage: description de ligne en majuscules
- Source de verite: premiere `account.invoice.line`
- `report_crop_name`
- Usage: campagne / crop
- Source de verite: contrat lie
- `report_attributes_name`
- Usage: attributs produit
- Source de verite: premiere ligne metier liee a la facture
- `report_price`
- Usage: prix en toutes lettres
- Source de verite: `sale.report_price` ou `purchase.report_price`
- `report_nb_bale`
- Usage: nombre de balles
- Source de verite: `sale.report_nb_bale` ou recalcul sur les lots physiques
- `report_gross`
- Usage: poids brut
- Source de verite: `sale.report_gross` ou recalcul sur les lots physiques
- `report_net`
- Usage: poids net
- Source de verite: `sale.report_net` ou `purchase.report_net` ou recalcul sur les lots physiques
- `report_lbs`
- Usage: poids net converti en LBS
- Source de verite: conversion de `report_net`
- `report_quantity_lines`
- Usage: detail quantite multi-lignes pour les templates facture
- Source de verite: `sale.report_quantity_lines` si vente source, sinon aggregation des `account.invoice.line`
### Bloc prix type `sale_ict`
- `report_rate_currency_upper`
- Usage: devise du bloc `At ... PER ...`
- Source de verite: premiere `account.invoice.line` de type `line`
- `report_rate_value`
- Usage: prix numerique du bloc `At ... PER ...`
- Source de verite: premiere `account.invoice.line` de type `line`
- `report_rate_unit_upper`
- Usage: unite du bloc `At ... PER ...`
- Source de verite: premiere `account.invoice.line` de type `line`
- `report_rate_price_words`
- Usage: prix en toutes lettres dans le bloc `At ... PER ...`
- Source de verite: premiere `account.invoice.line` de type `line`, fallback `report_price`
- `report_rate_pricing_text`
- Usage: texte de pricing additionnel
- Source de verite: premiere `account.invoice.line` de type `line`
- `report_rate_lines`
- Usage: detail multi-lignes du bloc `At ... PER ...`
- Source de verite: `sale.report_price_lines` si vente source, sinon aggregation des `account.invoice.line`
### Logistique / shipment
- `report_shipment`
- Usage: resume vessel / BL / shipment
- Source de verite: contrat lie
- `report_bl_date`
- Usage: date de BL
- Source de verite: shipment du lot physique
- `report_bl_nb`
- Usage: numero de BL
- Source de verite: shipment du lot physique
- `report_vessel`
- Usage: nom du vessel
- Source de verite: shipment du lot physique
- `report_loading_port`
- Usage: port of loading
- Source de verite: shipment du lot physique
- `report_discharge_port`
- Usage: port of discharge
- Source de verite: shipment du lot physique
- `report_controller_name`
- Usage: nom du controller
- Source de verite: shipment du lot physique
- `report_si_number`
- Usage: S/I number
- Source de verite: shipment du lot physique
### Conditions commerciales
- `report_incoterm`
- Usage: incoterm + location
- Source de verite: contrat lie
- `report_payment_date`
- Usage: date de paiement
- Source de verite: contrat lie
- `report_payment_description`
- Usage: description des conditions de paiement
- Source de verite: payment term du contrat ou de la facture
### Pro forma / freight
- `report_proforma_invoice_number`
- Usage: numero de facture provisoire
- Source de verite: lot physique via `invoice_line_prov` ou `sale_invoice_line_prov`
- `report_proforma_invoice_date`
- Usage: date de facture provisoire
- Source de verite: lot physique via `invoice_line_prov` ou `sale_invoice_line_prov`
- `report_freight_amount`
- Usage: `FREIGHT VALUE`
- Source de verite:
- lot physique
- shipment du lot
- `fee.fee` avec `product.name = 'Maritime freight'`
- montant = `fee.get_amount()`
- `report_freight_currency_symbol`
- Usage: devise du `FREIGHT VALUE`
- Source de verite: devise du fee `Maritime freight`, fallback devise facture
### Payment order
- `report_payment_order_short_name`
- Usage: nom court emetteur du payment order
- Source de verite: `invoice.company.party.rec_name`
- `report_payment_order_document_reference`
- Usage: reference du document payment order
- Source de verite: `invoice.number`, fallback `invoice.reference`
- `report_payment_order_from_account_nb`
- Usage: compte bancaire emetteur
- Source de verite: premier `bank.account` de la societe
- `report_payment_order_to_bank_name`
- Usage: banque destinataire
- Source de verite: banque du premier compte bancaire du partenaire facture
- `report_payment_order_to_bank_city`
- Usage: ville banque destinataire
- Source de verite: adresse de la banque destinataire
- `report_payment_order_amount`
- Usage: montant payment order
- Source de verite: `invoice.total_amount`
- `report_payment_order_currency_code`
- Usage: devise payment order
- Source de verite: `invoice.currency` (`code`, fallback `rec_name/symbol`)
- `report_payment_order_amount_text`
- Usage: montant en lettres
- Source de verite: conversion `amount_to_currency_words(invoice.total_amount)`
- `report_payment_order_value_date`
- Usage: date valeur
- Source de verite: `invoice.payment_term_date`, fallback `invoice.invoice_date`
- `report_payment_order_company_address`
- Usage: bloc beneficiaire
- Source de verite: `invoice.invoice_address.full_address`, fallback
`invoice.report_address`
- `report_payment_order_beneficiary_account_nb`
- Usage: compte beneficiaire
- Source de verite: premier compte bancaire du `invoice.party`
- `report_payment_order_beneficiary_bank_name`
- Usage: banque beneficiaire
- Source de verite: banque du compte beneficiaire
- `report_payment_order_beneficiary_bank_city`
- Usage: ville banque beneficiaire
- Source de verite: adresse banque beneficiaire
- `report_payment_order_swift_code`
- Usage: swift/bic beneficiaire
- Source de verite: `bank.bic`
- `report_payment_order_other_instructions`
- Usage: instructions complementaires
- Source de verite: `invoice.description`
- `report_payment_order_reference`
- Usage: reference business de paiement
- Source de verite: `invoice.reference`, fallback `invoice.number`
- `report_payment_order_current_user`
- Usage: signataire payment order
- Source de verite: utilisateur courant (`res.user`)
- `report_payment_order_current_user_email`
- Usage: email retour swift
- Source de verite: contact email du `party` utilisateur, fallback `user.email`
## 5) Proprietes disponibles sur `account.invoice.line`
Source code: `modules/purchase_trade/invoice.py`
- `report_product_description`
- Usage: description produit de la ligne
- Source de verite: `invoice_line.product` ou `origin.product`
- `report_description_upper`
- Usage: description de ligne en uppercase
- Source de verite: `invoice_line.description`
- `report_crop_name`
- Usage: crop de la ligne
- Source de verite: contrat relie via `origin`
- `report_attributes_name`
- Usage: attributs de la ligne
- Source de verite: `origin.attributes_name`
- `report_net`
- Usage: quantite nette de la ligne
- Source de verite: `invoice_line.quantity`
- `report_lbs`
- Usage: quantite convertie en LBS
- Source de verite: conversion de `report_net`
- `report_rate_currency_upper`
- Usage: devise de prix de la ligne
- Source de verite: `origin.linked_currency` ou `invoice_line.currency`
- `report_rate_value`
- Usage: prix numerique de la ligne
- Source de verite: `invoice_line.unit_price`
- `report_rate_unit_upper`
- Usage: unite de prix de la ligne
- Source de verite: `origin.linked_unit` ou `invoice_line.unit`
- `report_rate_price_words`
- Usage: prix en toutes lettres de la ligne
- Source de verite: contrat relie via `trade.report_price`
- `report_rate_pricing_text`
- Usage: texte de pricing de la ligne
- Source de verite: `origin.get_pricing_text`
## 6) Proprietes utiles deja presentes sur `sale.sale`
Source code: `modules/purchase_trade/sale.py`
- `report_terms`
- `report_crop_name`
- `report_gross`
- `report_net`
- `report_qt`
- `report_total_quantity`
- `report_quantity_unit_upper`
- `report_quantity_lines`
- `report_nb_bale`
- `report_deal`
- `report_packing`
- `report_price`
- `report_price_lines`
- `report_delivery`
- `report_payment_date`
- `report_shipment`
- `report_shipment_periods`
- `report_product_name`
- `report_product_description`
Usage typique:
- base de travail pour les templates de type `sale_ict.fodt`
- source de verite de plusieurs proprietes du bridge facture
## 7) Proprietes utiles deja presentes sur `purchase.purchase`
Source code: `modules/purchase_trade/purchase.py`
- `report_terms`
- `report_qt`
- `report_price`
- `report_delivery`
- `report_payment_date`
- `report_shipment`
Usage typique:
- templates et bridges pour facturation fournisseur
- fallback achat quand une facture n'est pas liee a une vente
## 8) Templates connus qui utilisent ces proprietes
- `modules/account_invoice/invoice_ict.fodt`
- `modules/account_invoice/invoice_ict_final.fodt`
- `modules/sale/sale_ict.fodt`
- `modules/stock/insurance.fodt`
## 9) Proprietes utiles deja presentes sur `stock.shipment.in`
Source code: `modules/purchase_trade/stock.py`
- `report_product_name`
- `report_product_description`
- `report_insurance_footer_ref`
- `report_insurance_certificate_number`
- `report_insurance_account_of`
- `report_insurance_goods_description`
- `report_insurance_loading_port`
- `report_insurance_discharge_port`
- `report_insurance_transport`
- `report_insurance_amount`
- `report_insurance_incoming_amount`
- `report_insurance_amount_insured`
- `report_insurance_surveyor`
- `report_insurance_contact_surveyor`
- `report_insurance_issue_place_and_date`
Usage typique:
- templates shipment relies a l'assurance
- `report_insurance_amount`: montant affiche dans `Amount insured` (priorite a
`110%` du total incoming, fallback fee `Insurance`)
- `report_insurance_incoming_amount`: somme `incoming_moves` de
`quantity * unit_price`, avec fallback lot
(`lot.line.unit_price * lot.get_current_quantity_converted()`)
- `report_insurance_amount_insured`: `110%` de
`report_insurance_incoming_amount`
- `report_insurance_contact_surveyor`: surveyor affiche sous
`Contact the following surveyor` (priorite au champ shipment `surveyor`,
puis fallback controller / fee `Insurance`)
- base de travail pour un certificat d'assurance lie a un shipment
## 10) Recommandations
- Avant d'ajouter une nouvelle expression dans un `.fodt`, verifier si une
propriete `report_*` existe deja ici.
- Si une nouvelle propriete est ajoutee pour un template, la documenter dans ce
fichier.
- Pour les donnees logistiques facture, privilegier toujours:
- facture -> ligne metier -> lot physique -> shipment / fee

View File

@@ -1,8 +1,8 @@
# Template Rules - Purchase Trade
Statut: `draft`
Version: `v0.4`
Derniere mise a jour: `2026-04-07`
Version: `v0.2`
Derniere mise a jour: `2026-03-27`
## 1) Scope
@@ -16,12 +16,6 @@ Derniere mise a jour: `2026-04-07`
- Eviter les erreurs de parsing Relatorio/Genshi lors de la generation des documents.
- Standardiser la maniere d'alimenter les templates metier a partir du code Python.
- Centraliser les proprietes `report_*` dans une documentation reutilisable.
## 2.1) Index de reference
- Catalogue des proprietes templates:
- `modules/purchase_trade/docs/template-properties.md`
## 3) Regles pratiques
@@ -98,131 +92,6 @@ Derniere mise a jour: `2026-04-07`
- 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-012 - Centraliser les templates client dans `Document Templates`
- Pour les templates client-specifiques, ne pas modifier `ir_action_report.report`
en base a la main selon l'environnement ou le client.
- Preferer la configuration singleton `purchase_trade.configuration`,
exposee dans `Documents > Configuration > Document Templates`.
- Sections actuellement attendues:
- `Sale`
- `Invoice`
- `Payment`
- `Purchase`
- `Shipment`
- Regle:
- si le champ de template correspondant est vide, le report doit echouer
explicitement avec `No template found`
- ne pas masquer dynamiquement l'action d'impression si ce n'est pas
necessaire
### TR-013 - `sale_melya.fodt` et `invoice_melya.fodt` doivent afficher nom + description produit
- Dans les templates client Melya, le bloc produit doit prevoir:
- une ligne pour le nom produit
- une ligne pour la description produit
- Ne pas dereferencer directement `line.product.name` / `line.product.description`
dans les `.fodt`.
- Preferer:
- `sale.report_product_name`
- `sale.report_product_description`
- `invoice.report_product_name`
- `invoice.report_product_description`
### TR-014 - `invoice_melya.fodt` doit afficher `Invoice` et `Reference` sur les bons champs
- Pour `modules/account_invoice/invoice_melya.fodt`:
- `Invoice` doit afficher `invoice.number`
- `Reference` doit afficher `invoice.report_contract_number`
- Ne pas reutiliser `invoice.reference` pour ce label dans ce template client
sans demande explicite
### TR-015 - Le template `stock/insurance.fodt` doit lire sur `stock.shipment.in`
- Le template `modules/stock/insurance.fodt` est pilote par le report
`stock.shipment.in.insurance`.
- Toutes les croix rouges / placeholders metier doivent etre remplacees par
des proprietes `report_*` exposees sur `stock.shipment.in`.
- Pour ce template, ne pas compter sur une variable Genshi locale `shipment`
dans tout le document; preferer `records[0]....` dans le `.fodt`.
- Source de verite du montant assure:
- sommer les montants des `incoming_moves` du shipment
- montant d'un move = `move.quantity * move.unit_price`
- si `move.unit_price` est vide, fallback via lot:
`lot.line.unit_price * lot.get_current_quantity_converted()`
- exposer au moins:
- le montant total des incoming moves
- le montant assure a `110%` de ce total
- pour le placeholder `Amount insured`, `report_insurance_amount` doit
afficher ce `110%`, avec fallback fee `Insurance` si aucun montant
incoming n'est calculable
### TR-016 - Hypotheses actuelles pour le certificat d'assurance shipment
- Tant qu'une source metier plus precise n'est pas fournie:
- numero du certificat: `shipment.bl_number`, sinon `shipment.number`
- `insured for account of`: client de la premiere ligne metier retrouvee via
lot physique, sinon `shipment.supplier`
- `surveyor`: `shipment.surveyor`, sinon `shipment.controller`, sinon
fournisseur du fee `Insurance`
- lieu/date d'emission: ville de la societe + date du jour
- Si une source differente est decidee plus tard, corriger la propriete Python
plutot que complexifier `insurance.fodt`
### TR-017 - `payment_order.fodt` doit utiliser des proprietes `report_payment_order_*`
- Pour `modules/account_invoice/payment_order.fodt`, ne pas utiliser des
placeholders externes legacy (tokens metier entre `<...>` du systeme source).
- Tous les placeholders du template doivent pointer vers des proprietes Python
stables exposees sur `account.invoice`:
- `report_payment_order_document_reference`
- `report_payment_order_from_account_nb`
- `report_payment_order_to_bank_name`
- `report_payment_order_to_bank_city`
- `report_payment_order_amount`
- `report_payment_order_currency_code`
- `report_payment_order_amount_text`
- `report_payment_order_value_date`
- `report_payment_order_company_address`
- `report_payment_order_beneficiary_account_nb`
- `report_payment_order_beneficiary_bank_name`
- `report_payment_order_beneficiary_bank_city`
- `report_payment_order_swift_code`
- `report_payment_order_other_instructions`
- `report_payment_order_reference`
- `report_payment_order_current_user`
- `report_payment_order_current_user_email`
- Eviter les marqueurs conditionnels heredites de l'ancien moteur (`++...`):
privilegier des placeholders simples avec fallback `or ''`.
### TR-018 - Un template configure n'apparait dans le form que si une action report existe
- Ajouter un champ dans `Document Templates` ne suffit pas a rendre un
template imprimable depuis la fiche.
- Pour afficher l'entree dans `account.invoice`, il faut aussi:
- un `ir.action.report` sur `model = account.invoice`
- un `ir.action.keyword` `form_print` lie a cette action
- Appliquer cette regle pour `Payment Order` comme pour `Invoice`,
`Prepayment` et `CN/DN`.
### TR-019 - Un placeholder Relatorio doit etre dans une balise `text:placeholder`
- Dans un `.fodt`, une expression du type `&lt;records[0].report_* ...&gt;`
ecrite en texte brut peut s'afficher telle quelle a l'impression.
- Regle stricte:
- encapsuler les expressions dans
`<text:placeholder text:placeholder-type="text">...</text:placeholder>`
- ne pas laisser de token `&lt;...&gt;` directement dans un `text:span`,
`text:p`, `text:h`, etc.
- Exemple:
- incorrect:
`PAYMENT ORDER &lt;records[0].report_payment_order_document_reference or &apos;&apos;&gt;`
- correct:
`PAYMENT ORDER <text:placeholder text:placeholder-type="text">&lt;records[0].report_payment_order_document_reference or &apos;&apos;&gt;</text:placeholder>`
### TR-007 - Pour une facture trade, privilegier le lot physique comme chemin de navigation
@@ -251,44 +120,6 @@ Derniere mise a jour: `2026-04-07`
- utiliser `fee.get_amount()`
- Si le fee a sa propre devise, preferer aussi exposer le symbole de devise depuis le fee plutot que depuis la facture.
### TR-009 - Ne pas dereferencer directement `del_period.description` dans les templates
- Eviter les expressions du type:
- `sale.lines[0].del_period.description`
- `purchase.lines[0].del_period.description`
- Meme avec un `if ... else`, ces acces sont fragiles dans un `.fodt` et
rendent le debug plus difficile.
- Preferer une propriete Python stable:
- `sale.report_delivery_period_description`
- `purchase.report_delivery_period_description`
- `invoice.report_delivery_period_description`
### TR-010 - En template, un contrat `basis` affiche le premium comme prix
- Pour les templates commerciaux/facture (`sale_ict`, `invoice_ict`, etc.),
le prix affiche d'une ligne `basis` ne doit pas etre le prix economique total
(`unit_price`, `linked_price` ou prix basis brut).
- La valeur a afficher est uniquement le `premium`:
- en devise/unite liee si `linked currency` est active
- sinon dans la devise/unite native de la ligne
- Le texte de curve / pricing (`ON ICE ...`) reste affiche a cote, mais la
valeur numerique et sa version en lettres doivent representer le premium.
### TR-011 - Pour `NB BALES` sur une facture, sommer les `lot_qt` des lignes facture
- Pour `invoice_ict.fodt` et `invoice_ict_final.fodt`, la source de verite du
nombre de bales n'est pas le poids (`report_net`, `report_gross`) mais
`line.lot.lot_qt`.
- La regle attendue est:
- lire les lignes de facture
- recuperer leur `lot`
- sommer `lot.lot_qt`
- sur une note finale, tenir compte du signe de la ligne de facture pour que
les lignes positives et negatives se compensent
- Ne pas recalculer le nombre de bales a partir du poids:
- les poids peuvent varier (humidite, poids net/gross)
- le nombre de bales peut rester stable
## 4) Workflow recommande pour corriger un template en erreur
1. Identifier le placeholder exact qui provoque l'erreur Relatorio.

View File

@@ -1,52 +1,17 @@
from decimal import Decimal, ROUND_HALF_UP
from datetime import date as dt_date
from decimal import Decimal
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.transaction import Transaction
from trytond.modules.account_invoice.invoice import (
InvoiceReport as BaseInvoiceReport)
from trytond.modules.sale.sale import SaleReport as BaseSaleReport
from trytond.modules.purchase.purchase import (
PurchaseReport as BasePurchaseReport)
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
@staticmethod
def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False,
strip_trailing_zeros=True):
value = Decimal(str(value or 0)).quantize(Decimal(digits))
text = format(value, 'f')
if strip_trailing_zeros:
text = text.rstrip('0').rstrip('.')
if keep_trailing_decimal and '.' not in text:
text += '.0'
return text or '0'
def _get_report_invoice_line(self):
for line in self.lines or []:
if getattr(line, 'type', None) == 'line':
return line
return self.lines[0] if self.lines else None
def _get_report_invoice_lines(self):
lines = [
line for line in (self.lines or [])
if getattr(line, 'type', None) == 'line'
]
return lines or list(self.lines or [])
@staticmethod
def _clean_report_description(value):
text = (value or '').strip()
normalized = text.replace(' ', '').upper()
if normalized == 'PROFORMA':
return ''
return text.upper() if text else ''
def _get_report_purchase(self):
purchases = list(self.purchases or [])
return purchases[0] if purchases else None
@@ -81,52 +46,6 @@ class Invoice(metaclass=PoolMeta):
return lot
return line.lots[0]
def _get_report_invoice_lots(self):
invoice_lines = self._get_report_invoice_lines()
if not invoice_lines:
return []
def _same_invoice_line(left, right):
if not left or not right:
return False
left_id = getattr(left, 'id', None)
right_id = getattr(right, 'id', None)
if left_id is not None and right_id is not None:
return left_id == right_id
return left is right
trade = self._get_report_trade()
trade_lines = getattr(trade, 'lines', []) if trade else []
lots = []
for line in trade_lines or []:
for lot in getattr(line, 'lots', []) or []:
if getattr(lot, 'lot_type', None) != 'physic':
continue
refs = [
getattr(lot, 'sale_invoice_line', None),
getattr(lot, 'sale_invoice_line_prov', None),
getattr(lot, 'invoice_line', None),
getattr(lot, 'invoice_line_prov', None),
]
if any(
_same_invoice_line(ref, invoice_line)
for ref in refs for invoice_line in invoice_lines):
lots.append(lot)
return lots
@staticmethod
def _format_report_package_label(unit):
label = (
getattr(unit, 'symbol', None)
or getattr(unit, 'rec_name', None)
or getattr(unit, 'name', None)
or 'BALE'
)
label = label.upper()
if not label.endswith('S'):
label += 'S'
return label
def _get_report_freight_fee(self):
pool = Pool()
Fee = pool.get('fee.fee')
@@ -149,158 +68,6 @@ class Invoice(metaclass=PoolMeta):
or getattr(lot, 'lot_shipment_internal', None)
)
@staticmethod
def _get_report_bank_account(party):
accounts = list(getattr(party, 'bank_accounts', []) or [])
return accounts[0] if accounts else None
@staticmethod
def _get_report_bank_account_number(account):
if not account:
return ''
numbers = list(getattr(account, 'numbers', []) or [])
for number in numbers:
if getattr(number, 'type', None) == 'iban' and getattr(number, 'number', None):
return number.number or ''
for number in numbers:
if getattr(number, 'number', None):
return number.number or ''
return ''
@staticmethod
def _get_report_bank_name(account):
bank = getattr(account, 'bank', None) if account else None
party = getattr(bank, 'party', None) if bank else None
return getattr(party, 'rec_name', None) or getattr(bank, 'rec_name', None) or ''
@staticmethod
def _get_report_bank_city(account):
bank = getattr(account, 'bank', None) if account else None
party = getattr(bank, 'party', None) if bank else None
address = party.address_get() if party and hasattr(party, 'address_get') else None
return getattr(address, 'city', None) or ''
@staticmethod
def _get_report_bank_swift(account):
bank = getattr(account, 'bank', None) if account else None
return getattr(bank, 'bic', None) or ''
@staticmethod
def _format_report_payment_amount(value):
amount = Decimal(str(value or 0)).quantize(Decimal('0.01'))
return format(amount, 'f')
@property
def _report_payment_order_company_account(self):
return self._get_report_bank_account(getattr(self.company, 'party', None))
@property
def _report_payment_order_beneficiary_account(self):
return self._get_report_bank_account(self.party)
@property
def report_payment_order_short_name(self):
company_party = getattr(self.company, 'party', None)
return getattr(company_party, 'rec_name', '') or ''
@property
def report_payment_order_document_reference(self):
return self.number or self.reference or ''
@property
def report_payment_order_from_account_nb(self):
return self._get_report_bank_account_number(
self._report_payment_order_company_account)
@property
def report_payment_order_to_bank_name(self):
return self._get_report_bank_name(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_to_bank_city(self):
return self._get_report_bank_city(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_amount(self):
return self._format_report_payment_amount(self.total_amount)
@property
def report_payment_order_currency_code(self):
currency = self.currency
code = getattr(currency, 'code', None) or ''
rec_name = getattr(currency, 'rec_name', None) or ''
symbol = getattr(currency, 'symbol', None) or ''
if code and any(ch.isalpha() for ch in code):
return code
if rec_name and any(ch.isalpha() for ch in rec_name):
return rec_name
if symbol and any(ch.isalpha() for ch in symbol):
return symbol
return code or rec_name or symbol or ''
@property
def report_payment_order_amount_text(self):
return amount_to_currency_words(self.total_amount)
@property
def report_payment_order_value_date(self):
value_date = self.payment_term_date or self.invoice_date
if isinstance(value_date, dt_date):
return value_date.strftime('%d-%m-%Y')
return ''
@property
def report_payment_order_company_address(self):
if self.invoice_address and getattr(self.invoice_address, 'full_address', None):
return self.invoice_address.full_address
return self.report_address
@property
def report_payment_order_beneficiary_account_nb(self):
return self._get_report_bank_account_number(
self._report_payment_order_beneficiary_account)
@property
def report_payment_order_beneficiary_bank_name(self):
return self._get_report_bank_name(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_beneficiary_bank_city(self):
return self._get_report_bank_city(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_swift_code(self):
return self._get_report_bank_swift(self._report_payment_order_beneficiary_account)
@property
def report_payment_order_other_instructions(self):
return self.description or ''
@property
def report_payment_order_reference(self):
return self.reference or self.number or ''
@staticmethod
def _get_report_current_user():
user_id = Transaction().user
if not user_id:
return None
User = Pool().get('res.user')
return User(user_id)
@property
def report_payment_order_current_user(self):
user = self._get_report_current_user()
return getattr(user, 'rec_name', None) or ''
@property
def report_payment_order_current_user_email(self):
user = self._get_report_current_user()
party = getattr(user, 'party', None) if user else None
if party and hasattr(party, 'contact_mechanism_get'):
return party.contact_mechanism_get('email') or ''
return getattr(user, 'email', None) or ''
@property
def report_address(self):
trade = self._get_report_trade()
@@ -352,17 +119,10 @@ class Invoice(metaclass=PoolMeta):
return line.product.description or ''
return ''
@property
def report_product_name(self):
line = self._get_report_trade_line()
if line and line.product:
return line.product.name or ''
return ''
@property
def report_description_upper(self):
if self.lines:
return self._clean_report_description(self.lines[0].description)
return (self.lines[0].description or '').upper()
return ''
@property
@@ -386,39 +146,6 @@ class Invoice(metaclass=PoolMeta):
return trade.report_price
return ''
@property
def report_quantity_lines(self):
details = []
for line in self._get_report_invoice_lines():
quantity = getattr(line, 'report_net', '')
if quantity == '':
quantity = getattr(line, 'quantity', '')
if quantity == '':
continue
quantity_text = self._format_report_number(
quantity, keep_trailing_decimal=True)
unit = getattr(line, 'unit', None)
unit_name = unit.rec_name.upper() if unit and unit.rec_name else ''
lbs = getattr(line, 'report_lbs', '')
parts = [quantity_text, unit_name]
if lbs != '':
parts.append(
f"({self._format_report_number(lbs, digits='0.01')} LBS)")
detail = ' '.join(part for part in parts if part)
if detail:
details.append(detail)
return '\n'.join(details)
@property
def report_trade_blocks(self):
blocks = []
quantity_lines = self.report_quantity_lines.splitlines()
rate_lines = self.report_rate_lines.splitlines()
for index, quantity_line in enumerate(quantity_lines):
price_line = rate_lines[index] if index < len(rate_lines) else ''
blocks.append((quantity_line, price_line))
return blocks
@property
def report_rate_currency_upper(self):
line = self._get_report_invoice_line()
@@ -454,66 +181,6 @@ class Invoice(metaclass=PoolMeta):
return line.report_rate_pricing_text
return ''
@property
def report_rate_lines(self):
details = []
for line in self._get_report_invoice_lines():
currency = getattr(line, 'report_rate_currency_upper', '') or ''
value = getattr(line, 'report_rate_value', '')
value_text = ''
if value != '':
value_text = self._format_report_number(
value, strip_trailing_zeros=False)
unit = getattr(line, 'report_rate_unit_upper', '') or ''
words = getattr(line, 'report_rate_price_words', '') or ''
pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
detail = ' '.join(
part for part in [
currency,
value_text,
'PER' if unit else '',
unit,
f"({words})" if words else '',
pricing_text,
] if part)
if detail:
details.append(detail)
return '\n'.join(details)
@property
def report_positive_rate_lines(self):
sale = self._get_report_sale()
if sale and getattr(sale, 'report_price_lines', None):
return sale.report_price_lines
details = []
for line in self._get_report_invoice_lines():
quantity = getattr(line, 'report_net', '')
if quantity == '':
quantity = getattr(line, 'quantity', '')
if Decimal(str(quantity or 0)) <= 0:
continue
currency = getattr(line, 'report_rate_currency_upper', '') or ''
value = getattr(line, 'report_rate_value', '')
value_text = ''
if value != '':
value_text = self._format_report_number(
value, strip_trailing_zeros=False)
unit = getattr(line, 'report_rate_unit_upper', '') or ''
words = getattr(line, 'report_rate_price_words', '') or ''
pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
detail = ' '.join(
part for part in [
currency,
value_text,
'PER' if unit else '',
unit,
f"({words})" if words else '',
pricing_text,
] if part)
if detail:
details.append(detail)
return '\n'.join(details)
@property
def report_payment_date(self):
trade = self._get_report_trade()
@@ -521,16 +188,6 @@ class Invoice(metaclass=PoolMeta):
return trade.report_payment_date
return ''
@property
def report_delivery_period_description(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'report_delivery_period_description', None):
return trade.report_delivery_period_description
line = self._get_report_trade_line()
if line and getattr(line, 'del_period', None):
return line.del_period.description or ''
return ''
@property
def report_payment_description(self):
trade = self._get_report_trade()
@@ -542,39 +199,6 @@ class Invoice(metaclass=PoolMeta):
@property
def report_nb_bale(self):
total_packages = Decimal(0)
package_unit = None
has_invoice_line_packages = False
for line in self._get_report_invoice_lines():
lot = getattr(line, 'lot', None)
if not lot or getattr(lot, 'lot_qt', None) in (None, ''):
continue
has_invoice_line_packages = True
if not package_unit and getattr(lot, 'lot_unit', None):
package_unit = lot.lot_unit
sign = Decimal(1)
if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0:
sign = Decimal(-1)
total_packages += (
Decimal(str(lot.lot_qt or 0)).quantize(
Decimal('1'), rounding=ROUND_HALF_UP) * sign)
if has_invoice_line_packages:
label = self._format_report_package_label(package_unit)
return f"NB {label}: {int(total_packages)}"
lots = self._get_report_invoice_lots()
if lots:
total_packages = Decimal(0)
package_unit = None
for lot in lots:
if getattr(lot, 'lot_qt', None):
total_packages += Decimal(str(lot.lot_qt or 0))
if not package_unit and getattr(lot, 'lot_unit', None):
package_unit = lot.lot_unit
package_qty = total_packages.quantize(
Decimal('1'), rounding=ROUND_HALF_UP)
label = self._format_report_package_label(package_unit)
return f"NB {label}: {int(package_qty)}"
sale = self._get_report_sale()
if sale and sale.report_nb_bale:
return sale.report_nb_bale
@@ -588,10 +212,9 @@ class Invoice(metaclass=PoolMeta):
@property
def report_gross(self):
if self.lines:
return sum(
Decimal(str(getattr(line, 'quantity', 0) or 0))
for line in self._get_report_invoice_lines())
sale = self._get_report_sale()
if sale and sale.report_gross != '':
return sale.report_gross
line = self._get_report_trade_line()
if line and line.lots:
return sum(
@@ -602,10 +225,9 @@ class Invoice(metaclass=PoolMeta):
@property
def report_net(self):
if self.lines:
return sum(
Decimal(str(getattr(line, 'quantity', 0) or 0))
for line in self._get_report_invoice_lines())
trade = self._get_report_trade()
if trade and getattr(trade, 'report_net', '') != '':
return trade.report_net
line = self._get_report_trade_line()
if line and line.lots:
return sum(
@@ -623,21 +245,6 @@ class Invoice(metaclass=PoolMeta):
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
@property
def report_weight_unit_upper(self):
line = self._get_report_trade_line() or self._get_report_invoice_line()
unit = getattr(line, 'unit', None) if line else None
if unit and unit.rec_name:
return unit.rec_name.upper()
return 'KGS'
@property
def report_note_title(self):
total = Decimal(str(self.total_amount or 0))
if total < 0:
return 'Debit Note'
return 'Credit Note'
@property
def report_bl_date(self):
shipment = self._get_report_shipment()
@@ -759,18 +366,9 @@ class InvoiceLine(metaclass=PoolMeta):
return origin.product.description or ''
return ''
@property
def report_product_name(self):
if self.product:
return self.product.name or ''
origin = getattr(self, 'origin', None)
if origin and getattr(origin, 'product', None):
return origin.product.name or ''
return ''
@property
def report_description_upper(self):
return Invoice._clean_report_description(self.description)
return (self.description or '').upper()
@property
def report_rate_currency_upper(self):
@@ -782,11 +380,6 @@ class InvoiceLine(metaclass=PoolMeta):
@property
def report_rate_value(self):
origin = self._get_report_trade_line()
if origin and getattr(origin, 'price_type', None) == 'basis':
if getattr(origin, 'enable_linked_currency', False) and getattr(origin, 'linked_currency', None):
return Decimal(str(origin.premium or 0))
return Decimal(str(origin._get_premium_price() or 0))
return self.unit_price if self.unit_price is not None else ''
@property
@@ -799,12 +392,6 @@ class InvoiceLine(metaclass=PoolMeta):
@property
def report_rate_price_words(self):
origin = self._get_report_trade_line()
if origin and getattr(origin, 'price_type', None) == 'basis':
value = self.report_rate_value
if self.report_rate_currency_upper == 'USC':
return amount_to_currency_words(value, 'USC', 'USC')
return amount_to_currency_words(value)
trade = self._get_report_trade()
if trade and getattr(trade, 'report_price', None):
return trade.report_price
@@ -841,96 +428,3 @@ class InvoiceLine(metaclass=PoolMeta):
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
class ReportTemplateMixin:
@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 _get_action_report_path(cls, action):
if isinstance(action, dict):
return action.get('report') or ''
return getattr(action, 'report', '') or ''
@classmethod
def _resolve_template_path(cls, action, field_name, default_prefix):
config = cls._get_purchase_trade_configuration()
template = getattr(config, field_name, '') if config else ''
template = (template or '').strip()
if not template:
raise UserError('No template found')
if '/' not in template:
return f'{default_prefix}/{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)
class InvoiceReport(ReportTemplateMixin, BaseInvoiceReport):
__name__ = 'account.invoice'
@classmethod
def _resolve_configured_report_path(cls, action):
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'):
field_name = 'invoice_prepayment_report_template'
elif (report_path.endswith('/payment_order.fodt')
or action_name == 'Payment Order'):
field_name = 'invoice_payment_order_report_template'
elif (report_path.endswith('/invoice_ict_final.fodt')
or action_name == 'CN/DN'):
field_name = 'invoice_cndn_report_template'
else:
field_name = 'invoice_report_template'
return cls._resolve_template_path(action, field_name, 'account_invoice')
class SaleReport(ReportTemplateMixin, BaseSaleReport):
__name__ = 'sale.sale'
@classmethod
def _resolve_configured_report_path(cls, action):
report_path = cls._get_action_report_path(action)
action_name = cls._get_action_name(action)
if report_path.endswith('/bill.fodt') or action_name == 'Bill':
field_name = 'sale_bill_report_template'
elif report_path.endswith('/sale_final.fodt') or action_name == 'Sale (final)':
field_name = 'sale_final_report_template'
else:
field_name = 'sale_report_template'
return cls._resolve_template_path(action, field_name, 'sale')
class PurchaseReport(ReportTemplateMixin, BasePurchaseReport):
__name__ = 'purchase.purchase'
@classmethod
def _resolve_configured_report_path(cls, action):
return cls._resolve_template_path(
action, 'purchase_report_template', 'purchase')

View File

@@ -1,16 +0,0 @@
<tryton>
<data>
<record model="ir.action.report" id="report_payment_order">
<field name="name">Payment Order</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/payment_order.fodt</field>
<field name="single" eval="True"/>
</record>
<record model="ir.action.keyword" id="report_payment_order_keyword">
<field name="keyword">form_print</field>
<field name="model">account.invoice,-1</field>
<field name="action" ref="report_payment_order"/>
</record>
</data>
</tryton>

View File

@@ -2774,54 +2774,40 @@ class LotInvoice(Wizard):
'action': act
}
def transition_invoicing(self):
Lot = Pool().get('lot.lot')
Purchase = Pool().get('purchase.purchase')
Sale = Pool().get('sale.sale')
lots = []
purchases = []
sales = []
action = self.inv.action
for r in self.records:
purchase = r.r_line.purchase if r.r_line else None
sale = r.r_sale_line.sale if r.r_sale_line else None
if purchase and purchase not in purchases:
purchases.append(purchase)
if sale and sale not in sales:
sales.append(sale)
lot = Lot(r.r_lot_p)
# if lot.move == None:
# Warning = Pool().get('res.user.warning')
# warning_name = Warning.format("Lot not confirmed", [])
# if Warning.check(warning_name):
def transition_invoicing(self):
Lot = Pool().get('lot.lot')
Purchase = Pool().get('purchase.purchase')
Sale = Pool().get('sale.sale')
lots = []
action = self.inv.action
for r in self.records:
purchase = r.r_line.purchase
sale = None
if r.r_sale_line:
sale = r.r_sale_line.sale
lot = Lot(r.r_lot_p)
# if lot.move == None:
# Warning = Pool().get('res.user.warning')
# warning_name = Warning.format("Lot not confirmed", [])
# if Warning.check(warning_name):
# raise QtWarning(warning_name,
# "Lot not confirmed, click yes to confirm and invoice")
# continue
if lot.invoice_line:
continue
lots.append(lot)
invoice_line = None
if self.inv.type == 'purchase':
Purchase._process_invoice(purchases, lots, action, self.inv.pp_pur)
for lot in lots:
lot = Lot(lot.id)
invoice_line = lot.invoice_line or lot.invoice_line_prov
if invoice_line:
break
else:
if sales:
Sale._process_invoice(sales, lots, action, self.inv.pp_sale)
for lot in lots:
lot = Lot(lot.id)
invoice_line = lot.sale_invoice_line or lot.sale_invoice_line_prov
if invoice_line:
break
if not invoice_line:
raise UserError("No invoice line was generated from the selected lots.")
self.message.invoice = invoice_line.invoice
return 'message'
if lot.invoice_line:
continue
lots.append(lot)
invoice_line = None
if self.inv.type == 'purchase':
Purchase._process_invoice([purchase],lots,action,self.inv.pp_pur)
invoice_line = r.r_lot_p.invoice_line if r.r_lot_p.invoice_line else r.r_lot_p.invoice_line_prov
else:
if sale:
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
invoice_line = r.r_lot_p.invoice_line if r.r_lot_p.sale_invoice_line else r.r_lot_p.sale_invoice_line_prov
self.message.invoice = invoice_line.invoice
return 'message'
def default_message(self, fields):
return {
@@ -3168,55 +3154,37 @@ class CreateContracts(Wizard):
def transition_start(self):
return 'ct'
def default_ct(self, fields):
LotQt = Pool().get('lot.qt')
Lot = Pool().get('lot.lot')
context = Transaction().context
ids = context.get('active_ids')
def default_ct(self, fields):
LotQt = Pool().get('lot.qt')
Lot = Pool().get('lot.lot')
context = Transaction().context
ids = context.get('active_ids')
unit = None
product = None
sh_in = None
sh_int = None
sh_out = None
lot = None
qt = Decimal(0)
type = None
shipment_in_values = set()
shipment_internal_values = set()
shipment_out_values = set()
for i in ids:
if i < 10000000:
raise UserError("You must create contract from an open quantity !")
l = LotQt(i - 10000000)
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
current_type = "Sale" if l.lot_p else "Purchase"
if type and current_type != type:
raise UserError("You must select open quantities from the same side.")
type = current_type
if product and ll.lot_product.id != product:
raise UserError("You must select open quantities with the same product.")
if unit and l.lot_unit.id != unit:
raise UserError("You must select open quantities with the same unit.")
unit = l.lot_unit.id
qt += abs(Decimal(str(l.lot_quantity or 0)))
product = ll.lot_product.id
shipment_in_values.add(l.lot_shipment_in.id if l.lot_shipment_in else None)
shipment_internal_values.add(
l.lot_shipment_internal.id if l.lot_shipment_internal else None)
shipment_out_values.add(l.lot_shipment_out.id if l.lot_shipment_out else None)
if lot is None:
lot = ll.id
if len(shipment_in_values) == 1:
sh_in = next(iter(shipment_in_values))
if len(shipment_internal_values) == 1:
sh_int = next(iter(shipment_internal_values))
if len(shipment_out_values) == 1:
sh_out = next(iter(shipment_out_values))
return {
'quantity': qt,
'unit': unit,
sh_out = None
lot = None
qt = None
type = None
for i in ids:
val = {}
if i < 10000000:
raise UserError("You must create contract from an open quantity !")
l = LotQt(i - 10000000)
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
type = "Sale" if l.lot_p else "Purchase"
unit = l.lot_unit.id
qt = l.lot_quantity
product = ll.lot_product.id
sh_in = l.lot_shipment_in.id if l.lot_shipment_in else None
sh_int = l.lot_shipment_internal.id if l.lot_shipment_internal else None
sh_out = l.lot_shipment_out.id if l.lot_shipment_out else None
lot = ll.id
return {
'quantity': qt,
'unit': unit,
'product': product,
'shipment_in': sh_in,
'shipment_internal': sh_int,
@@ -3391,4 +3359,4 @@ class ContractDetail(ModelView):
if self.del_period:
self.from_del = self.del_period.beg_date
self.to_del = self.del_period.end_date

View File

@@ -7,74 +7,17 @@ from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql import Table
from trytond.pyson import Bool, Eval, Id, If
class PartyExecution(ModelSQL,ModelView):
"Party Execution"
__name__ = 'party.execution'
class PartyExecution(ModelSQL,ModelView):
"Party Execution"
__name__ = 'party.execution'
party = fields.Many2One('party.party',"Party")
area = fields.Many2One('country.region',"Area")
percent = fields.Numeric("% targeted")
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
@staticmethod
def _to_decimal(value):
if value is None:
return Decimal('0')
if isinstance(value, Decimal):
return value
return Decimal(str(value))
@classmethod
def _round_percent(cls, value):
return cls._to_decimal(value).quantize(
Decimal('0.01'), rounding=ROUND_HALF_UP)
def matches_country(self, country):
if not self.area or not country or not getattr(country, 'region', None):
return False
region = country.region
while region:
if region.id == self.area.id:
return True
region = getattr(region, 'parent', None)
return False
def matches_shipment(self, shipment):
location = getattr(shipment, 'to_location', None)
country = getattr(location, 'country', None)
return self.matches_country(country)
@classmethod
def compute_achieved_percent_for(cls, party, area):
if not party or not area:
return Decimal('0')
Shipment = Pool().get('stock.shipment.in')
shipments = Shipment.search([
('controller', '!=', None),
])
execution = cls()
execution.area = area
shipments = [
shipment for shipment in shipments
if execution.matches_shipment(shipment)]
total = len(shipments)
if not total:
return Decimal('0')
achieved = sum(
1 for shipment in shipments
if shipment.controller and shipment.controller.id == party.id)
return cls._round_percent(
(Decimal(achieved) * Decimal('100')) / Decimal(total))
def compute_achieved_percent(self):
return self.__class__.compute_achieved_percent_for(
self.party, self.area)
def get_target_gap(self):
return self._to_decimal(self.percent) - self.compute_achieved_percent()
def get_percent(self,name):
return self.compute_achieved_percent()
area = fields.Many2One('country.region',"Area")
percent = fields.Numeric("% targeted")
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
def get_percent(self,name):
return 2
class PartyExecutionSla(ModelSQL,ModelView):
"Party Execution Sla"
@@ -112,8 +55,8 @@ class PartyExecutionPlace(ModelSQL,ModelView):
'readonly': Eval('mode') != 'ppack',
})
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
@@ -122,25 +65,13 @@ class Party(metaclass=PoolMeta):
origin =fields.Char("Origin")
execution = fields.One2Many('party.execution','party',"")
sla = fields.One2Many('party.execution.sla','party', "Sla")
initial = fields.Char("Initials")
def IsAvailableForControl(self,sh):
return True
def get_controller_execution_priority(self, shipment):
best_rule = None
best_gap = None
for execution in self.execution or []:
if not execution.matches_shipment(shipment):
continue
gap = execution.get_target_gap()
if best_gap is None or gap > best_gap:
best_gap = gap
best_rule = execution
return best_gap, best_rule
def get_sla_cost(self,location):
if self.sla:
initial = fields.Char("Initials")
def IsAvailableForControl(self,sh):
return True
def get_sla_cost(self,location):
if self.sla:
for sla in self.sla:
SlaPlace = Pool().get('party.execution.place')
sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)])
@@ -180,4 +111,4 @@ class Party(metaclass=PoolMeta):
pc.category = cat[0].id
PartyCategory.save([pc])
return p

View File

@@ -384,22 +384,16 @@ class Purchase(metaclass=PoolMeta):
return ''
@property
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_delivery_period_description(self):
if self.lines and self.lines[0].del_period:
return self.lines[0].del_period.description or ''
return ''
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_payment_date(self):
@@ -549,16 +543,14 @@ class Purchase(metaclass=PoolMeta):
OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_purchase_line(line)
#line unit_price calculation
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
previous_linked_price = line.linked_price
line.sync_linked_price_from_basis()
unit_price = line.get_basis_price()
logger.info("VALIDATEPURCHASE:%s",unit_price)
if unit_price != line.unit_price or line.linked_price != previous_linked_price:
line.unit_price = unit_price
logger.info("VALIDATEPURCHASE2:%s",line.unit_price)
Line.save([line])
#line unit_price calculation
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
logger.info("VALIDATEPURCHASE:%s",unit_price)
if unit_price != line.unit_price:
line.unit_price = unit_price
logger.info("VALIDATEPURCHASE2:%s",line.unit_price)
Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
@@ -1026,21 +1018,10 @@ class QualityAnalysis(ModelSQL,ModelView):
))
return " | ".join(filter(None, values))
class Line(metaclass=PoolMeta):
__name__ = 'purchase.line'
@classmethod
def default_pricing_rule(cls):
try:
Configuration = Pool().get('purchase_trade.configuration')
except KeyError:
return ''
configurations = Configuration.search([], limit=1)
if configurations:
return configurations[0].pricing_rule or ''
return ''
quantity_theorical = fields.Numeric("Contractual Qt", digits='unit', readonly=False)
class Line(metaclass=PoolMeta):
__name__ = 'purchase.line'
quantity_theorical = fields.Numeric("Contractual Qt", digits='unit', readonly=False)
price_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
@@ -1095,23 +1076,18 @@ class Line(metaclass=PoolMeta):
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
certif = fields.Many2One('purchase.certification',"Certification",states={'readonly': (Eval('inherit_cer')),})
inherit_cer = fields.Boolean("Inherit certification")
enable_linked_currency = fields.Boolean("Linked currencies")
linked_price = fields.Numeric("Price", digits='unit',states={
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
'readonly': Eval('price_type') == 'basis',
}, depends=['enable_linked_currency', 'price_type'])
linked_currency = fields.Many2One('currency.linked',"Currency",states={
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
enable_linked_currency = fields.Boolean("Linked currencies")
linked_price = fields.Numeric("Price", digits='unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_currency = fields.Many2One('currency.linked',"Currency",states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
premium = fields.Numeric("Premium/Discount",digits='unit')
fee_ = fields.Many2One('fee.fee',"Fee")
pricing_rule = fields.Text("Pricing description")
fee_ = fields.Many2One('fee.fee',"Fee")
attributes = fields.Dict(
'product.attribute', 'Attributes',
@@ -1149,16 +1125,9 @@ class Line(metaclass=PoolMeta):
('umpire', 'Umpire'),
], "Type")
@classmethod
def default_finished(cls):
return False
@property
def report_fixing_rule(self):
pricing_rule = ''
if self.pricing_rule:
pricing_rule = self.pricing_rule
return pricing_rule
@classmethod
def default_finished(cls):
return False
@fields.depends('product')
@@ -1235,148 +1204,49 @@ class Line(metaclass=PoolMeta):
if ps:
return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)
def getVirtualLot(self):
if self.lots:
return [l for l in self.lots if l.lot_type=='virtual'][0]
def _get_linked_unit_factor(self):
if not (self.enable_linked_currency and self.linked_currency):
return None
factor = Decimal(self.linked_currency.factor or 0)
if not factor:
return None
unit_factor = Decimal(1)
if self.linked_unit:
source_unit = getattr(self, 'unit', None)
if not source_unit and self.product:
source_unit = self.product.purchase_uom or self.product.default_uom
if not source_unit:
return factor
Uom = Pool().get('product.uom')
unit_factor = Decimal(str(
Uom.compute_qty(source_unit, float(1), self.linked_unit) or 0))
return factor * unit_factor
def _linked_to_line_price(self, price):
factor = self._get_linked_unit_factor()
price = Decimal(price or 0)
if not factor:
return price
return round(price * factor, 4)
def _line_to_linked_price(self, price):
factor = self._get_linked_unit_factor()
price = Decimal(price or 0)
if not factor:
return price
return round(price / factor, 4)
def _get_premium_price(self):
premium = Decimal(self.premium or 0)
if not premium:
return Decimal(0)
if self.enable_linked_currency and self.linked_currency:
return self._linked_to_line_price(premium)
return premium
def _get_basis_component_price(self):
price = Decimal(0)
if self.terms:
for t in self.terms:
price += (t.manual_price if t.manual_price else Decimal(0))
else:
def getVirtualLot(self):
if self.lots:
return [l for l in self.lots if l.lot_type=='virtual'][0]
def get_basis_price(self):
price = Decimal(0)
if self.terms:
for t in self.terms:
price += (t.manual_price if t.manual_price else Decimal(0))
else:
for pc in self.price_components:
PP = Pool().get('purchase.pricing.summary')
pp = PP.search([('price_component','=',pc.id),('line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
def get_basis_price(self):
return round(self._get_basis_component_price(), 4)
def sync_linked_price_from_basis(self):
if self.enable_linked_currency and self.linked_currency:
self.linked_price = self._line_to_linked_price(
self._get_basis_component_price())
def get_price(self,lot_premium=0):
return round(
Decimal(self.unit_price or 0)
+ Decimal(lot_premium or 0),
4)
def get_price_linked_currency(self,lot_premium=0):
return round(
self._linked_to_line_price(
Decimal(self.linked_price or 0)
+ Decimal(lot_premium or 0)),
4)
pp = PP.search([('price_component','=',pc.id),('line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
if self.price_type == 'basis':
self.sync_linked_price_from_basis()
price = self.get_basis_price()
logger.info("ONCHANGEUNITPRICE_IN:%s",price)
return price
if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
return self.get_price_linked_currency()
if self.price_type == 'efp':
if hasattr(self, 'derivatives') and self.derivatives:
for d in self.derivatives:
return round(d.price_index.get_price(Date.today(),self.unit,self.purchase.currency,True),4)
return self.get_price()
@fields.depends(
'type', 'quantity', 'unit_price', 'unit', 'product',
'purchase', '_parent_purchase.currency',
'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
def on_change_with_amount(self):
if (self.type == 'line'
and self.quantity is not None
and self.unit_price is not None):
currency = self.purchase.currency if self.purchase else None
amount = Decimal(str(self.quantity)) * (
Decimal(self.unit_price or 0) + self._get_premium_price())
if currency:
return currency.round(amount)
return amount
return Decimal(0)
@fields.depends(
'unit', 'product', 'price_type', 'enable_linked_currency',
'linked_currency', 'linked_unit', 'linked_price', 'premium',
methods=['on_change_with_unit_price', 'on_change_with_amount'])
def _recompute_trade_price_fields(self):
self.unit_price = self.on_change_with_unit_price()
self.amount = self.on_change_with_amount()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_premium(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_price_type(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_enable_linked_currency(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_linked_price(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_linked_currency(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_linked_unit(self):
self._recompute_trade_price_fields()
def get_price(self,lot_premium=0):
return (self.unit_price + Decimal(lot_premium)) if self.unit_price else Decimal(0) + (self.premium if self.premium else Decimal(0))
def get_price_linked_currency(self,lot_premium=0):
if self.linked_unit:
Uom = Pool().get('product.uom')
qt = Uom.compute_qty(self.unit, float(1), self.linked_unit)
return round(((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor) * Decimal(qt), 4)
else:
return round((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor, 4)
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','premium','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
if self.price_type == 'basis' and self.lots: #self.price_pricing and self.price_components and
price = self.get_basis_price()
logger.info("ONCHANGEUNITPRICE_IN:%s",price)
return price
if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
return self.get_price_linked_currency()
if self.price_type == 'efp':
if hasattr(self, 'derivatives') and self.derivatives:
for d in self.derivatives:
return round(d.price_index.get_price(Date.today(),self.unit,self.purchase.currency,True),4)
return self.unit_price
@classmethod
def write(cls, *args):

View File

@@ -319,296 +319,102 @@ class Sale(metaclass=PoolMeta):
def default_tol_min(cls):
return 0
@classmethod
def default_tol_max(cls):
return 0
def _get_report_lines(self):
return [line for line in self.lines if getattr(line, 'type', None) == 'line']
def _get_report_first_line(self):
lines = self._get_report_lines()
if lines:
return lines[0]
@staticmethod
def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False,
strip_trailing_zeros=True):
value = Decimal(str(value or 0)).quantize(Decimal(digits))
text = format(value, 'f')
if strip_trailing_zeros:
text = text.rstrip('0').rstrip('.')
if keep_trailing_decimal and '.' not in text:
text += '.0'
return text or '0'
def _format_report_price_words(self, line):
value = self._get_report_display_price_value(line)
currency = self._get_report_display_currency(line)
if currency and (currency.rec_name or '').upper() == 'USC':
return amount_to_currency_words(value, 'USC', 'USC')
return amount_to_currency_words(value)
def _get_report_display_currency(self, line):
if getattr(line, 'price_type', None) == 'basis':
if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None):
return line.linked_currency
return self.currency
return getattr(line, 'linked_currency', None) or self.currency
def _get_report_display_unit(self, line):
if getattr(line, 'price_type', None) == 'basis':
if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_unit', None):
return line.linked_unit
return getattr(line, 'unit', None)
return getattr(line, 'linked_unit', None) or getattr(line, 'unit', None)
def _get_report_display_price_value(self, line):
if getattr(line, 'price_type', None) == 'basis':
if getattr(line, 'enable_linked_currency', False) and getattr(line, 'linked_currency', None):
return Decimal(str(line.premium or 0))
return Decimal(str(line._get_premium_price() or 0))
if getattr(line, 'linked_price', None):
return Decimal(str(line.linked_price or 0))
return Decimal(str(line.unit_price or 0))
def _format_report_price_line(self, line):
currency = self._get_report_display_currency(line)
unit = self._get_report_display_unit(line)
pricing_text = getattr(line, 'get_pricing_text', '') or ''
parts = [
(currency.rec_name.upper() if currency and currency.rec_name else '').strip(),
self._format_report_number(
self._get_report_display_price_value(line),
strip_trailing_zeros=False),
'PER',
(unit.rec_name.upper() if unit and unit.rec_name else '').strip(),
f"({self._format_report_price_words(line)})",
]
if pricing_text:
parts.append(pricing_text)
return ' '.join(part for part in parts if part)
@property
def report_terms(self):
line = self._get_report_first_line()
if line:
return line.note
return ''
@property
def report_gross(self):
lines = self._get_report_lines()
if lines:
total = Decimal(0)
for line in lines:
phys_lots = [l for l in line.lots if l.lot_type == 'physic']
if phys_lots:
total += sum(Decimal(str(l.get_current_gross_quantity() or 0))
for l in phys_lots)
else:
total += Decimal(str(line.quantity or 0))
return total
return ''
@property
def report_net(self):
lines = self._get_report_lines()
if lines:
total = Decimal(0)
for line in lines:
phys_lots = [l for l in line.lots if l.lot_type == 'physic']
if phys_lots:
total += sum(Decimal(str(l.get_current_quantity() or 0))
for l in phys_lots)
else:
total += Decimal(str(line.quantity or 0))
return total
return ''
@property
def report_total_quantity(self):
lines = self._get_report_lines()
if lines:
total = sum(Decimal(str(line.quantity or 0)) for line in lines)
return self._format_report_number(total, keep_trailing_decimal=True)
return '0.0'
@property
def report_quantity_unit_upper(self):
line = self._get_report_first_line()
if line and line.unit:
return line.unit.rec_name.upper()
return ''
def _get_report_line_quantity(self, line):
phys_lots = [l for l in line.lots if l.lot_type == 'physic']
if phys_lots:
return sum(Decimal(str(l.get_current_quantity() or 0))
for l in phys_lots)
return Decimal(str(line.quantity or 0))
@property
def report_qt(self):
lines = self._get_report_lines()
if lines:
total = sum(self._get_report_line_quantity(line) for line in lines)
return quantity_to_words(total)
return ''
@property
def report_quantity_lines(self):
lines = self._get_report_lines()
if not lines:
return ''
details = []
for line in lines:
current_quantity = self._get_report_line_quantity(line)
quantity = self._format_report_number(
current_quantity, keep_trailing_decimal=True)
unit = line.unit.rec_name.upper() if line.unit and line.unit.rec_name else ''
words = quantity_to_words(current_quantity)
period = line.del_period.description if getattr(line, 'del_period', None) else ''
detail = ' '.join(
part for part in [
quantity,
unit,
f"({words})",
f"- {period}" if period else '',
] if part)
if detail:
details.append(detail)
return '\n'.join(details)
@property
def report_nb_bale(self):
nb_bale = 0
lines = self._get_report_lines()
if lines:
for line in lines:
if line.lots:
nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
if nb_bale:
return 'NB BALES: ' + str(int(nb_bale))
return ''
@property
def report_product_name(self):
line = self._get_report_first_line()
if line and line.product:
return line.product.name or ''
return ''
@property
def report_product_description(self):
line = self._get_report_first_line()
if line and line.product:
return line.product.description or ''
return ''
@property
def report_crop_name(self):
if self.crop:
return self.crop.name or ''
return ''
@property
def report_deal(self):
if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1:
return self.lines[0].lots[1].line.purchase.number + ' ' + self.number
@classmethod
def default_tol_max(cls):
return 0
@property
def report_terms(self):
if self.lines:
return self.lines[0].note
else:
return ''
@property
def report_gross(self):
if self.lines:
return sum([l.get_current_gross_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
else:
return ''
@property
def report_net(self):
if self.lines:
return sum([l.get_current_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
else:
return ''
@property
def report_qt(self):
if self.lines:
return quantity_to_words(self.lines[0].quantity)
else:
return ''
@property
def report_nb_bale(self):
text_bale = 'NB BALES: '
nb_bale = 0
if self.lines:
for line in self.lines:
if line.lots:
nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
return text_bale + str(int(nb_bale))
@property
def report_deal(self):
if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1:
return self.lines[0].lots[1].line.purchase.number + ' ' + self.number
else:
''
@property
def report_packing(self):
nb_packing = 0
unit = ''
lines = self._get_report_lines()
if lines:
for line in lines:
if line.lots:
nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
if len(line.lots)>1:
unit = line.lots[1].lot_unit.name
return str(int(nb_packing)) + unit
def report_packing(self):
nb_packing = 0
unit = ''
if self.lines:
for line in self.lines:
if line.lots:
nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
if len(line.lots)>1:
unit = line.lots[1].lot_unit.name
return str(int(nb_packing)) + unit
@property
def report_price(self):
line = self._get_report_first_line()
if line:
return self._format_report_price_words(line)
return ''
@property
def report_price_lines(self):
lines = self._get_report_lines()
if lines:
return '\n'.join(self._format_report_price_line(line) for line in lines)
return ''
@property
def report_trade_blocks(self):
lines = self._get_report_lines()
blocks = []
for line in lines:
current_quantity = self._get_report_line_quantity(line)
quantity = self._format_report_number(
current_quantity, keep_trailing_decimal=True)
unit = line.unit.rec_name.upper() if line.unit and line.unit.rec_name else ''
words = quantity_to_words(current_quantity)
period = line.del_period.description if getattr(line, 'del_period', None) else ''
quantity_line = ' '.join(
part for part in [
quantity,
unit,
f"({words})",
f"- {period}" if period else '',
] if part)
price_line = self._format_report_price_line(line)
blocks.append((quantity_line, price_line))
return blocks
def report_price(self):
if self.lines:
if self.lines[0].price_type == 'priced':
if self.lines[0].linked_price:
return amount_to_currency_words(self.lines[0].linked_price,'USC','USC')
else:
return amount_to_currency_words(self.lines[0].unit_price)
elif self.lines[0].price_type == 'basis':
return amount_to_currency_words(self.lines[0].unit_price) + ' ' + self.lines[0].get_pricing_text()
else:
return ''
@property
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_delivery_period_description(self):
line = self._get_report_first_line()
if line and line.del_period:
return line.del_period.description or ''
return ''
@property
def report_shipment_periods(self):
periods = []
for line in self._get_report_lines():
period = line.del_period.description if line.del_period else ''
if period and period not in periods:
periods.append(period)
if periods:
return '\n'.join(periods)
return ''
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_payment_date(self):
line = self._get_report_first_line()
if line:
if self.lc_date:
return format_date_en(self.lc_date)
Date = Pool().get('ir.date')
payment_date = line.sale.payment_term.lines[0].get_date(Date.today(), line)
if payment_date:
payment_date = format_date_en(payment_date)
return payment_date
def report_payment_date(self):
if self.lines:
if self.lc_date:
return format_date_en(self.lc_date)
Date = Pool().get('ir.date')
payment_date = self.lines[0].sale.payment_term.lines[0].get_date(Date.today(),self.lines[0])
if payment_date:
payment_date = format_date_en(payment_date)
return payment_date
@property
def report_shipment(self):
@@ -741,14 +547,12 @@ class Sale(metaclass=PoolMeta):
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_sale_line(line)
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
previous_linked_price = line.linked_price
line.sync_linked_price_from_basis()
unit_price = line.get_basis_price()
if unit_price != line.unit_price or line.linked_price != previous_linked_price:
Line = Pool().get('sale.line')
line.unit_price = unit_price
Line.save([line])
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
if unit_price != line.unit_price:
Line = Pool().get('sale.line')
line.unit_price = unit_price
Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
@@ -760,24 +564,13 @@ class PriceComposition(metaclass=PoolMeta):
sale_line = fields.Many2One('sale.line',"Sale line")
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
@classmethod
def default_pricing_rule(cls):
try:
Configuration = Pool().get('purchase_trade.configuration')
except KeyError:
return ''
configurations = Configuration.search([], limit=1)
if configurations:
return configurations[0].pricing_rule or ''
return ''
del_period = fields.Many2One('product.month',"Delivery Period")
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
del_period = fields.Many2One('product.month',"Delivery Period")
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=False)
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=True)
premium = fields.Numeric("Premium/Discount",digits='unit')
price_type = fields.Selection([
('cash', 'Cash Price'),
@@ -828,20 +621,16 @@ class SaleLine(metaclass=PoolMeta):
# ('bci', 'BCI'),
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
inherit_cer = fields.Boolean("Inherit certification")
enable_linked_currency = fields.Boolean("Linked currencies")
linked_price = fields.Numeric("Price", digits='unit',states={
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
'readonly': Eval('price_type') == 'basis',
}, depends=['enable_linked_currency', 'price_type'])
linked_currency = fields.Many2One('currency.linked',"Currency",states={
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
'required': Eval('enable_linked_currency'),
}, depends=['enable_linked_currency'])
enable_linked_currency = fields.Boolean("Linked currencies")
linked_price = fields.Numeric("Price", digits='unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_currency = fields.Many2One('currency.linked',"Currency",states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
premium = fields.Numeric("Premium/Discount",digits='unit')
fee_ = fields.Many2One('fee.fee',"Fee")
@@ -882,22 +671,14 @@ class SaleLine(metaclass=PoolMeta):
pricing_rule = self.pricing_rule
return pricing_rule
@property
def get_pricing_text(self):
parts = []
if self.price_components:
for pc in self.price_components:
if pc.price_index:
price_desc = pc.price_index.price_desc or ''
period_desc = (
pc.price_index.price_period.description
if pc.price_index.price_period else '') or ''
part = ' '.join(
piece for piece in ['ON', price_desc, period_desc]
if piece)
if part:
parts.append(part)
return ' '.join(parts)
@property
def get_pricing_text(self):
pricing_text = ''
if self.price_components:
for pc in self.price_components:
if pc.price_index:
pricing_text += 'ON ' + pc.price_index.price_desc + ' ' + (pc.price_index.price_period.description if pc.price_index.price_period else '')
return pricing_text
@fields.depends('product')
def on_change_with_attribute_set(self, name=None):
@@ -976,141 +757,44 @@ class SaleLine(metaclass=PoolMeta):
if ps:
return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)
def getVirtualLot(self):
if self.lots:
return [l for l in self.lots if l.lot_type=='virtual'][0]
def _get_linked_unit_factor(self):
if not (self.enable_linked_currency and self.linked_currency):
return None
factor = Decimal(self.linked_currency.factor or 0)
if not factor:
return None
unit_factor = Decimal(1)
if self.linked_unit:
source_unit = getattr(self, 'unit', None)
if not source_unit and self.product:
source_unit = self.product.sale_uom
if not source_unit:
return factor
Uom = Pool().get('product.uom')
unit_factor = Decimal(str(
Uom.compute_qty(source_unit, float(1), self.linked_unit) or 0))
return factor * unit_factor
def _linked_to_line_price(self, price):
factor = self._get_linked_unit_factor()
price = Decimal(price or 0)
if not factor:
return price
return round(price * factor, 4)
def _line_to_linked_price(self, price):
factor = self._get_linked_unit_factor()
price = Decimal(price or 0)
if not factor:
return price
return round(price / factor, 4)
def _get_premium_price(self):
premium = Decimal(self.premium or 0)
if not premium:
return Decimal(0)
if self.enable_linked_currency and self.linked_currency:
return self._linked_to_line_price(premium)
return premium
def get_price(self,lot_premium=0):
return round(
Decimal(self.unit_price or 0)
+ Decimal(lot_premium or 0),
4)
def _get_basis_component_price(self):
price = Decimal(0)
for pc in self.price_components:
PP = Pool().get('sale.pricing.summary')
pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
def get_basis_price(self):
return round(self._get_basis_component_price(), 4)
def sync_linked_price_from_basis(self):
if self.enable_linked_currency and self.linked_currency:
self.linked_price = self._line_to_linked_price(
self._get_basis_component_price())
def get_price_linked_currency(self,lot_premium=0):
return round(
self._linked_to_line_price(
Decimal(self.linked_price or 0)
+ Decimal(lot_premium or 0)),
4)
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
if self.price_type == 'basis':
self.sync_linked_price_from_basis()
logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
return self.get_basis_price()
if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
return self.get_price_linked_currency()
if self.price_type == 'efp':
if hasattr(self, 'derivatives') and self.derivatives:
def getVirtualLot(self):
if self.lots:
return [l for l in self.lots if l.lot_type=='virtual'][0]
def get_price(self,lot_premium=0):
return (self.unit_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) if self.unit_price else Decimal(0)
def get_basis_price(self):
price = Decimal(0)
for pc in self.price_components:
PP = Pool().get('sale.pricing.summary')
pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
def get_price_linked_currency(self,lot_premium=0):
if self.linked_unit:
Uom = Pool().get('product.uom')
qt = Uom.compute_qty(self.unit, float(1), self.linked_unit)
return round(((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor) * Decimal(qt), 4)
else:
return round((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor, 4)
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','premium','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
if self.price_type == 'basis' and self.lots: #self.price_pricing and self.price_components and
logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
return self.get_basis_price()
if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
return self.get_price_linked_currency()
if self.price_type == 'efp':
if hasattr(self, 'derivatives') and self.derivatives:
for d in self.derivatives:
return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True)
return self.get_price()
@fields.depends(
'type', 'quantity', 'unit_price', 'unit', 'product',
'sale', '_parent_sale.currency',
'premium', 'enable_linked_currency', 'linked_currency', 'linked_unit')
def on_change_with_amount(self):
if self.type == 'line':
currency = self.sale.currency if self.sale else None
amount = Decimal(str(self.quantity or 0)) * (
Decimal(self.unit_price or 0) + self._get_premium_price())
if currency:
return currency.round(amount)
return amount
return Decimal(0)
@fields.depends(
'unit', 'product', 'price_type', 'enable_linked_currency',
'linked_currency', 'linked_unit', 'linked_price', 'premium',
methods=['on_change_with_unit_price', 'on_change_with_amount'])
def _recompute_trade_price_fields(self):
self.unit_price = self.on_change_with_unit_price()
self.amount = self.on_change_with_amount()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_premium(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_price_type(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_enable_linked_currency(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_linked_price(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_linked_currency(self):
self._recompute_trade_price_fields()
@fields.depends(methods=['_recompute_trade_price_fields'])
def on_change_linked_unit(self):
self._recompute_trade_price_fields()
return self.get_price()
def check_from_to(self,tr):
if tr.pricing_period:
@@ -1245,77 +929,28 @@ class SaleLine(metaclass=PoolMeta):
Pricing.save([p])
index += 1
@classmethod
def write(cls, *args):
Lot = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
old_values = {}
for records, values in zip(args[::2], args[1::2]):
if 'quantity_theorical' in values:
for record in records:
old_values[record.id] = record.quantity_theorical
super().write(*args)
lines = sum(args[::2], [])
for line in lines:
if line.id not in old_values:
continue
old = Decimal(old_values[line.id] or 0)
new = Decimal(line.quantity_theorical or 0)
delta = new - old
if delta == 0:
continue
virtual_lots = [
lot for lot in (line.lots or [])
if lot.lot_type == 'virtual'
]
if not virtual_lots:
continue
vlot = virtual_lots[0]
lqts = LotQt.search([
('lot_s', '=', vlot.id),
('lot_p', '=', None),
('lot_shipment_in', '=', None),
('lot_shipment_internal', '=', None),
('lot_shipment_out', '=', None),
])
if delta > 0:
new_qty = round(
Decimal(vlot.get_current_quantity_converted() or 0) + delta,
5)
vlot.set_current_quantity(new_qty, new_qty, 1)
Lot.save([vlot])
if lqts:
lqt = lqts[0]
lqt.lot_quantity = round(
Decimal(lqt.lot_quantity or 0) + delta, 5)
LotQt.save([lqt])
else:
lqt = LotQt()
lqt.lot_p = None
lqt.lot_s = vlot.id
lqt.lot_quantity = round(delta, 5)
lqt.lot_unit = line.unit
LotQt.save([lqt])
elif delta < 0:
decrease = abs(delta)
if not lqts or Decimal(lqts[0].lot_quantity or 0) < decrease:
raise UserError("Please unlink or unmatch lot")
new_qty = round(
Decimal(vlot.get_current_quantity_converted() or 0)
- decrease,
5)
vlot.set_current_quantity(new_qty, new_qty, 1)
Lot.save([vlot])
lqt = lqts[0]
lqt.lot_quantity = round(
Decimal(lqt.lot_quantity or 0) - decrease, 5)
LotQt.save([lqt])
# @classmethod
# def write(cls, records, values):
# if 'quantity' in values:
# for record in records:
# old_qt = record.quantity
# new_qt = values['quantity']
# logger.info("WRITE_OLD_QT:%s",old_qt)
# logger.info("WRITE_NEW_QT:%s",new_qt)
# if old_qt != new_qt:
# LotQt = Pool().get('lot.qt')
# lqts = LotQt.search(['lot_s','=',record.lots[0]])
# if len(lqts)>1:
# raise UserError("You cannot changed quantity with open quantities defined !")
# return
# elif len(lqts)==1:
# if lqts[0].lot_p or lqts[0].lot_shipment_origin:
# raise UserError("You cannot changed quantity with open quantities defined !")
# return
# lqts[0].lot_quantity = new_qt
# LotQt.save(lqts)
# super().write(records, values)
@classmethod
def delete(cls, lines):
@@ -1354,15 +989,14 @@ class SaleLine(metaclass=PoolMeta):
return super().copy(lines, default=default)
@classmethod
def validate(cls, salelines):
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
Pnl = Pool().get('valuation.valuation')
super(SaleLine, cls).validate(salelines)
for line in salelines:
if line.price_components:
for pc in line.price_components:
if pc.triggers:
def validate(cls, salelines):
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
super(SaleLine, cls).validate(salelines)
for line in salelines:
if line.price_components:
for pc in line.price_components:
if pc.triggers:
for tr in pc.triggers:
line.check_from_to(tr)
line.check_pricing()
@@ -1399,22 +1033,18 @@ class SaleLine(metaclass=PoolMeta):
fl.sale_line = line.id
FeeLots.save([fl])
#generate valuation for purchase and sale
LotQt = Pool().get('lot.qt')
line = cls(line.id)
generated_purchase_side = False
if line.lots:
for lot in line.lots:
lqts = LotQt.search([('lot_s','=',lot.id),('lot_p','>',0)])
logger.info("VALIDATE_SL:%s",lqts)
if lqts:
generated_purchase_side = True
purchase_lines = [e.lot_p.line for e in lqts]
if purchase_lines:
for pl in purchase_lines:
Pnl.generate(pl)
if line.lots and not generated_purchase_side:
Pnl.generate_from_sale_line(line)
#generate valuation for purchase and sale
LotQt = Pool().get('lot.qt')
if line.lots:
for lot in line.lots:
lqts = LotQt.search([('lot_s','=',lot.id),('lot_p','>',0)])
logger.info("VALIDATE_SL:%s",lqts)
if lqts:
purchase_lines = [e.lot_p.line for e in lqts]
if purchase_lines:
for pl in purchase_lines:
Pnl = Pool().get('valuation.valuation')
Pnl.generate(pl)
class SaleCreatePurchase(Wizard):
"Create mirror purchase"
@@ -1552,12 +1182,11 @@ class ValuationDyn(metaclass=PoolMeta):
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.base_amount).as_('r_base_amount'),
Sum(val.rate).as_('r_rate'),
Avg(val.mtm_price).as_('r_mtm_price'),
Sum(val.mtm).as_('r_mtm'),
Max(val.strategy).as_('r_strategy'),
Sum(val.amount).as_('r_amount'),
Sum(val.base_amount).as_('r_base_amount'),
Sum(val.rate).as_('r_rate'),
Sum(val.mtm).as_('r_mtm'),
Max(val.strategy).as_('r_strategy'),
Max(val.lot).as_('r_lot'),
Max(val.sale_line).as_('r_sale_line'),
where=wh,

View File

@@ -4,7 +4,6 @@ import logging
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
logger = logging.getLogger(__name__)
@@ -36,11 +35,16 @@ class ContractFactory:
Date = pool.get('ir.date')
created = []
sources = cls._get_sources(ct, type_)
base_contract = cls._get_base_contract(sources, ct, type_)
base_contract = (
ct.lot.sale_line.sale
if type_ == 'Purchase'
else ct.lot.line.purchase
)
for c in contracts:
contract = Purchase() if type_ == 'Purchase' else Sale()
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
# ---------- CONTRACT ----------
parts = c.currency_unit.split("_")
@@ -75,34 +79,33 @@ class ContractFactory:
contract.save()
line_sources = cls._get_line_sources(c, sources, ct)
for source in line_sources:
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
# ---------- LINE ----------
line.quantity = c.quantity
line.quantity_theorical = c.quantity
line.product = ct.product
line.unit = ct.unit
line.price_type = c.price_type
line.created_by_code = ct.matched
line.premium = Decimal(0)
# ---------- LINE ----------
line.quantity = source['quantity']
line.quantity_theorical = source['quantity']
line.product = ct.product
line.unit = ct.unit
line.price_type = c.price_type
line.created_by_code = ct.matched
line.premium = Decimal(0)
if type_ == 'Purchase':
line.purchase = contract.id
else:
line.sale = contract.id
if type_ == 'Purchase':
line.purchase = contract.id
else:
line.sale = contract.id
cls._apply_price(line, c, parts)
cls._apply_price(line, c, parts)
cls._apply_delivery(line, c, source)
line.del_period = c.del_period
line.from_del = c.from_del
line.to_del = c.to_del
line.save()
line.save()
logger.info("CREATE_ID:%s", contract.id)
logger.info("CREATE_LINE_ID:%s", line.id)
logger.info("CREATE_ID:%s", contract.id)
logger.info("CREATE_LINE_ID:%s", line.id)
if ct.matched:
cls._create_lot(line, c, source, type_)
if ct.matched:
cls._create_lot(line, c, ct, type_)
created.append(contract)
@@ -152,124 +155,12 @@ class ContractFactory:
else:
line.unit_price = c.price if c.price else Decimal(0)
@staticmethod
def _apply_delivery(line, c, source):
source_line = source.get('trade_line')
if source.get('use_source_delivery') and source_line:
line.del_period = getattr(source_line, 'del_period', None)
line.from_del = getattr(source_line, 'from_del', None)
line.to_del = getattr(source_line, 'to_del', None)
return
line.del_period = c.del_period
line.from_del = c.from_del
line.to_del = c.to_del
@staticmethod
def _normalize_quantity(quantity):
return abs(Decimal(str(quantity or 0))).quantize(Decimal('0.00001'))
@classmethod
def _get_base_contract(cls, sources, ct, type_):
if sources:
source_lot = sources[0]['lot']
return (
source_lot.sale_line.sale
if type_ == 'Purchase'
else source_lot.line.purchase
)
return (
ct.lot.sale_line.sale
if type_ == 'Purchase'
else ct.lot.line.purchase
)
@classmethod
def _get_sources(cls, ct, type_):
pool = Pool()
LotQt = pool.get('lot.qt')
context = Transaction().context or {}
active_ids = context.get('active_ids') or []
sources = []
if active_ids:
for record_id in active_ids:
if record_id < 10000000:
continue
lqt = LotQt(record_id - 10000000)
lot = lqt.lot_p or lqt.lot_s
if not lot:
continue
trade_line = (
lot.sale_line if type_ == 'Purchase' else lot.line
)
sources.append({
'lqt': lqt,
'lot': lot,
'trade_line': trade_line,
'quantity': cls._normalize_quantity(lqt.lot_quantity),
'shipment_origin': lqt.lot_shipment_origin,
})
elif getattr(ct, 'lot', None):
lot = ct.lot
trade_line = (
lot.sale_line if type_ == 'Purchase' else lot.line
)
sources.append({
'lqt': None,
'lot': lot,
'trade_line': trade_line,
'quantity': cls._normalize_quantity(getattr(ct, 'quantity', 0)),
'shipment_origin': cls._get_shipment_origin(ct),
})
cls._validate_sources(sources, type_)
return sources
@classmethod
def _validate_sources(cls, sources, type_):
if not sources:
return
first_line = sources[0]['trade_line']
for source in sources[1:]:
line = source['trade_line']
if bool(getattr(line, 'sale', None)) != bool(getattr(first_line, 'sale', None)):
raise UserError('Selected lots must all come from the same side.')
if getattr(line.product, 'id', None) != getattr(first_line.product, 'id', None):
raise UserError('Selected lots must share the same product.')
if getattr(line.unit, 'id', None) != getattr(first_line.unit, 'id', None):
raise UserError('Selected lots must share the same unit.')
@classmethod
def _get_line_sources(cls, contract_detail, sources, ct):
if not ct.matched or len(sources) <= 1:
quantity = cls._normalize_quantity(contract_detail.quantity)
source = sources[0] if sources else {
'lot': getattr(ct, 'lot', None),
'trade_line': None,
'shipment_origin': cls._get_shipment_origin(ct),
}
return [{
**source,
'quantity': quantity,
'use_source_delivery': False,
}]
selected_total = sum(source['quantity'] for source in sources)
requested = cls._normalize_quantity(contract_detail.quantity)
if requested != selected_total:
raise UserError(
'For multi-lot matched creation, quantity must equal the total selected open quantity.'
)
return [{
**source,
'use_source_delivery': True,
} for source in sources]
# -------------------------------------------------------------------------
# LOT / MATCHING (repris tel quel du wizard)
# -------------------------------------------------------------------------
@classmethod
def _create_lot(cls, line, c, source, type_):
def _create_lot(cls, line, c, ct, type_):
pool = Pool()
Lot = pool.get('lot.lot')
LotQtHist = pool.get('lot.qt.hist')
@@ -301,9 +192,10 @@ class ContractFactory:
lot.save()
vlot = source['lot']
shipment_origin = source.get('shipment_origin')
qt = source['quantity']
vlot = ct.lot
shipment_origin = cls._get_shipment_origin(ct)
qt = c.quantity
if type_ == 'Purchase':
if not lot.updateVirtualPart(qt, shipment_origin, vlot):

View File

@@ -6,7 +6,7 @@ from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
@@ -23,10 +23,8 @@ import io
import base64
import logging
import json
import re
import html
from trytond.exceptions import UserError
from trytond.modules.stock.shipment import SupplierShipping as BaseSupplierShipping
import re
import html
logger = logging.getLogger(__name__)
@@ -389,8 +387,8 @@ class ShipmentWR(ModelSQL,ModelView):
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
wr = fields.Many2One('weight.report',"WR")
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
from_location = fields.Many2One('stock.location', 'From location')
to_location = fields.Many2One('stock.location', 'To location')
@@ -441,10 +439,9 @@ class ShipmentIn(metaclass=PoolMeta):
'shipment',
'Container'
)
shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
controller = fields.Many2One('party.party',"Controller")
surveyor = fields.Many2One('party.party', "Surveyor")
controller_target = fields.Char("Targeted controller")
shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
controller = fields.Many2One('party.party',"Controller")
controller_target = fields.Char("Targeted controller")
send_instruction = fields.Boolean("Send instruction")
instructions = fields.Text("Instructions")
add_bl = fields.Boolean("Add BL")
@@ -462,196 +459,9 @@ class ShipmentIn(metaclass=PoolMeta):
'send': {},
})
def get_vessel_type(self,name=None):
if self.vessel:
return self.vessel.vessel_type
def _get_report_primary_move(self):
moves = list(self.incoming_moves or self.moves or [])
return moves[0] if moves else None
def _get_report_primary_lot(self):
move = self._get_report_primary_move()
return getattr(move, 'lot', None) if move else None
def _get_report_trade_line(self):
lot = self._get_report_primary_lot()
if not lot:
return None
return getattr(lot, 'sale_line', None) or getattr(lot, 'line', None)
def _get_report_insurance_fee(self):
for fee in self.fees or []:
product = getattr(fee, 'product', None)
name = ((getattr(product, 'name', '') or '')).strip().lower()
if 'insurance' in name:
return fee
return None
def _get_report_incoming_amount_data(self):
total = Decimal('0.0')
currency = None
for move in (self.incoming_moves or []):
move_amount, move_currency = self._get_report_incoming_move_amount(
move)
total += move_amount
if not currency and move_currency:
currency = move_currency
return total, currency
def _get_report_incoming_move_amount(self, move):
quantity = Decimal(str(getattr(move, 'quantity', 0) or 0))
unit_price = getattr(move, 'unit_price', None)
if unit_price not in (None, ''):
move_currency = getattr(move, 'currency', None)
return quantity * Decimal(str(unit_price or 0)), move_currency
lot = getattr(move, 'lot', None)
line = getattr(lot, 'line', None) if lot else None
if not lot or not line:
return Decimal('0.0'), None
lot_quantity = Decimal(str(
lot.get_current_quantity_converted() or 0))
line_unit_price = Decimal(str(getattr(line, 'unit_price', 0) or 0))
trade = getattr(line, 'purchase', None)
line_currency = getattr(trade, 'currency', None) if trade else None
return lot_quantity * line_unit_price, line_currency
@staticmethod
def _get_report_currency_text(currency):
return (
getattr(currency, 'rec_name', None)
or getattr(currency, 'code', None)
or getattr(currency, 'symbol', None)
or '')
@staticmethod
def _format_report_amount(value):
if value in (None, ''):
return ''
value = Decimal(str(value or 0)).quantize(Decimal('0.01'))
return format(value, 'f')
@property
def report_product_name(self):
line = self._get_report_trade_line()
product = getattr(line, 'product', None) if line else None
if product:
return product.name or ''
move = self._get_report_primary_move()
product = getattr(move, 'product', None) if move else None
return getattr(product, 'name', '') or ''
@property
def report_product_description(self):
line = self._get_report_trade_line()
product = getattr(line, 'product', None) if line else None
if product:
return product.description or ''
move = self._get_report_primary_move()
product = getattr(move, 'product', None) if move else None
return getattr(product, 'description', '') or ''
@property
def report_insurance_footer_ref(self):
return self.bl_number or self.number or ''
@property
def report_insurance_certificate_number(self):
return self.bl_number or self.number or ''
@property
def report_insurance_account_of(self):
line = self._get_report_trade_line()
trade = getattr(line, 'sale', None) or getattr(line, 'purchase', None)
party = getattr(trade, 'party', None) if trade else None
if party:
return party.rec_name or ''
return getattr(self.supplier, 'rec_name', '') or ''
@property
def report_insurance_goods_description(self):
name = self.report_product_name
description = self.report_product_description
if description and description != name:
return ' - '.join(part for part in [name, description] if part)
return name or description
@property
def report_insurance_loading_port(self):
return getattr(self.from_location, 'name', '') or ''
@property
def report_insurance_discharge_port(self):
return getattr(self.to_location, 'name', '') or ''
@property
def report_insurance_transport(self):
if self.vessel and self.vessel.vessel_name:
return self.vessel.vessel_name
return self.transport_type or ''
@property
def report_insurance_amount(self):
insured_amount, insured_currency = self._get_report_incoming_amount_data()
if insured_amount:
insured_amount *= Decimal('1.10')
currency_text = self._get_report_currency_text(insured_currency)
amount_text = self._format_report_amount(insured_amount)
return ' '.join(part for part in [currency_text, amount_text] if part)
fee = self._get_report_insurance_fee()
if not fee:
return ''
currency = getattr(fee, 'currency', None)
currency_text = self._get_report_currency_text(currency)
amount = self._format_report_amount(fee.get_amount())
return ' '.join(part for part in [currency_text, amount] if part)
@property
def report_insurance_incoming_amount(self):
amount, currency = self._get_report_incoming_amount_data()
currency_text = self._get_report_currency_text(currency)
amount_text = self._format_report_amount(amount)
return ' '.join(part for part in [currency_text, amount_text] if part)
@property
def report_insurance_amount_insured(self):
amount, currency = self._get_report_incoming_amount_data()
insured_amount = amount * Decimal('1.10')
currency_text = self._get_report_currency_text(currency)
amount_text = self._format_report_amount(insured_amount)
return ' '.join(part for part in [currency_text, amount_text] if part)
@property
def report_insurance_surveyor(self):
if self.surveyor:
return self.surveyor.rec_name or ''
if self.controller:
return self.controller.rec_name or ''
fee = self._get_report_insurance_fee()
supplier = getattr(fee, 'supplier', None) if fee else None
return getattr(supplier, 'rec_name', '') or ''
@property
def report_insurance_contact_surveyor(self):
return self.report_insurance_surveyor
@property
def report_insurance_issue_place_and_date(self):
Date = Pool().get('ir.date')
address = None
if self.company and getattr(self.company, 'party', None):
address = self.company.party.address_get()
place = (
getattr(address, 'city', None)
or getattr(self.company.party, 'rec_name', None)
if self.company and getattr(self.company, 'party', None) else ''
) or ''
today = Date.today()
date_text = today.strftime('%d-%m-%Y') if today else ''
return ', '.join(part for part in [place, date_text] if part)
def get_vessel_type(self,name=None):
if self.vessel:
return self.vessel.vessel_type
def get_rec_name(self, name=None):
if self.number:
@@ -677,26 +487,16 @@ class ShipmentIn(metaclass=PoolMeta):
fee.price = price
Fee.save([fee])
def get_controller(self):
ControllerCategory = Pool().get('party.category')
PartyCategory = Pool().get('party.party-party.category')
cc = ControllerCategory.search(['name','=','CONTROLLER'])
if cc:
cc = cc[0]
controllers = PartyCategory.search(['category','=',cc.id])
prioritized = []
for c in controllers:
if not c.party.IsAvailableForControl(self):
continue
gap, rule = c.party.get_controller_execution_priority(self)
prioritized.append((
1 if rule else 0,
gap if gap is not None else Decimal('-999999'),
c.party,
))
if prioritized:
prioritized.sort(key=lambda item: (item[0], item[1]), reverse=True)
return prioritized[0][2]
def get_controller(self):
ControllerCategory = Pool().get('party.category')
PartyCategory = Pool().get('party.party-party.category')
cc = ControllerCategory.search(['name','=','CONTROLLER'])
if cc:
cc = cc[0]
controllers = PartyCategory.search(['category','=',cc.id])
for c in controllers:
if c.party.IsAvailableForControl(self):
return c.party
def get_instructions_html(self,inv_date,inv_nb):
vessel = self.vessel.vessel_name if self.vessel else ""
@@ -2078,7 +1878,7 @@ class Revaluate(Wizard):
return 'end'
class RevaluateStart(ModelView):
class RevaluateStart(ModelView):
"Revaluate"
__name__ = 'account.revaluate.start'
revaluation_date = fields.Date(
@@ -2092,64 +1892,5 @@ class RevaluateStart(ModelView):
return Date.today()
@classmethod
def default_delete_after(cls):
return False
class ShipmentTemplateReportMixin:
@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_report_path(cls, action):
if isinstance(action, dict):
return action.get('report') or ''
return getattr(action, 'report', '') or ''
@classmethod
def _resolve_template_path(cls, field_name, default_prefix):
config = cls._get_purchase_trade_configuration()
template = getattr(config, field_name, '') if config else ''
template = (template or '').strip()
if not template:
raise UserError('No template found')
if '/' not in template:
return f'{default_prefix}/{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)
class ShipmentShippingReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
__name__ = 'stock.shipment.in.shipping'
@classmethod
def _resolve_configured_report_path(cls, action):
return cls._resolve_template_path(
'shipment_shipping_report_template', 'stock')
class ShipmentInsuranceReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
__name__ = 'stock.shipment.in.insurance'
@classmethod
def _resolve_configured_report_path(cls, action):
return cls._resolve_template_path(
'shipment_insurance_report_template', 'stock')
def default_delete_after(cls):
return False

View File

@@ -56,22 +56,10 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">stock.shipment.in,-1</field>
<field name="action" ref="act_vf"/>
</record>
<record model="ir.action.url" id="url_vessel_finder">
<field name="name">Find Vessel</field>
<field name="url">https://www.vesselfinder.com</field>
</record>
<record model="ir.action.report" id="report_shipment_in_insurance">
<field name="name">Insurance</field>
<field name="model">stock.shipment.in</field>
<field name="report_name">stock.shipment.in.insurance</field>
<field name="report">stock/insurance.fodt</field>
</record>
<record model="ir.action.keyword" id="report_shipment_in_insurance_keyword">
<field name="keyword">form_print</field>
<field name="model">stock.shipment.in,-1</field>
<field name="action" ref="report_shipment_in_insurance"/>
</record>
<record model="ir.action.url" id="url_vessel_finder">
<field name="name">Find Vessel</field>
<field name="url">https://www.vesselfinder.com</field>
</record>
<record model="ir.action.wizard" id="act_update_sof">
<field name="name">Update with SoF PDF</field>
@@ -138,4 +126,4 @@ this repository contains the full copyright notices and license terms. -->
id="menu_revaluate"/>
</data>
</tryton>
</tryton>

View File

@@ -6,9 +6,6 @@ from unittest.mock import Mock, patch
from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.exceptions import UserError
from trytond.modules.purchase_trade import valuation as valuation_module
from trytond.modules.purchase_trade.service import ContractFactory
class PurchaseTradeTestCase(ModuleTestCase):
@@ -73,807 +70,5 @@ class PurchaseTradeTestCase(ModuleTestCase):
strategy.get_mtm(line, Decimal('10')),
Decimal('250.00'))
def test_get_strategy_mtm_price_returns_unit_price(self):
'strategy mtm price exposes the raw unit valuation price'
strategy = Mock(
scenario=Mock(
valuation_date='2026-03-29',
use_last_price=True,
),
currency=Mock(),
)
strategy.components = [Mock(
price_source_type='curve',
price_index=Mock(get_price=Mock(return_value=Decimal('100'))),
price_matrix=None,
ratio=Decimal('25'),
)]
line = Mock(unit=Mock())
self.assertEqual(
valuation_module.Valuation._get_strategy_mtm_price(strategy, line),
Decimal('100.0000'))
def test_sale_line_is_unmatched_checks_lot_links(self):
'sale line unmatched helper ignores empty matches and detects linked purchases'
sale_line = Mock()
sale_line.get_matched_lines.return_value = []
self.assertTrue(
valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line))
linked = Mock(lot_p=Mock(line=Mock()))
sale_line.get_matched_lines.return_value = [linked]
self.assertFalse(
valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line))
def test_parse_numbers_supports_inline_and_legacy_separators(self):
'parse_numbers keeps supporting inline entry and legacy separators'
self.assertEqual(
valuation_module.ValuationProcess._parse_numbers(
'PUR-001 PUR-002, PUR-003\nPUR-004;PUR-005'
),
['PUR-001', 'PUR-002', 'PUR-003', 'PUR-004', 'PUR-005'])
def test_get_generate_types_maps_business_groups(self):
'valuation type groups map to the expected stored valuation types'
Valuation = Pool().get('valuation.valuation')
self.assertEqual(
Valuation._get_generate_types('fees'),
{'line fee', 'pur. fee', 'sale fee', 'shipment fee'})
self.assertEqual(
Valuation._get_generate_types('derivatives'),
{'derivative'})
self.assertIn('pur. priced', Valuation._get_generate_types('goods'))
def test_filter_values_by_types_keeps_matching_entries_only(self):
'type filtering keeps only the requested valuation entries'
Valuation = Pool().get('valuation.valuation')
values = [
{'type': 'pur. fee', 'amount': Decimal('10')},
{'type': 'pur. priced', 'amount': Decimal('20')},
{'type': 'derivative', 'amount': Decimal('30')},
]
self.assertEqual(
Valuation._filter_values_by_types(
values, {'pur. fee', 'derivative'}),
[
{'type': 'pur. fee', 'amount': Decimal('10')},
{'type': 'derivative', 'amount': Decimal('30')},
])
def test_sale_report_crop_name_handles_missing_crop(self):
'sale report crop name returns an empty string when crop is missing'
Sale = Pool().get('sale.sale')
sale = Sale()
sale.crop = None
self.assertEqual(sale.report_crop_name, '')
sale.crop = Mock(name='crop')
sale.crop.name = 'Main Crop'
self.assertEqual(sale.report_crop_name, 'Main Crop')
def test_sale_line_default_pricing_rule_comes_from_configuration(self):
'sale line pricing_rule defaults to the purchase_trade singleton value'
SaleLine = Pool().get('sale.line')
config = Mock(pricing_rule='Default pricing rule')
configuration_model = Mock()
configuration_model.search.return_value = [config]
with patch(
'trytond.modules.purchase_trade.sale.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = configuration_model
self.assertEqual(
SaleLine.default_pricing_rule(), 'Default pricing rule')
def test_purchase_line_default_pricing_rule_comes_from_configuration(self):
'purchase line pricing_rule defaults to the purchase_trade singleton value'
PurchaseLine = Pool().get('purchase.line')
config = Mock(pricing_rule='Default pricing rule')
configuration_model = Mock()
configuration_model.search.return_value = [config]
with patch(
'trytond.modules.purchase_trade.purchase.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = configuration_model
self.assertEqual(
PurchaseLine.default_pricing_rule(), 'Default pricing rule')
def test_sale_line_write_updates_virtual_lot_when_theorical_qty_increases(self):
'sale line write increases virtual lot and open lot.qt when contractual qty grows'
SaleLine = Pool().get('sale.line')
line = Mock(id=1, quantity_theorical=Decimal('10'))
line.unit = Mock()
vlot = Mock(id=99, lot_type='virtual')
vlot.get_current_quantity_converted.return_value = Decimal('10')
line.lots = [vlot]
lqt = Mock(lot_quantity=Decimal('10'))
lot_model = Mock()
lotqt_model = Mock()
lotqt_model.search.return_value = [lqt]
with patch(
'trytond.modules.purchase_trade.sale.Pool'
) as PoolMock, patch(
'trytond.modules.purchase_trade.sale.super'
) as super_mock:
PoolMock.return_value.get.side_effect = lambda name: {
'lot.lot': lot_model,
'lot.qt': lotqt_model,
}[name]
def fake_super_write(*args):
for records, values in zip(args[::2], args[1::2]):
if 'quantity_theorical' in values:
for record in records:
record.quantity_theorical = values['quantity_theorical']
super_mock.return_value.write.side_effect = fake_super_write
SaleLine.write([line], {'quantity_theorical': Decimal('12')})
self.assertEqual(lqt.lot_quantity, Decimal('12'))
vlot.set_current_quantity.assert_called_once_with(
Decimal('12'), Decimal('12'), 1)
lot_model.save.assert_called()
lotqt_model.save.assert_called()
def test_sale_line_write_blocks_theorical_qty_decrease_when_no_open_quantity(self):
'sale line write blocks contractual qty decrease when open lot.qt is insufficient'
SaleLine = Pool().get('sale.line')
line = Mock(id=2, quantity_theorical=Decimal('10'))
vlot = Mock(id=100, lot_type='virtual')
vlot.get_current_quantity_converted.return_value = Decimal('10')
line.lots = [vlot]
lqt = Mock(lot_quantity=Decimal('1'))
lot_model = Mock()
lotqt_model = Mock()
lotqt_model.search.return_value = [lqt]
with patch(
'trytond.modules.purchase_trade.sale.Pool'
) as PoolMock, patch(
'trytond.modules.purchase_trade.sale.super'
) as super_mock:
PoolMock.return_value.get.side_effect = lambda name: {
'lot.lot': lot_model,
'lot.qt': lotqt_model,
}[name]
def fake_super_write(*args):
for records, values in zip(args[::2], args[1::2]):
if 'quantity_theorical' in values:
for record in records:
record.quantity_theorical = values['quantity_theorical']
super_mock.return_value.write.side_effect = fake_super_write
with self.assertRaises(UserError):
SaleLine.write([line], {'quantity_theorical': Decimal('8')})
def test_party_execution_achieved_percent_uses_real_area_statistics(self):
'party execution achieved percent reflects the controller share in its area'
PartyExecution = Pool().get('party.execution')
execution = PartyExecution()
execution.party = Mock(id=1)
execution.area = Mock(id=10)
shipments = [
Mock(controller=Mock(id=1)),
Mock(controller=Mock(id=2)),
Mock(controller=Mock(id=1)),
Mock(controller=Mock(id=2)),
Mock(controller=Mock(id=1)),
]
shipment_model = Mock()
shipment_model.search.return_value = shipments
with patch(
'trytond.modules.purchase_trade.party.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = shipment_model
self.assertEqual(
execution.get_percent('achieved_percent'),
Decimal('60.00'))
def test_get_controller_prioritizes_controller_farthest_from_target(self):
'shipment controller selection prioritizes the most under-target rule'
Shipment = Pool().get('stock.shipment.in')
Party = Pool().get('party.party')
PartyExecution = Pool().get('party.execution')
shipment = Shipment()
shipment.to_location = Mock(
country=Mock(region=Mock(id=20, parent=Mock(id=10, parent=None))))
party_a = Party()
party_a.id = 1
rule_a = PartyExecution()
rule_a.party = party_a
rule_a.area = Mock(id=10)
rule_a.percent = Decimal('80')
rule_a.compute_achieved_percent = Mock(return_value=Decimal('40'))
party_a.execution = [rule_a]
party_b = Party()
party_b.id = 2
rule_b = PartyExecution()
rule_b.party = party_b
rule_b.area = Mock(id=10)
rule_b.percent = Decimal('50')
rule_b.compute_achieved_percent = Mock(return_value=Decimal('45'))
party_b.execution = [rule_b]
category_model = Mock()
category_model.search.return_value = [Mock(id=99)]
party_category_model = Mock()
party_category_model.search.return_value = [
Mock(party=party_b),
Mock(party=party_a),
]
with patch(
'trytond.modules.purchase_trade.stock.Pool'
) as PoolMock:
def get_model(name):
return {
'party.category': category_model,
'party.party-party.category': party_category_model,
}[name]
PoolMock.return_value.get.side_effect = get_model
self.assertIs(shipment.get_controller(), party_a)
def test_weight_report_get_source_shipment_rejects_multiple_shipments(self):
'weight report export must not guess when the same WR is linked twice'
WeightReport = Pool().get('weight.report')
report = WeightReport()
report.id = 7
shipment_wr_model = Mock()
shipment_wr_model.search.return_value = [
Mock(shipment_in=Mock(id=1)),
Mock(shipment_in=Mock(id=2)),
]
with patch(
'trytond.modules.purchase_trade.weight_report.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = shipment_wr_model
with self.assertRaises(UserError):
report.get_source_shipment()
def test_weight_report_remote_context_requires_controller_and_returned_id(self):
'weight report export checks the shipment prerequisites before calling FastAPI'
WeightReport = Pool().get('weight.report')
report = WeightReport()
report.bales = 100
report.report_date = Mock(strftime=Mock(return_value='20260406'))
report.weight_date = Mock(strftime=Mock(return_value='20260406'))
shipment = Mock(
controller=None,
returned_id='RET-001',
agent=Mock(),
to_location=Mock(),
)
with self.assertRaises(UserError):
report.validate_remote_weight_report_context(shipment)
shipment.controller = Mock()
shipment.returned_id = None
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(
sale_report_template='sale_melya.fodt',
sale_bill_report_template='bill_melya.fodt',
sale_final_report_template='sale_final_melya.fodt',
invoice_report_template='invoice_melya.fodt',
invoice_cndn_report_template='invoice_ict_final.fodt',
invoice_prepayment_report_template='prepayment.fodt',
invoice_payment_order_report_template='payment_order.fodt',
purchase_report_template='purchase_melya.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')
self.assertEqual(
report_class._resolve_configured_report_path({
'name': 'Payment Order',
'report': 'account_invoice/payment_order.fodt',
}),
'account_invoice/payment_order.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='',
invoice_payment_order_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',
})
with self.assertRaises(UserError):
report_class._resolve_configured_report_path({
'name': 'Payment Order',
'report': 'account_invoice/payment_order.fodt',
})
def test_sale_report_uses_templates_from_configuration(self):
'sale report paths are resolved from purchase_trade configuration'
report_class = Pool().get('sale.sale', type='report')
config_model = Mock()
config_model.search.return_value = [
Mock(
sale_report_template='sale_melya.fodt',
sale_bill_report_template='bill_melya.fodt',
sale_final_report_template='sale_final_melya.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': 'Sale',
'report': 'sale/sale.fodt',
}),
'sale/sale_melya.fodt')
self.assertEqual(
report_class._resolve_configured_report_path({
'name': 'Bill',
'report': 'sale/bill.fodt',
}),
'sale/bill_melya.fodt')
self.assertEqual(
report_class._resolve_configured_report_path({
'name': 'Sale (final)',
'report': 'sale/sale_final.fodt',
}),
'sale/sale_final_melya.fodt')
def test_purchase_report_uses_template_from_configuration(self):
'purchase report path is resolved from purchase_trade configuration'
report_class = Pool().get('purchase.purchase', type='report')
config_model = Mock()
config_model.search.return_value = [
Mock(purchase_report_template='purchase_melya.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': 'Purchase',
'report': 'purchase/purchase.fodt',
}),
'purchase/purchase_melya.fodt')
def test_shipment_reports_use_templates_from_configuration(self):
'shipment report paths are resolved from purchase_trade configuration'
shipping_report = Pool().get('stock.shipment.in.shipping', type='report')
insurance_report = Pool().get('stock.shipment.in.insurance', type='report')
config_model = Mock()
config_model.search.return_value = [
Mock(
shipment_shipping_report_template='si_custom.fodt',
shipment_insurance_report_template='insurance_custom.fodt',
)
]
with patch(
'trytond.modules.purchase_trade.stock.Pool'
) as PoolMock:
PoolMock.return_value.get.return_value = config_model
self.assertEqual(
shipping_report._resolve_configured_report_path({
'name': 'Shipping instructions',
'report': 'stock/si.fodt',
}),
'stock/si_custom.fodt')
self.assertEqual(
insurance_report._resolve_configured_report_path({
'name': 'Insurance',
'report': 'stock/insurance.fodt',
}),
'stock/insurance_custom.fodt')
def test_shipment_insurance_helpers_use_fee_and_controller(self):
'shipment insurance helpers read insurance fee and shipment context'
ShipmentIn = Pool().get('stock.shipment.in')
shipment = ShipmentIn()
shipment.number = 'IN/0001'
shipment.bl_number = 'BL-001'
shipment.from_location = Mock(name='LIVERPOOL')
shipment.to_location = Mock(name='LE HAVRE')
shipment.vessel = Mock(vessel_name='MV ATLANTIC')
shipment.controller = Mock(rec_name='CONTROL UNION')
shipment.supplier = Mock(rec_name='MELYA SA')
sale_party = Mock(rec_name='SGT FR')
sale = Mock(party=sale_party)
product = Mock(name='COTTON UPLAND', description='RAW WHITE COTTON')
line = Mock(product=product, sale=sale)
lot = Mock(sale_line=line, line=None)
move = Mock(lot=lot, product=product)
shipment.incoming_moves = [move]
shipment.moves = [move]
insurance_fee = Mock()
insurance_fee.product = Mock(name='Insurance')
insurance_fee.currency = Mock(rec_name='USD')
insurance_fee.get_amount.return_value = Decimal('1234.56')
insurance_fee.supplier = Mock(rec_name='HELVETIA')
shipment.fees = [insurance_fee]
with patch(
'trytond.modules.purchase_trade.stock.Pool'
) as PoolMock:
date_model = Mock()
date_model.today.return_value = datetime.date(2026, 4, 6)
config_model = Mock()
PoolMock.return_value.get.side_effect = lambda name: {
'ir.date': date_model,
'purchase_trade.configuration': config_model,
}[name]
shipment.company = Mock(
party=Mock(
rec_name='MELYA SA',
address_get=Mock(return_value=Mock(city='GENEVA')),
)
)
self.assertEqual(
shipment.report_insurance_certificate_number, 'BL-001')
self.assertEqual(
shipment.report_insurance_account_of, 'SGT FR')
self.assertEqual(
shipment.report_insurance_goods_description,
'COTTON UPLAND - RAW WHITE COTTON')
self.assertEqual(
shipment.report_insurance_amount, 'USD 1234.56')
self.assertEqual(
shipment.report_insurance_surveyor, 'CONTROL UNION')
self.assertEqual(
shipment.report_insurance_issue_place_and_date,
'GENEVA, 06-04-2026')
def test_shipment_insurance_amount_fallback_uses_lot_and_incoming_moves(self):
'insurance amount falls back to lot unit price and shipment quantities'
ShipmentIn = Pool().get('stock.shipment.in')
shipment = ShipmentIn()
purchase_currency = Mock(rec_name='USD')
purchase = Mock(currency=purchase_currency)
line = Mock(unit_price=Decimal('100'), purchase=purchase)
lot = Mock(line=line)
lot.get_current_quantity_converted = Mock(return_value=Decimal('5'))
move = Mock(quantity=Decimal('0'), unit_price=None, currency=None, lot=lot)
shipment.incoming_moves = [move]
shipment.fees = []
self.assertEqual(
shipment.report_insurance_incoming_amount, 'USD 500.00')
self.assertEqual(
shipment.report_insurance_amount_insured, 'USD 550.00')
self.assertEqual(
shipment.report_insurance_amount, 'USD 550.00')
def test_shipment_insurance_contact_surveyor_prefers_shipment_surveyor(self):
'insurance contact surveyor property uses shipment surveyor first'
ShipmentIn = Pool().get('stock.shipment.in')
shipment = ShipmentIn()
shipment.surveyor = Mock(rec_name='SGS')
shipment.controller = Mock(rec_name='CONTROL UNION')
shipment.fees = []
self.assertEqual(
shipment.report_insurance_contact_surveyor, 'SGS')
self.assertEqual(
shipment.report_insurance_surveyor, 'SGS')
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')
def make_line(quantity, period, linked_price):
line = Mock()
line.type = 'line'
line.quantity = quantity
line.note = ''
line.price_type = 'priced'
line.unit_price = Decimal('0')
line.linked_price = Decimal(linked_price)
line.linked_currency = Mock(rec_name='USC')
line.linked_unit = Mock(rec_name='POUND')
line.unit = Mock(rec_name='MT')
line.del_period = Mock(description=period)
line.get_pricing_text = f'ON ICE Cotton #2 {period}'
line.lots = []
return line
sale = Sale()
sale.currency = Mock(rec_name='USD')
sale.lines = [
make_line('1000', 'MARCH 2026', '72.5000'),
make_line('1000', 'MAY 2026', '70.2500'),
]
self.assertEqual(sale.report_total_quantity, '2000.0')
self.assertEqual(sale.report_quantity_unit_upper, 'MT')
self.assertEqual(sale.report_qt, 'TWO THOUSAND METRIC TONS')
self.assertEqual(sale.report_nb_bale, '')
self.assertEqual(
sale.report_quantity_lines.splitlines(),
[
'1000.0 MT (ONE THOUSAND METRIC TONS) - MARCH 2026',
'1000.0 MT (ONE THOUSAND METRIC TONS) - MAY 2026',
])
self.assertEqual(
sale.report_shipment_periods.splitlines(),
['MARCH 2026', 'MAY 2026'])
self.assertEqual(
sale.report_price_lines.splitlines(),
[
'USC 72.5000 PER POUND (SEVENTY TWO USC AND FIFTY CENTS) ON ICE Cotton #2 MARCH 2026',
'USC 70.2500 PER POUND (SEVENTY USC AND TWENTY FIVE CENTS) ON ICE Cotton #2 MAY 2026',
])
def test_report_product_fields_expose_name_and_description(self):
'sale and invoice templates use stable product name/description helpers'
Sale = Pool().get('sale.sale')
Invoice = Pool().get('account.invoice')
product = Mock(name='COTTON UPLAND', description='RAW WHITE COTTON')
sale_line = Mock(product=product)
invoice_line = Mock(product=product)
sale = Sale()
sale.lines = [sale_line]
invoice = Invoice()
invoice.lines = [invoice_line]
invoice._get_report_trade_line = Mock(return_value=sale_line)
self.assertEqual(sale.report_product_name, 'COTTON UPLAND')
self.assertEqual(sale.report_product_description, 'RAW WHITE COTTON')
self.assertEqual(invoice.report_product_name, 'COTTON UPLAND')
self.assertEqual(invoice.report_product_description, 'RAW WHITE COTTON')
def test_contract_factory_uses_one_line_per_selected_source(self):
'matched multi-lot contract creation keeps one generated line per source lot'
contract_detail = Mock(quantity=Decimal('2000'))
ct = Mock(matched=True)
line_a = Mock()
line_b = Mock()
sources = [
{'lot': Mock(), 'trade_line': line_a, 'quantity': Decimal('1000')},
{'lot': Mock(), 'trade_line': line_b, 'quantity': Decimal('1000')},
]
result = ContractFactory._get_line_sources(contract_detail, sources, ct)
self.assertEqual(len(result), 2)
self.assertEqual([r['quantity'] for r in result], [
Decimal('1000'), Decimal('1000')])
self.assertTrue(all(r['use_source_delivery'] for r in result))
def test_contract_factory_rejects_multi_lot_quantity_mismatch(self):
'matched multi-lot contract creation rejects totals that do not match the selection'
contract_detail = Mock(quantity=Decimal('1500'))
ct = Mock(matched=True)
sources = [
{'lot': Mock(), 'trade_line': Mock(), 'quantity': Decimal('1000')},
{'lot': Mock(), 'trade_line': Mock(), 'quantity': Decimal('1000')},
]
with self.assertRaises(UserError):
ContractFactory._get_line_sources(contract_detail, sources, ct)
def test_sale_report_price_lines_basis_displays_premium_only(self):
'basis report pricing displays only the premium in templates'
Sale = Pool().get('sale.sale')
line = Mock()
line.type = 'line'
line.price_type = 'basis'
line.enable_linked_currency = True
line.linked_currency = Mock(rec_name='USC')
line.linked_unit = Mock(rec_name='POUND')
line.unit = Mock(rec_name='MT')
line.unit_price = Decimal('1598.3495')
line.linked_price = Decimal('72.5000')
line.premium = Decimal('8.3000')
line.get_pricing_text = 'ON ICE Cotton #2 MARCH 2026'
sale = Sale()
sale.currency = Mock(rec_name='USD')
sale.lines = [line]
self.assertEqual(
sale.report_price_lines,
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026')
def test_sale_report_net_and_gross_sum_all_lines(self):
'sale report totals aggregate every line instead of the first one only'
Sale = Pool().get('sale.sale')
def make_lot(quantity):
lot = Mock()
lot.lot_type = 'physic'
lot.get_current_quantity.return_value = Decimal(quantity)
lot.get_current_gross_quantity.return_value = Decimal(quantity)
return lot
line_a = Mock(type='line', quantity=Decimal('1000'))
line_a.lots = [make_lot('1000')]
line_b = Mock(type='line', quantity=Decimal('1000'))
line_b.lots = [make_lot('1000')]
sale = Sale()
sale.lines = [line_a, line_b]
self.assertEqual(sale.report_net, Decimal('2000'))
self.assertEqual(sale.report_gross, Decimal('2000'))
def test_sale_report_trade_blocks_use_lot_current_quantity(self):
'sale trade blocks use current lot quantity for quantity display'
Sale = Pool().get('sale.sale')
lot = Mock()
lot.lot_type = 'physic'
lot.get_current_quantity.return_value = Decimal('950')
line = Mock()
line.type = 'line'
line.lots = [lot]
line.quantity = Decimal('1000')
line.unit = Mock(rec_name='MT')
line.del_period = Mock(description='MARCH 2026')
line.price_type = 'priced'
line.linked_currency = Mock(rec_name='USC')
line.linked_unit = Mock(rec_name='POUND')
line.linked_price = Decimal('8.3000')
line.unit_price = Decimal('0')
line.get_pricing_text = 'ON ICE Cotton #2 MARCH 2026'
sale = Sale()
sale.currency = Mock(rec_name='USD')
sale.lines = [line]
self.assertEqual(
sale.report_trade_blocks,
[(
'950.0 MT (NINE HUNDRED AND FIFTY METRIC TONS) - MARCH 2026',
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026',
)])
def test_invoice_report_note_title_uses_total_amount_sign(self):
'final invoice title switches between credit and debit note'
Invoice = Pool().get('account.invoice')
credit = Invoice()
credit.total_amount = Decimal('10')
self.assertEqual(credit.report_note_title, 'Credit Note')
debit = Invoice()
debit.total_amount = Decimal('-10')
self.assertEqual(debit.report_note_title, 'Debit Note')
def test_invoice_report_net_sums_signed_invoice_lines(self):
'invoice report net uses the signed differential from invoice lines'
Invoice = Pool().get('account.invoice')
line_a = Mock(type='line', quantity=Decimal('1000'))
line_b = Mock(type='line', quantity=Decimal('-200'))
invoice = Invoice()
invoice.lines = [line_a, line_b]
self.assertEqual(invoice.report_net, Decimal('800'))
def test_invoice_report_nb_bale_sums_signed_line_lot_quantities(self):
'invoice reports packaging from the signed sum of line lot_qt values'
Invoice = Pool().get('account.invoice')
lot = Mock(lot_qt=Decimal('350'), lot_unit=Mock(symbol='bale'))
negative = Mock(type='line', quantity=Decimal('-1000'), lot=lot)
positive = Mock(type='line', quantity=Decimal('1000'), lot=lot)
invoice = Invoice()
invoice.lines = [negative, positive]
self.assertEqual(invoice.report_nb_bale, 'NB BALES: 0')
def test_invoice_report_positive_rate_lines_keep_positive_components(self):
'invoice final note pricing section keeps only positive component lines'
Invoice = Pool().get('account.invoice')
sale = Mock()
sale.report_price_lines = (
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026\n'
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MAY 2026'
)
invoice = Invoice()
invoice.sales = [sale]
invoice.lines = []
self.assertEqual(
invoice.report_positive_rate_lines.splitlines(),
[
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MARCH 2026',
'USC 8.3000 PER POUND (EIGHT USC AND THIRTY CENTS) ON ICE Cotton #2 MAY 2026',
])
def test_lot_invoice_sale_uses_sale_invoice_line_reference(self):
'sale invoicing must resolve the generated invoice from sale invoice links'
sale_invoice = Mock()
sale_invoice_line = Mock(invoice=sale_invoice)
lot = Mock(
sale_invoice_line=sale_invoice_line,
sale_invoice_line_prov=None,
invoice_line=None,
invoice_line_prov=None,
)
invoice_line = lot.sale_invoice_line or lot.sale_invoice_line_prov
self.assertIs(invoice_line.invoice, sale_invoice)
del ModuleTestCase

View File

@@ -1,9 +1,8 @@
[tryton]
version=7.2.7
depends:
ir
price
purchase
depends:
ir
purchase
sale
account_invoice
stock
@@ -11,13 +10,12 @@ depends:
lot
document_incoming
incoterm
xml:
purchase.xml
sale.xml
lot.xml
pricing.xml
configuration.xml
stock.xml
xml:
purchase.xml
sale.xml
lot.xml
pricing.xml
stock.xml
workflow.xml
lc.xml
optional.xml
@@ -33,8 +31,7 @@ xml:
global_reporting.xml
derivative.xml
valuation.xml
weight_report.xml
dimension.xml
backtoback.xml
invoice.xml
account.xml
weight_report.xml
dimension.xml
backtoback.xml
account.xml

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,5 @@
<tryton>
<data>
<record model="ir.ui.view" id="valuation_process_dimension_view_tree">
<field name="model">valuation.process.dimension</field>
<field name="type">tree</field>
<field name="name">valuation_process_dimension_tree</field>
</record>
<record model="ir.ui.view" id="valuation_process_dimension_view_form">
<field name="model">valuation.process.dimension</field>
<field name="type">form</field>
<field name="name">valuation_process_dimension_form</field>
</record>
<record model="ir.ui.view" id="valuation_process_start_view_form">
<field name="model">valuation.process.start</field>
<field name="type">form</field>
<field name="name">valuation_process_start_form</field>
</record>
<record model="ir.ui.view" id="valuation_process_result_view_form">
<field name="model">valuation.process.result</field>
<field name="type">form</field>
<field name="name">valuation_process_result_form</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
<field name="model">valuation.valuation</field>
<field name="type">tree</field>
@@ -56,10 +36,6 @@
<field name="res_model">valuation.report</field>
<field name="context_model">valuation.report.context</field>
</record>
<record model="ir.action.wizard" id="act_valuation_process">
<field name="name">Process valuation</field>
<field name="wiz_name">valuation.process</field>
</record>
<record model="ir.action.act_window.view" id="act_valuation_form_view">
<field name="sequence" eval="70"/>
<field name="view" ref="valuation_view_list"/>
@@ -67,18 +43,9 @@
</record>
<menuitem
name="Valuation"
sequence="98"
id="menu_valuation"/>
<menuitem
parent="menu_valuation"
sequence="10"
action="act_valuation_process"
id="menu_valuation_process"/>
<menuitem
parent="menu_valuation"
sequence="20"
parent="purchase_trade.menu_global_reporting"
sequence="120"
action="act_valuation_form"
id="menu_valuation_form"/>
</data>
</tryton>
</tryton>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0"?>
<form col="4">
<label name="pricing_rule"/>
<field name="pricing_rule" colspan="3"/>
</form>

View File

@@ -9,13 +9,13 @@
<field name="tol_min"/>
<field name="tol_max"/>
<field name="price_type"/>
<field name="from_location"/>
<field name="to_location"/>
<field name="payment_term"/>
<field name="incoterm"/>
<field name="crop"/>
<field name="del_period"/>
<field name="from_del"/>
<field name="to_del"/>
<field name="from_location"/>
<field name="to_location"/>
<field name="category" tree_invisible="1"/>
</tree>

View File

@@ -1,4 +1,4 @@
<tree>
<tree editable="1">
<field name="r_lot_type" width="80">
<prefix name="qt_icon"/>
</field>

View File

@@ -2,37 +2,37 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
<newline/>
<label name="quantity_theorical"/>
<field name="quantity_theorical"/>
<label name="finished"/>
<field name="finished"/>
<newline/>
<label name="price_type"/>
<field name="price_type"/>
<label name="enable_linked_currency"/>
<field name="enable_linked_currency"/>
<newline/>
<label name="linked_currency"/>
<field name="linked_currency"/>
<label name="linked_unit"/>
<field name="linked_unit"/>
<newline/>
<label name="linked_price"/>
<field name="linked_price"/>
<label name="premium"/>
<field name="premium"/>
<newline/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/field[@name='amount']" position="after">
<newline/>
<label name="progress"/>
<field name="progress" widget="progressbar"/>
<newline/>
<label name="inherit_tol"/>
<field name="inherit_tol"/>
<newline/>
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
<newline/>
<label name="quantity_theorical"/>
<field name="quantity_theorical"/>
<label name="finished"/>
<field name="finished"/>
<newline/>
<label name="enable_linked_currency"/>
<field name="enable_linked_currency"/>
<newline/>
<label name="linked_price"/>
<field name="linked_price"/>
<label name="linked_currency"/>
<field name="linked_currency"/>
<label name="linked_unit"/>
<field name="linked_unit"/>
<newline/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/field[@name='amount']" position="after">
<newline/>
<label name="price_type"/>
<field name="price_type"/>
<label name="progress"/>
<field name="progress" widget="progressbar"/>
<newline/>
<label name="premium"/>
<field name="premium"/>
<newline/>
<label name="inherit_tol"/>
<field name="inherit_tol"/>
<newline/>
<label name="tol_min"/>
<field name="tol_min"/>
<label name="tol_min_v"/>
@@ -93,15 +93,11 @@ this repository contains the full copyright notices and license terms. -->
<page string="Pricing dates" col="4" id="pricing_date">
<field name="price_pricing" />
</page>
<page string="Summary" col="4" id="summary">
<field name="price_summary" />
</page>
<page string="Report" col="4" id="report">
<label name="pricing_rule" />
<field name="pricing_rule" />
</page>
</notebook>
</page>
<page string="Summary" col="4" id="summary">
<field name="price_summary" />
</page>
</notebook>
</page>
<page string="Estimated dates" col="4" id="estimated">
<field name="estimated_date" />
</page>
@@ -109,4 +105,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="optional" />
</page>
</xpath>
</data>
</data>

View File

@@ -7,7 +7,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="from_del"/>
<field name="to_del"/>
</xpath>
<xpath expr="//field[@name='unit_price']" position="after">
<field name="premium"/>
</xpath>
</data>
</data>

View File

@@ -2,37 +2,37 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
<newline/>
<label name="quantity_theorical"/>
<field name="quantity_theorical"/>
<label name="finished"/>
<field name="finished"/>
<newline/>
<label name="price_type"/>
<field name="price_type"/>
<label name="enable_linked_currency"/>
<field name="enable_linked_currency"/>
<newline/>
<label name="linked_currency"/>
<field name="linked_currency"/>
<label name="linked_unit"/>
<field name="linked_unit"/>
<newline/>
<label name="linked_price"/>
<field name="linked_price"/>
<label name="premium"/>
<field name="premium"/>
<newline/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/field[@name='amount']" position="after">
<newline/>
<label name="progress"/>
<field name="progress" widget="progressbar"/>
<newline/>
<label name="inherit_tol"/>
<field name="inherit_tol"/>
<newline/>
<xpath expr="/form/notebook/page[@id='general']/field[@name='unit']" position="after">
<newline/>
<label name="quantity_theorical"/>
<field name="quantity_theorical"/>
<label name="finished"/>
<field name="finished"/>
<newline/>
<label name="enable_linked_currency"/>
<field name="enable_linked_currency"/>
<newline/>
<label name="linked_price"/>
<field name="linked_price"/>
<label name="linked_currency"/>
<field name="linked_currency"/>
<label name="linked_unit"/>
<field name="linked_unit"/>
<newline/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/field[@name='amount']" position="after">
<newline/>
<label name="price_type"/>
<field name="price_type"/>
<label name="progress"/>
<field name="progress" widget="progressbar"/>
<newline/>
<label name="premium"/>
<field name="premium"/>
<newline/>
<label name="inherit_tol"/>
<field name="inherit_tol"/>
<newline/>
<label name="tol_min"/>
<field name="tol_min"/>
<label name="tol_min_v"/>
@@ -91,4 +91,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="estimated_date" />
</page>
</xpath>
</data>
</data>

View File

@@ -7,7 +7,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="from_del"/>
<field name="to_del"/>
</xpath>
<xpath expr="//field[@name='unit_price']" position="after">
<field name="premium"/>
</xpath>
</data>
</data>

View File

@@ -137,9 +137,5 @@ this repository contains the full copyright notices and license terms. -->
<newline/>
<field name="shipment_wr" colspan="4" mode="tree" view_ids="purchase_trade.shipment_wr_view_tree"/>
</page>
<page string="Surveyor" col="4" id="surveyor">
<label name="surveyor"/>
<field name="surveyor"/>
</page>
</xpath>
</data>
</data>

View File

@@ -1,32 +0,0 @@
<?xml version="1.0"?>
<form col="4">
<separator id="sale_templates" string="Sale" colspan="4"/>
<label name="sale_report_template"/>
<field name="sale_report_template" colspan="3"/>
<label name="sale_bill_report_template"/>
<field name="sale_bill_report_template" colspan="3"/>
<label name="sale_final_report_template"/>
<field name="sale_final_report_template" colspan="3"/>
<separator id="invoice_templates" string="Invoice" colspan="4"/>
<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"/>
<separator id="payment_templates" string="Payment" colspan="4"/>
<label name="invoice_payment_order_report_template"/>
<field name="invoice_payment_order_report_template" colspan="3"/>
<separator id="purchase_templates" string="Purchase" colspan="4"/>
<label name="purchase_report_template"/>
<field name="purchase_report_template" colspan="3"/>
<separator id="shipment_templates" string="Shipment" colspan="4"/>
<label name="shipment_shipping_report_template"/>
<field name="shipment_shipping_report_template" colspan="3"/>
<label name="shipment_insurance_report_template"/>
<field name="shipment_insurance_report_template" colspan="3"/>
</form>

View File

@@ -12,6 +12,5 @@
<field name="base_amount" sum="1"/>
<field name="rate"/>
<field name="strategy"/>
<field name="mtm_price"/>
<field name="mtm" optional="0" sum="1"/>
</tree>
</tree>

View File

@@ -1,6 +0,0 @@
<form col="4">
<label name="dimension"/>
<field name="dimension"/>
<label name="value"/>
<field name="value"/>
</form>

View File

@@ -1,4 +0,0 @@
<tree editable="1">
<field name="dimension"/>
<field name="value"/>
</tree>

View File

@@ -1,3 +0,0 @@
<form>
<field name="message"/>
</form>

View File

@@ -1,18 +0,0 @@
<form col="4">
<label name="valuation_type"/>
<field name="valuation_type"/>
<newline/>
<label name="purchase_from_date"/>
<field name="purchase_from_date"/>
<label name="purchase_to_date"/>
<field name="purchase_to_date"/>
<label name="purchase_numbers"/>
<field name="purchase_numbers" colspan="3"/>
<label name="sale_from_date"/>
<field name="sale_from_date"/>
<label name="sale_to_date"/>
<field name="sale_to_date"/>
<label name="sale_numbers"/>
<field name="sale_numbers" colspan="3"/>
<field name="analytic_dimensions" colspan="4"/>
</form>

View File

@@ -11,6 +11,5 @@ this repository contains the full copyright notices and license terms. -->
<field name="quantity" symbol="unit"/>
<field name="amount" sum="1"/>
<field name="strategy"/>
<field name="mtm_price"/>
<field name="mtm" optional="0" sum="1"/>
</tree>
</tree>

View File

@@ -11,7 +11,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="r_quantity" symbol="r_unit"/>
<field name="r_amount" sum="1"/>
<field name="r_strategy"/>
<field name="r_mtm_price"/>
<field name="r_mtm" optional="0" sum="1"/>
</tree>

View File

@@ -79,16 +79,9 @@
<label name="weight_date"/>
<field name="weight_date"/>
</group>
<group id="remote_wr" colspan="8" col="4">
<button name="create_remote_weight_reports" string="Create Remote WRs" colspan="4"/>
<label name="remote_weight_report_sent_at"/>
<field name="remote_weight_report_sent_at"/>
<label name="remote_weight_report_keys"/>
<field name="remote_weight_report_keys" colspan="4"/>
</group>
<!-- <group id="buttons" colspan="8">
<button name="import_json" string="Import JSON"/>
<button name="export_json" string="Export JSON"/>
</group> -->
</form>
</form>

View File

@@ -12,7 +12,6 @@
<field name="invoice_net_kg"/>
<field name="gain_loss_kg"/>
<field name="gain_loss_percent"/>
<field name="remote_weight_report_sent_at"/>
<!-- <button name="import_json" tree_invisible="1"/>
<button name="export_json" tree_invisible="1"/> -->
</tree>
</tree>

View File

@@ -1,10 +1,7 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool
from trytond.exceptions import UserError
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime as dt
import datetime
import requests
import logging
logger = logging.getLogger(__name__)
@@ -52,127 +49,21 @@ class WeightReport(ModelSQL, ModelView):
invoice_net_kg = fields.Numeric('Invoice Net (kg)', digits=(16, 2))
gain_loss_kg = fields.Numeric('Gain/Loss (kg)', digits=(16, 2))
gain_loss_percent = fields.Numeric('Gain/Loss (%)', digits=(16, 2))
remote_weight_report_keys = fields.Text('Remote WR Keys', readonly=True)
remote_weight_report_sent_at = fields.Date(
'Remote WR Sent At', readonly=True)
@classmethod
def __setup__(cls):
super().__setup__()
cls._order = [('report_date', 'DESC')]
cls._buttons.update({
'create_remote_weight_reports': {},
})
# cls._buttons.update({
# 'import_json': {},
# 'export_json': {},
# })
def get_rec_name(self, name):
items = [self.lab]
if self.reference:
items.append('[%s]' % self.reference)
return ' '.join(items)
def create_remote_weight_report(self, wr_payload):
response = requests.post(
"http://automation-service:8006/weight-report",
json=wr_payload,
timeout=10
)
response.raise_for_status()
return response.json()
def get_related_shipments(self):
ShipmentWR = Pool().get('shipment.wr')
links = ShipmentWR.search([('wr', '=', self.id)])
return [link.shipment_in for link in links if link.shipment_in]
def get_source_shipment(self):
shipments = self.get_related_shipments()
if not shipments:
raise UserError('No shipment is linked to this weight report.')
unique_shipments = {shipment.id: shipment for shipment in shipments}
if len(unique_shipments) > 1:
raise UserError(
'This weight report is linked to multiple shipments.')
return next(iter(unique_shipments.values()))
def get_remote_weight_report_lots(self, shipment):
lots = []
seen = set()
for move in shipment.incoming_moves or []:
lot = getattr(move, 'lot', None)
if (not lot or lot.lot_type != 'physic'
or lot.id in seen):
continue
seen.add(lot.id)
lots.append(lot)
if not lots:
raise UserError(
'No physical lot was found on the incoming moves.')
return lots
def validate_remote_weight_report_context(self, shipment):
if not shipment.controller:
raise UserError(
'A controller is required before creating remote weight reports.')
if not shipment.returned_id:
raise UserError(
'A returned ID is required before creating remote weight reports.')
if not shipment.agent:
raise UserError(
'A booking agent is required before creating remote weight reports.')
if not shipment.to_location:
raise UserError(
'A destination location is required before creating remote weight reports.')
if not self.bales:
raise UserError(
'The global weight report must define the number of bales.')
if not self.report_date or not self.weight_date:
raise UserError(
'Report date and weight date are required.')
def build_remote_weight_report_payload(self, shipment, lot):
if not lot.lot_chunk_key:
raise UserError(
'Each physical lot must have a chunk key before export.')
factor_net = self.net_landed_kg / self.bales
factor_gross = self.gross_landed_kg / self.bales
lot_ratio = Decimal(lot.lot_qt) / self.bales
return {
"chunk_key": lot.lot_chunk_key,
"gross_weight": float(round(
Decimal(lot.lot_qt) * factor_gross, 5)),
"net_weight": float(round(
Decimal(lot.lot_qt) * factor_net, 5)),
"tare_total": float(round(self.tare_kg * lot_ratio, 5)),
"bags": int(lot.lot_qt),
"surveyor_code": shipment.controller.get_alf(),
"place_key": shipment.to_location.get_places(),
"report_date": int(self.report_date.strftime("%Y%m%d")),
"weight_date": int(self.weight_date.strftime("%Y%m%d")),
"agent": shipment.agent.get_alf(),
"forwarder_ref": shipment.returned_id,
}
@classmethod
@ModelView.button
def create_remote_weight_reports(cls, reports):
to_save = []
for report in reports:
shipment = report.get_source_shipment()
report.validate_remote_weight_report_context(shipment)
lots = report.get_remote_weight_report_lots(shipment)
created = []
for lot in lots:
payload = report.build_remote_weight_report_payload(
shipment, lot)
logger.info("REMOTE_WR_PAYLOAD:%s", payload)
data = report.create_remote_weight_report(payload)
created.append(
f"{lot.rec_name}: {data.get('weight_report_key')}")
report.remote_weight_report_keys = '\n'.join(created)
report.remote_weight_report_sent_at = datetime.date.today()
to_save.append(report)
if to_save:
cls.save(to_save)
# @classmethod
# @ModelView.button_action('weight_report.act_import_json')
# def import_json(cls, reports):
@@ -309,4 +200,4 @@ class WeightReport(ModelSQL, ModelView):
report['gain_loss_percent'] = gain_loss_percent.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
# 7. Création du rapport
return cls.create([report])[0]
return cls.create([report])[0]

View File

@@ -103,12 +103,18 @@
</record> -->
<!-- Model Buttons -->
<record model="ir.model.button" id="weight_report_create_remote_button">
<!-- <record model="ir.model.button" id="weight_report_import_button">
<field name="model">weight.report</field>
<field name="name">create_remote_weight_reports</field>
<field name="string">Create Remote WRs</field>
<field name="name">import_json</field>
<field name="string">Import JSON</field>
</record>
<record model="ir.model.button" id="weight_report_export_button">
<field name="model">weight.report</field>
<field name="name">export_json</field>
<field name="string">Export JSON</field>
</record> -->
<!-- Menu Structure -->
<menuitem
name="Weight Reports"
@@ -130,4 +136,4 @@
sequence="30"
id="menu_gr_weight_reports"/> -->
</data>
</tryton>
</tryton>

View File

@@ -4069,7 +4069,7 @@
<text:s />
<text:span text:style-name="T21">CROP </text:span>
<text:span text:style-name="T21">
<text:placeholder text:placeholder-type="text">&lt;sale.report_crop_name&gt;</text:placeholder>
<text:placeholder text:placeholder-type="text">&lt;sale.crop.name&gt;</text:placeholder>
</text:span>
</text:p>
</table:table-cell>
@@ -4091,19 +4091,10 @@
<text:p text:style-name="P42" />
</table:table-cell>
<table:table-cell table:style-name="Table4.A1" office:value-type="string">
<text:p text:style-name="P31">ABOUT <text:placeholder text:placeholder-type="text">&lt;sale.report_total_quantity&gt;</text:placeholder><text:s /><text:placeholder text:placeholder-type="text">&lt;sale.report_quantity_unit_upper&gt;</text:placeholder><text:s /><text:span text:style-name="T23">(</text:span><text:span text:style-name="T23"><text:placeholder text:placeholder-type="text">&lt;sale.report_qt&gt;</text:placeholder></text:span><text:span text:style-name="T23">) </text:span></text:p>
<text:p text:style-name="P31">ABOUT <text:placeholder text:placeholder-type="text">&lt;sum(line.quantity for line in sale.lines)&gt;</text:placeholder><text:s /><text:placeholder text:placeholder-type="text">&lt;sale.lines[0].unit.rec_name.upper() if sale.lines and sale.lines[0].unit else ''&gt;</text:placeholder><text:s /><text:span text:style-name="T23">(</text:span><text:span text:style-name="T23"><text:placeholder text:placeholder-type="text">&lt;sale.report_qt&gt;</text:placeholder></text:span><text:span text:style-name="T23">) </text:span></text:p>
<text:p text:style-name="P39">
<text:placeholder text:placeholder-type="text">&lt;sale.report_nb_bale&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P39">
<text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in sale.report_quantity_lines.splitlines()&quot;&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P39">
<text:placeholder text:placeholder-type="text">&lt;line&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P39">
<text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P39" />
</table:table-cell>
</table:table-row>
@@ -4119,13 +4110,27 @@
</table:table-cell>
<table:table-cell table:style-name="Table5.A1" office:value-type="string">
<text:p text:style-name="P56">
<text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in sale.report_price_lines.splitlines()&quot;&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P56">
<text:placeholder text:placeholder-type="text">&lt;line&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P56">
<text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder>
<text:placeholder text:placeholder-type="text">&lt;sale.lines[0].linked_currency.rec_name.upper() if sale.lines[0].linked_currency else sale.currency.rec_name.upper()&gt;</text:placeholder>
<text:s />
<text:placeholder text:placeholder-type="text">&lt;sale.lines[0].linked_price if sale.lines[0].linked_price else sale.lines[0].unit_price&gt;</text:placeholder>
<text:span text:style-name="T23">
<text:s />
</text:span>
<text:span text:style-name="T26">PER </text:span>
<text:span text:style-name="T26">
<text:placeholder text:placeholder-type="text">&lt;sale.lines[0].linked_unit.rec_name.upper() if sale.lines[0].linked_unit else sale.lines[0].unit.rec_name.upper()&gt;</text:placeholder>
</text:span>
<text:span text:style-name="T26">
<text:s />
</text:span>
<text:span text:style-name="T23">(</text:span>
<text:span text:style-name="T26">
<text:placeholder text:placeholder-type="text">&lt;sale.report_price&gt;</text:placeholder>
</text:span>
<text:span text:style-name="T23">) </text:span>
<text:span text:style-name="T23">
<text:placeholder text:placeholder-type="text">&lt;sale.lines[0].get_pricing_text&gt;</text:placeholder>
</text:span>
</text:p>
<text:p text:style-name="P37" />
</table:table-cell>
@@ -4194,18 +4199,12 @@
</text:p>
</table:table-cell>
<table:table-cell table:style-name="Table6.A1" office:value-type="string">
<text:p text:style-name="P36">
<text:placeholder text:placeholder-type="text">&lt;for each=&quot;line in sale.report_shipment_periods.splitlines()&quot;&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P36">
<text:s />
<text:span text:style-name="T24">
<text:placeholder text:placeholder-type="text">&lt;line&gt;</text:placeholder>
<text:placeholder text:placeholder-type="text">&lt;sale.lines[0].del_period.description if sale.lines[0].del_period else ''&gt;</text:placeholder>
</text:span>
</text:p>
<text:p text:style-name="P36">
<text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder>
</text:p>
<text:p text:style-name="P34" />
</table:table-cell>
</table:table-row>
@@ -4390,4 +4389,4 @@
</text:p>
</office:text>
</office:body>
</office:document>
</office:document>

View File

@@ -1763,8 +1763,8 @@
<text:p text:style-name="P25" loext:marker-style-name="T39"/>
<text:p text:style-name="P24" loext:marker-style-name="T39"/>
<text:p text:style-name="P24" loext:marker-style-name="T39"/>
<text:p text:style-name="P39" loext:marker-style-name="T26"><text:span text:style-name="T26"><text:s text:c="2"/></text:span><text:span text:style-name="T42"><text:placeholder text:placeholder-type="text">&lt;sale.report_product_name or &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P30" loext:marker-style-name="T26"><text:span text:style-name="T42"><text:placeholder text:placeholder-type="text">&lt;sale.report_product_description or &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P39" loext:marker-style-name="T26"><text:span text:style-name="T26"><text:s text:c="2"/></text:span><text:span text:style-name="T42"><text:placeholder text:placeholder-type="text">&lt;sale.lines[0].product.name if sale.lines and sale.lines[0].product else &apos;&apos;&gt;</text:placeholder></text:span></text:p>
<text:p text:style-name="P30" loext:marker-style-name="T26"/>
<text:p text:style-name="P27" loext:marker-style-name="T42"><text:span text:style-name="T42"><text:s/></text:span></text:p>
</table:table-cell>
<table:table-cell table:style-name="Tableau1.B2" office:value-type="string">
@@ -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.report_delivery_period_description or &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.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: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"/>
@@ -1803,13 +1803,6 @@
<text:p text:style-name="P2"/>
<text:p text:style-name="P7">PAYMENT TERMS:<text:tab/><text:placeholder text:placeholder-type="text">&lt;sale.payment_term.description if sale.payment_term else &apos;&apos;&gt;</text:placeholder></text:p>
<text:p text:style-name="P2"/>
<text:p text:style-name="P8">BANK: UBS SWITZERLAND AG</text:p>
<text:p text:style-name="P8">Case Postale</text:p>
<text:p text:style-name="P8">CH-1211 Geneve 2</text:p>
<text:p text:style-name="P2"/>
<text:p text:style-name="P8">IBAN : CH36 0021 5215 2487 7461 D</text:p>
<text:p text:style-name="P8">SwifT Code: UBSWCHZH80A</text:p>
<text:p text:style-name="P2"/>
<text:p text:style-name="P2"/>
<text:p text:style-name="P2">DELIVERY IS SUBJECT TO THE DELIVERY BY OUR SUPPLIER.</text:p>
<text:p text:style-name="P2"/>
@@ -1821,4 +1814,4 @@
<text:p text:style-name="Standard"><text:placeholder text:placeholder-type="text">&lt;/for&gt;</text:placeholder></text:p>
</office:text>
</office:body>
</office:document>
</office:document>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import (
financing,
market,
valuation,
document,
template_execution,
counterparty,
fx,
operational,
facility,
constraint_type,
)
def register():
Pool.register(
financing.FinancingType,
financing.OperationalStatus,
market.MarketIndex,
market.MarketIndexRate,
market.IndexTerm,
market.InterestFormula,
valuation.ValuationMethod,
valuation.HaircutFormula,
valuation.CollateralType,
document.EvidenceType,
template_execution.TemplateSegment,
template_execution.ExecutionTemplate,
template_execution.ExecutionTemplateLine,
counterparty.ReceivableCategory,
counterparty.PaymentConditionType,
fx.FxFeeder,
operational.BlockingReason,
operational.ChargeType,
facility.FacilityStatus,
facility.Facility,
facility.FacilityCurrency,
facility.FacilityCovenant,
facility.FacilityLimit,
facility.FacilityLimitHaircut,
facility.FacilityLimitCurrency,
facility.FacilityLimitCost,
facility.FacilityLimitCostVariation,
facility.FacilityLimitOpStatus,
facility.FacilityLimitBankAccount,
facility.FacilityCap,
facility.FacilityCapHaircut,
facility.FacilityConstraint,
constraint_type.ConstraintType,
module='trade_finance', type_='model')
Pool.register(
fx.PriceCalendar,
module='price', type_='model')

View File

@@ -0,0 +1,54 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
from trytond.transaction import Transaction
__all__ = ['ConstraintType']
class ConstraintType(ModelSQL, ModelView):
'Constraint Type'
__name__ = 'trade_finance.constraint_type'
_rec_name = 'name'
name = fields.Char('Type', required=True, size=50)
view_name = fields.Selection([], 'View')
value_field = fields.Selection(
'get_column_selection', 'Value Field',
selection_change_with=['view_name'])
label_field = fields.Selection(
'get_column_selection', 'Label Field',
selection_change_with=['view_name'])
@fields.depends('view_name')
def get_column_selection(self):
if not self.view_name:
return [('', '')]
cursor = Transaction().connection.cursor()
cursor.execute(
"SELECT column_name FROM information_schema.columns "
"WHERE table_schema = 'public' AND table_name = %s "
"ORDER BY ordinal_position",
(self.view_name,)
)
return [('', '')] + [(r[0], r[0]) for r in cursor.fetchall()]
@fields.depends('view_name')
def on_change_view_name(self):
self.value_field = None
self.label_field = None
@classmethod
def fields_get(cls, fields_names=None, level=0):
res = super().fields_get(fields_names, level)
if 'view_name' in res:
cursor = Transaction().connection.cursor()
cursor.execute(
"SELECT table_name FROM information_schema.views "
"WHERE table_schema = 'public' AND table_name LIKE 'vw_tf_const_%' "
"ORDER BY table_name"
)
res['view_name']['selection'] = (
[('', '')] + [(r[0], r[0]) for r in cursor.fetchall()])
return res

View File

@@ -0,0 +1,66 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- ================================================================ -->
<!-- CONSTRAINT TYPE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="constraint_type_view_tree">
<field name="model">trade_finance.constraint_type</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">constraint_type_tree</field>
</record>
<record model="ir.ui.view" id="constraint_type_view_form">
<field name="model">trade_finance.constraint_type</field>
<field name="type">form</field>
<field name="name">constraint_type_form</field>
</record>
<record model="ir.action.act_window" id="act_constraint_type">
<field name="name">Constraint Types</field>
<field name="res_model">trade_finance.constraint_type</field>
</record>
<record model="ir.action.act_window.view" id="act_constraint_type_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="constraint_type_view_tree"/>
<field name="act_window" ref="act_constraint_type"/>
</record>
<record model="ir.action.act_window.view" id="act_constraint_type_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="constraint_type_view_form"/>
<field name="act_window" ref="act_constraint_type"/>
</record>
<menuitem
name="Constraint Types"
sequence="10"
id="menu_constraint_type"
parent="menu_trade_finance_configuration"
action="act_constraint_type"/>
<record model="ir.model.access" id="access_constraint_type">
<field name="model">trade_finance.constraint_type</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_constraint_type_user">
<field name="model">trade_finance.constraint_type</field>
<field name="group" ref="group_trade_finance_user"/>
<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.model.access" id="access_constraint_type_admin">
<field name="model">trade_finance.constraint_type</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,39 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
__all__ = ['ReceivableCategory', 'PaymentConditionType']
class ReceivableCategory(ModelSQL, ModelView):
'Receivable Category'
__name__ = 'trade_finance.receivable_category'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
description = fields.Text('Description')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class PaymentConditionType(ModelSQL, ModelView):
'Payment Condition Type'
__name__ = 'trade_finance.payment_condition_type'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
remaining_risk_pct = fields.Numeric(
'Remaining Risk (%)', digits=(16, 2),
help='Percentage of residual credit risk retained by the company '
'under this payment condition')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True

View File

@@ -0,0 +1,21 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
__all__ = ['EvidenceType']
class EvidenceType(ModelSQL, ModelView):
'Evidence Type'
__name__ = 'trade_finance.evidence_type'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
description = fields.Text('Description')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True

View File

@@ -0,0 +1,497 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
from trytond.model import sequence_ordered
from trytond.pool import Pool
from trytond.pyson import Eval, Bool
from trytond.exceptions import UserError
__all__ = [
'FacilityStatus',
'Facility',
'FacilityCurrency',
'FacilityCovenant',
'FacilityLimit',
'FacilityLimitHaircut',
'FacilityLimitCurrency',
'FacilityLimitCost',
'FacilityLimitCostVariation',
'FacilityLimitOpStatus',
'FacilityLimitBankAccount',
'FacilityCap',
'FacilityCapHaircut',
'FacilityConstraint',
]
# ---------------------------------------------------------------------------
# Shared Selection lists
# ---------------------------------------------------------------------------
ATTRIBUTE_TYPES = [
('commodity', 'Commodity'),
('article', 'Article'),
('origin', 'Origin'),
('destination', 'Destination'),
('loading_place', 'Loading Place'),
('supplier', 'Supplier'),
('customer', 'Customer'),
('payment_condition_purchase', 'Payment Condition (Purchase)'),
('payment_condition_sale', 'Payment Condition (Sale)'),
('purchase_currency', 'Purchase Currency'),
('warehouse', 'Warehouse'),
('incoterm', 'Incoterm'),
('counterparty_rating', 'Counterparty Rating'),
('receivable_category', 'Receivable Category'),
]
# ---------------------------------------------------------------------------
# Facility Status (configurable reference)
# ---------------------------------------------------------------------------
class FacilityStatus(ModelSQL, ModelView):
'Facility Status'
__name__ = 'trade_finance.facility_status'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
# ---------------------------------------------------------------------------
# Facility Header
# ---------------------------------------------------------------------------
class Facility(ModelSQL, ModelView):
'TF Facility'
__name__ = 'trade_finance.facility'
_rec_name = 'name'
name = fields.Char('Name', required=True)
tfe = fields.Many2One('bank', 'Trade Finance Entity', required=True,
ondelete='RESTRICT',
help='Bank or fund providing this facility')
description = fields.Text('Description')
status = fields.Many2One('trade_finance.facility_status', 'Status',
ondelete='RESTRICT')
commitment_status = fields.Selection([
('uncommitted', 'Uncommitted'),
('committed', 'Committed'),
], 'Commitment Status', required=True)
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
currency = fields.Many2One('currency.currency', 'Facility Currency',
required=True, ondelete='RESTRICT')
fx_feeder = fields.Many2One('trade_finance.fx_feeder', 'FX Rate Feeder',
ondelete='RESTRICT')
fx_haircut_pct = fields.Numeric('FX Haircut (%)', digits=(16, 2))
is_tpa = fields.Boolean('TPA',
help='Third Party Account — broker managed structure')
broker = fields.Many2One('party.party', 'Broker',
states={'invisible': ~Bool(Eval('is_tpa'))},
depends=['is_tpa'], ondelete='RESTRICT')
broker_account = fields.Char('Broker Account',
states={'invisible': ~Bool(Eval('is_tpa'))},
depends=['is_tpa'])
limits = fields.One2Many('trade_finance.facility_limit', 'facility',
'Limits')
currencies = fields.One2Many('trade_finance.facility_currency', 'facility',
'Accepted Currencies')
caps = fields.One2Many('trade_finance.facility_cap', 'facility', 'Caps')
covenants = fields.One2Many('trade_finance.facility_covenant', 'facility',
'Covenants')
constraints = fields.One2Many('trade_finance.facility_constraint',
'facility', 'Facility Constraints',
domain=[('limit', '=', None)])
@classmethod
def default_status(cls):
pool = Pool()
FacilityStatus = pool.get('trade_finance.facility_status')
statuses = FacilityStatus.search([('code', '=', 'DRAFT')], limit=1)
if statuses:
return statuses[0].id
return None
@staticmethod
def default_commitment_status():
return 'uncommitted'
@staticmethod
def default_is_tpa():
return False
# ---------------------------------------------------------------------------
# Facility Accepted Currency
# ---------------------------------------------------------------------------
class FacilityCurrency(ModelSQL, ModelView):
'Facility Currency'
__name__ = 'trade_finance.facility_currency'
facility = fields.Many2One('trade_finance.facility', 'Facility',
required=True, ondelete='CASCADE')
currency = fields.Many2One('currency.currency', 'Currency',
required=True, ondelete='RESTRICT')
fx_haircut_formula = fields.Many2One('trade_finance.haircut_formula',
'FX Haircut Formula', ondelete='RESTRICT')
fx_feeder = fields.Many2One('trade_finance.fx_feeder', 'FX Rate Feeder',
ondelete='RESTRICT')
valuation_method = fields.Many2One('trade_finance.valuation_method',
'Valuation Method', ondelete='RESTRICT')
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
# ---------------------------------------------------------------------------
# Covenant
# ---------------------------------------------------------------------------
class FacilityCovenant(ModelSQL, ModelView):
'Facility Covenant'
__name__ = 'trade_finance.facility_covenant'
_rec_name = 'name'
facility = fields.Many2One('trade_finance.facility', 'Facility',
required=True, ondelete='CASCADE')
name = fields.Char('Covenant', required=True)
ratio_type = fields.Selection([
('leverage_ratio', 'Leverage Ratio'),
('dso', 'DSO'),
('interest_coverage', 'Interest Coverage Ratio'),
('current_ratio', 'Current Ratio'),
('other', 'Other'),
], 'Ratio Type', required=True)
threshold = fields.Numeric('Threshold', digits=(16, 4))
currency = fields.Many2One('currency.currency', 'Currency',
ondelete='RESTRICT')
notes = fields.Text('Notes')
# ---------------------------------------------------------------------------
# Facility Limit (Global Limit + Sub-Limits in one model)
# ---------------------------------------------------------------------------
class FacilityLimit(ModelSQL, ModelView):
'Facility Limit'
__name__ = 'trade_finance.facility_limit'
_rec_name = 'name'
facility = fields.Many2One('trade_finance.facility', 'Facility',
required=True, ondelete='CASCADE',
states={'readonly': Bool(Eval('parent'))},
depends=['parent'])
parent = fields.Many2One('trade_finance.facility_limit', 'Parent Limit',
ondelete='RESTRICT',
domain=[('facility', '=', Eval('facility'))],
depends=['facility'],
help='Leave empty for Global Limit (root node)')
children = fields.One2Many('trade_finance.facility_limit', 'parent',
'Sub-Limits')
name = fields.Char('Name', required=True)
alternative_name = fields.Char('Limit Alternative Name')
financing_type = fields.Many2One('trade_finance.financing_type',
'Financing Type', ondelete='RESTRICT')
amount = fields.Numeric('Amount', digits=(16, 2), required=True)
tenor = fields.Integer('Tenor (days)',
help='Maximum duration of financing from drawdown to repayment')
sequence = fields.Integer('Sequence')
_order = [('sequence', 'ASC'), ('id', 'ASC')]
haircuts = fields.One2Many('trade_finance.facility_limit_haircut', 'limit',
'Haircuts')
currencies = fields.One2Many('trade_finance.facility_limit_currency',
'limit', 'Accepted Currencies')
costs = fields.One2Many('trade_finance.facility_limit_cost', 'limit',
'Costs')
op_statuses = fields.One2Many('trade_finance.facility_limit_op_status',
'limit', 'Expected Operational Statuses')
bank_accounts = fields.One2Many('trade_finance.facility_limit_bank_account',
'limit', 'Bank Accounts')
constraints = fields.One2Many('trade_finance.facility_constraint', 'limit',
'Limit Constraints',
domain=[('facility', '=', None)])
@staticmethod
def default_sequence():
return 10
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for values in vlist:
if values.get('parent') and not values.get('facility'):
parent = cls(values['parent'])
values['facility'] = parent.facility.id
return super().create(vlist)
@classmethod
def validate(cls, limits):
super().validate(limits)
for limit in limits:
limit.check_single_root()
limit.check_amount_vs_parent()
limit.check_children_amounts()
def check_single_root(self):
if self.parent is None:
roots = self.__class__.search([
('facility', '=', self.facility.id),
('parent', '=', None),
('id', '!=', self.id),
])
if roots:
raise UserError(
f"Facility '{self.facility.name}' already has a Global "
f"Limit ('{roots[0].name}'). Only one root limit is "
f"allowed per facility.")
def check_amount_vs_parent(self):
if self.parent and self.amount > self.parent.amount:
raise UserError(
f"Sub-limit '{self.name}' amount ({self.amount}) "
f"cannot exceed parent limit '{self.parent.name}' "
f"amount ({self.parent.amount}).")
def check_children_amounts(self):
for child in self.children:
if child.amount > self.amount:
raise UserError(
f"Sub-limit '{child.name}' amount ({child.amount}) "
f"cannot exceed parent limit '{self.name}' "
f"amount ({self.amount}).")
# ---------------------------------------------------------------------------
# Limit Haircut
# ---------------------------------------------------------------------------
class FacilityLimitHaircut(ModelSQL, ModelView):
'Facility Limit Haircut'
__name__ = 'trade_finance.facility_limit_haircut'
limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
required=True, ondelete='CASCADE')
attribute = fields.Selection(ATTRIBUTE_TYPES, 'Attribute Type',
required=True)
value = fields.Char('Value',
help='The specific attribute value this haircut applies to '
'(e.g. "Brazil", "Coffee", "USD")')
haircut_pct = fields.Numeric('Haircut (%)', digits=(16, 2), required=True)
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
# ---------------------------------------------------------------------------
# Limit Accepted Currency
# ---------------------------------------------------------------------------
class FacilityLimitCurrency(ModelSQL, ModelView):
'Facility Limit Currency'
__name__ = 'trade_finance.facility_limit_currency'
limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
required=True, ondelete='CASCADE')
currency = fields.Many2One('currency.currency', 'Currency',
required=True, ondelete='RESTRICT')
haircut_pct = fields.Numeric('FX Haircut (%)', digits=(16, 2))
fx_feeder = fields.Many2One('trade_finance.fx_feeder', 'FX Rate Feeder',
ondelete='RESTRICT')
valuation_method = fields.Many2One('trade_finance.valuation_method',
'Valuation Method', ondelete='RESTRICT')
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
# ---------------------------------------------------------------------------
# Limit Cost
# ---------------------------------------------------------------------------
class FacilityLimitCost(ModelSQL, ModelView):
'Facility Limit Cost'
__name__ = 'trade_finance.facility_limit_cost'
limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
required=True, ondelete='CASCADE')
cost_type = fields.Selection([
('interest', 'Interest-Based'),
('flat', 'Flat / Minimal Amount'),
], 'Cost Type', required=True)
# Interest-based fields
spread = fields.Numeric('Spread (%)', digits=(16, 4),
states={'invisible': Eval('cost_type') != 'interest'},
depends=['cost_type'])
index = fields.Many2One('trade_finance.market_index', 'Market Index',
ondelete='RESTRICT',
states={'invisible': Eval('cost_type') != 'interest'},
depends=['cost_type'])
index_term = fields.Many2One('trade_finance.index_term', 'Index Term',
ondelete='RESTRICT',
states={'invisible': Eval('cost_type') != 'interest'},
depends=['cost_type'])
interest_formula = fields.Many2One('trade_finance.interest_formula',
'Interest Formula', ondelete='RESTRICT',
states={'invisible': Eval('cost_type') != 'interest'},
depends=['cost_type'])
# Flat fields
flat_amount = fields.Numeric('Flat Amount', digits=(16, 2),
states={'invisible': Eval('cost_type') != 'flat'},
depends=['cost_type'])
flat_currency = fields.Many2One('currency.currency', 'Currency',
ondelete='RESTRICT',
states={'invisible': Eval('cost_type') != 'flat'},
depends=['cost_type'])
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
variations = fields.One2Many('trade_finance.facility_limit_cost_variation',
'cost', 'Cost Variations')
# ---------------------------------------------------------------------------
# Limit Cost Variation
# ---------------------------------------------------------------------------
class FacilityLimitCostVariation(ModelSQL, ModelView):
'Facility Limit Cost Variation'
__name__ = 'trade_finance.facility_limit_cost_variation'
cost = fields.Many2One('trade_finance.facility_limit_cost', 'Cost',
required=True, ondelete='CASCADE')
attribute = fields.Selection(ATTRIBUTE_TYPES, 'Attribute Type',
required=True)
value = fields.Char('Value')
variation_type = fields.Selection([
('pct', 'Percentage (+/-)'),
('flat', 'Flat Amount (+/-)'),
], 'Variation Type', required=True)
variation_value = fields.Numeric('Variation Value', digits=(16, 4),
required=True)
variation_currency = fields.Many2One('currency.currency', 'Currency',
ondelete='RESTRICT',
states={'invisible': Eval('variation_type') != 'flat'},
depends=['variation_type'])
# ---------------------------------------------------------------------------
# Limit Expected Operational Status
# ---------------------------------------------------------------------------
class FacilityLimitOpStatus(ModelSQL, ModelView):
'Facility Limit Operational Status'
__name__ = 'trade_finance.facility_limit_op_status'
limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
required=True, ondelete='CASCADE')
operational_status = fields.Many2One('trade_finance.operational_status',
'Operational Status', required=True, ondelete='RESTRICT')
evidence_type = fields.Many2One('trade_finance.evidence_type',
'Required Evidence', ondelete='RESTRICT',
help='Evidence required when this operational status is reached')
# ---------------------------------------------------------------------------
# Limit Bank Account
# ---------------------------------------------------------------------------
class FacilityLimitBankAccount(ModelSQL, ModelView):
'Facility Limit Bank Account'
__name__ = 'trade_finance.facility_limit_bank_account'
limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
required=True, ondelete='CASCADE')
bank_account = fields.Many2One('bank.account', 'Bank Account',
required=True, ondelete='RESTRICT')
currency = fields.Many2One('currency.currency', 'Currency',
required=True, ondelete='RESTRICT')
is_default = fields.Boolean('Default')
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
@staticmethod
def default_is_default():
return False
# ---------------------------------------------------------------------------
# Cap
# ---------------------------------------------------------------------------
class FacilityCap(ModelSQL, ModelView):
'Facility Cap'
__name__ = 'trade_finance.facility_cap'
_rec_name = 'name'
facility = fields.Many2One('trade_finance.facility', 'Facility',
required=True, ondelete='CASCADE')
name = fields.Char('Name', required=True)
amount = fields.Numeric('Cap Amount', digits=(16, 2), required=True)
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
haircuts = fields.One2Many('trade_finance.facility_cap_haircut', 'cap',
'Haircuts')
constraints = fields.One2Many('trade_finance.facility_constraint', 'cap',
'Cap Constraints')
# ---------------------------------------------------------------------------
# Cap Haircut
# ---------------------------------------------------------------------------
class FacilityCapHaircut(ModelSQL, ModelView):
'Facility Cap Haircut'
__name__ = 'trade_finance.facility_cap_haircut'
cap = fields.Many2One('trade_finance.facility_cap', 'Cap',
required=True, ondelete='CASCADE')
attribute = fields.Selection(ATTRIBUTE_TYPES, 'Attribute Type',
required=True)
value = fields.Char('Value')
haircut_pct = fields.Numeric('Haircut (%)', digits=(16, 2), required=True)
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
# ---------------------------------------------------------------------------
# Constraint (shared by Facility, Limit, Cap)
# ---------------------------------------------------------------------------
class FacilityConstraint(ModelSQL, ModelView):
'Facility Constraint'
__name__ = 'trade_finance.facility_constraint'
facility = fields.Many2One('trade_finance.facility', 'Facility',
ondelete='CASCADE')
limit = fields.Many2One('trade_finance.facility_limit', 'Limit',
ondelete='CASCADE')
cap = fields.Many2One('trade_finance.facility_cap', 'Cap',
ondelete='CASCADE')
constraint_type = fields.Many2One('trade_finance.constraint_type',
'Constraint Type', required=True, ondelete='RESTRICT')
is_exclusion = fields.Boolean('Exclusion',
help='Checked = Exclusion constraint, unchecked = Inclusion constraint')
date_from = fields.Date('Valid From')
date_to = fields.Date('Valid To')
@staticmethod
def default_is_exclusion():
return False

View File

@@ -0,0 +1,447 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- ================================================================ -->
<!-- ACCESS GROUP -->
<!-- ================================================================ -->
<record model="res.group" id="group_trade_finance_user">
<field name="name">Trade Finance</field>
</record>
<record model="res.user-res.group"
id="user_admin_group_trade_finance_user">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_trade_finance_user"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY STATUS — Reference Data -->
<!-- ================================================================ -->
<record model="trade_finance.facility_status" id="facility_status_draft">
<field name="code">DRAFT</field>
<field name="name">Draft</field>
<field name="active" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY STATUS -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_status_view_tree">
<field name="model">trade_finance.facility_status</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_status_tree</field>
</record>
<record model="ir.ui.view" id="facility_status_view_form">
<field name="model">trade_finance.facility_status</field>
<field name="type">form</field>
<field name="name">facility_status_form</field>
</record>
<record model="ir.action.act_window" id="act_facility_status">
<field name="name">Facility Statuses</field>
<field name="res_model">trade_finance.facility_status</field>
</record>
<record model="ir.action.act_window.view" id="act_facility_status_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="facility_status_view_tree"/>
<field name="act_window" ref="act_facility_status"/>
</record>
<record model="ir.action.act_window.view" id="act_facility_status_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="facility_status_view_form"/>
<field name="act_window" ref="act_facility_status"/>
</record>
<menuitem
name="Facility Statuses"
sequence="5"
id="menu_facility_status"
parent="menu_trade_finance_configuration"
action="act_facility_status"/>
<record model="ir.model.access" id="access_facility_status">
<field name="model">trade_finance.facility_status</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_status_user">
<field name="model">trade_finance.facility_status</field>
<field name="group" ref="group_trade_finance_user"/>
<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.model.access" id="access_facility_status_admin">
<field name="model">trade_finance.facility_status</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_view_tree">
<field name="model">trade_finance.facility</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_tree</field>
</record>
<record model="ir.ui.view" id="facility_view_form">
<field name="model">trade_finance.facility</field>
<field name="type">form</field>
<field name="name">facility_form</field>
</record>
<record model="ir.action.act_window" id="act_facility">
<field name="name">TF Facilities</field>
<field name="res_model">trade_finance.facility</field>
</record>
<record model="ir.action.act_window.view" id="act_facility_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="facility_view_tree"/>
<field name="act_window" ref="act_facility"/>
</record>
<record model="ir.action.act_window.view" id="act_facility_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="facility_view_form"/>
<field name="act_window" ref="act_facility"/>
</record>
<menuitem
name="Facilities"
sequence="10"
id="menu_facility"
parent="menu_trade_finance"
action="act_facility"/>
<record model="ir.model.access" id="access_facility">
<field name="model">trade_finance.facility</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_user">
<field name="model">trade_finance.facility</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_admin">
<field name="model">trade_finance.facility</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY CURRENCY -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_currency_view_tree">
<field name="model">trade_finance.facility_currency</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_currency_tree</field>
</record>
<record model="ir.model.access" id="access_facility_currency">
<field name="model">trade_finance.facility_currency</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_currency_user">
<field name="model">trade_finance.facility_currency</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY COVENANT -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_covenant_view_tree">
<field name="model">trade_finance.facility_covenant</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_covenant_tree</field>
</record>
<record model="ir.model.access" id="access_facility_covenant">
<field name="model">trade_finance.facility_covenant</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_covenant_user">
<field name="model">trade_finance.facility_covenant</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY LIMIT -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_limit_view_tree">
<field name="model">trade_finance.facility_limit</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_tree</field>
</record>
<record model="ir.ui.view" id="facility_limit_view_form">
<field name="model">trade_finance.facility_limit</field>
<field name="type">form</field>
<field name="name">facility_limit_form</field>
</record>
<record model="ir.model.access" id="access_facility_limit">
<field name="model">trade_finance.facility_limit</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_user">
<field name="model">trade_finance.facility_limit</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY LIMIT CHILD TABLES -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_limit_haircut_view_tree">
<field name="model">trade_finance.facility_limit_haircut</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_haircut_tree</field>
</record>
<record model="ir.model.access" id="access_facility_limit_haircut">
<field name="model">trade_finance.facility_limit_haircut</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_haircut_user">
<field name="model">trade_finance.facility_limit_haircut</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="facility_limit_currency_view_tree">
<field name="model">trade_finance.facility_limit_currency</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_currency_tree</field>
</record>
<record model="ir.model.access" id="access_facility_limit_currency">
<field name="model">trade_finance.facility_limit_currency</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_currency_user">
<field name="model">trade_finance.facility_limit_currency</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="facility_limit_cost_view_tree">
<field name="model">trade_finance.facility_limit_cost</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_cost_tree</field>
</record>
<record model="ir.model.access" id="access_facility_limit_cost">
<field name="model">trade_finance.facility_limit_cost</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_cost_user">
<field name="model">trade_finance.facility_limit_cost</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="facility_limit_cost_variation_view_tree">
<field name="model">trade_finance.facility_limit_cost_variation</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_cost_variation_tree</field>
</record>
<record model="ir.model.access" id="access_facility_limit_cost_variation">
<field name="model">trade_finance.facility_limit_cost_variation</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_cost_variation_user">
<field name="model">trade_finance.facility_limit_cost_variation</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="facility_limit_op_status_view_tree">
<field name="model">trade_finance.facility_limit_op_status</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_op_status_tree</field>
</record>
<record model="ir.model.access" id="access_facility_limit_op_status">
<field name="model">trade_finance.facility_limit_op_status</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_op_status_user">
<field name="model">trade_finance.facility_limit_op_status</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="facility_limit_bank_account_view_tree">
<field name="model">trade_finance.facility_limit_bank_account</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_limit_bank_account_tree</field>
</record>
<record model="ir.model.access" id="access_facility_limit_bank_account">
<field name="model">trade_finance.facility_limit_bank_account</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_limit_bank_account_user">
<field name="model">trade_finance.facility_limit_bank_account</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY CAP -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_cap_view_tree">
<field name="model">trade_finance.facility_cap</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_cap_tree</field>
</record>
<record model="ir.model.access" id="access_facility_cap">
<field name="model">trade_finance.facility_cap</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_cap_user">
<field name="model">trade_finance.facility_cap</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="facility_cap_haircut_view_tree">
<field name="model">trade_finance.facility_cap_haircut</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_cap_haircut_tree</field>
</record>
<record model="ir.model.access" id="access_facility_cap_haircut">
<field name="model">trade_finance.facility_cap_haircut</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_cap_haircut_user">
<field name="model">trade_finance.facility_cap_haircut</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FACILITY CONSTRAINT -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="facility_constraint_view_tree">
<field name="model">trade_finance.facility_constraint</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">facility_constraint_tree</field>
</record>
<record model="ir.ui.view" id="facility_constraint_view_form">
<field name="model">trade_finance.facility_constraint</field>
<field name="type">form</field>
<field name="name">facility_constraint_form</field>
</record>
<record model="ir.model.access" id="access_facility_constraint">
<field name="model">trade_finance.facility_constraint</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_facility_constraint_user">
<field name="model">trade_finance.facility_constraint</field>
<field name="group" ref="group_trade_finance_user"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,47 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
__all__ = ['FinancingType', 'OperationalStatus']
class FinancingType(ModelSQL, ModelView):
'Financing Type'
__name__ = 'trade_finance.financing_type'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
sequence = fields.Integer('Sequence')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
@staticmethod
def default_sequence():
return 10
class OperationalStatus(ModelSQL, ModelView):
'Operational Status'
__name__ = 'trade_finance.operational_status'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
financing_type = fields.Many2One(
'trade_finance.financing_type', 'Default Financing Type',
ondelete='RESTRICT')
sequence = fields.Integer('Sequence')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
@staticmethod
def default_sequence():
return 10

View File

@@ -0,0 +1,37 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
__all__ = ['FxFeeder', 'PriceCalendar']
class FxFeeder(ModelSQL, ModelView):
'FX Rate Feeder'
__name__ = 'trade_finance.fx_feeder'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
source_description = fields.Text(
'Source Description',
help='Description of the FX rate source / provider')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class PriceCalendar(metaclass=PoolMeta):
__name__ = 'price.calendar'
purpose = fields.Selection([
(None, ''),
('banking', 'Banking'),
('market', 'Market'),
('factoring', 'Factoring'),
], 'Purpose',
help='Use of this calendar: Banking days, Market trading days, '
'or Factoring program calendar')

View File

@@ -0,0 +1,64 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
__all__ = ['MarketIndex', 'MarketIndexRate', 'IndexTerm', 'InterestFormula']
class MarketIndex(ModelSQL, ModelView):
'Market Index'
__name__ = 'trade_finance.market_index'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
rates = fields.One2Many(
'trade_finance.market_index_rate', 'index', 'Rates')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class MarketIndexRate(ModelSQL, ModelView):
'Market Index Rate'
__name__ = 'trade_finance.market_index_rate'
index = fields.Many2One(
'trade_finance.market_index', 'Index',
required=True, ondelete='CASCADE')
rate_date = fields.Date('Date', required=True)
rate = fields.Numeric('Rate (%)', digits=(16, 6), required=True)
class IndexTerm(ModelSQL, ModelView):
'Index Term'
__name__ = 'trade_finance.index_term'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
days = fields.Integer('Days', required=True,
help='Approximate number of days for this term (O/N=1, 1W=7, 1M=30, '
'3M=90, 6M=180, 1Y=365)')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class InterestFormula(ModelSQL, ModelView):
'Interest Calculation Formula'
__name__ = 'trade_finance.interest_formula'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True

View File

@@ -0,0 +1,35 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
__all__ = ['BlockingReason', 'ChargeType']
class BlockingReason(ModelSQL, ModelView):
'Blocking Reason'
__name__ = 'trade_finance.blocking_reason'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class ChargeType(ModelSQL, ModelView):
'Charge Type'
__name__ = 'trade_finance.charge_type'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
description = fields.Text('Description')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True

View File

@@ -0,0 +1,947 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- ================================================================ -->
<!-- ACCESS GROUPS -->
<!-- ================================================================ -->
<record model="res.group" id="group_trade_finance_admin">
<field name="name">Trade Finance Administration</field>
</record>
<record model="res.user-res.group"
id="user_admin_group_trade_finance_admin">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_trade_finance_admin"/>
</record>
<!-- ================================================================ -->
<!-- TOP-LEVEL MENU -->
<!-- ================================================================ -->
<menuitem
name="Trade Finance"
sequence="70"
id="menu_trade_finance"/>
<menuitem
name="Configuration"
sequence="99"
id="menu_trade_finance_configuration"
parent="menu_trade_finance"/>
<menuitem
name="Market Data"
sequence="20"
id="menu_trade_finance_market_data"
parent="menu_trade_finance"/>
<!-- ================================================================ -->
<!-- FINANCING TYPE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="financing_type_view_tree">
<field name="model">trade_finance.financing_type</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">financing_type_tree</field>
</record>
<record model="ir.ui.view" id="financing_type_view_form">
<field name="model">trade_finance.financing_type</field>
<field name="type">form</field>
<field name="name">financing_type_form</field>
</record>
<record model="ir.action.act_window" id="act_financing_type">
<field name="name">Financing Types</field>
<field name="res_model">trade_finance.financing_type</field>
</record>
<record model="ir.action.act_window.view" id="act_financing_type_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="financing_type_view_tree"/>
<field name="act_window" ref="act_financing_type"/>
</record>
<record model="ir.action.act_window.view" id="act_financing_type_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="financing_type_view_form"/>
<field name="act_window" ref="act_financing_type"/>
</record>
<menuitem
name="Financing Types"
sequence="10"
id="menu_financing_type"
parent="menu_trade_finance_configuration"
action="act_financing_type"/>
<record model="ir.model.access" id="access_financing_type">
<field name="model">trade_finance.financing_type</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_financing_type_admin">
<field name="model">trade_finance.financing_type</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- OPERATIONAL STATUS -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="operational_status_view_tree">
<field name="model">trade_finance.operational_status</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">operational_status_tree</field>
</record>
<record model="ir.ui.view" id="operational_status_view_form">
<field name="model">trade_finance.operational_status</field>
<field name="type">form</field>
<field name="name">operational_status_form</field>
</record>
<record model="ir.action.act_window" id="act_operational_status">
<field name="name">Operational Statuses</field>
<field name="res_model">trade_finance.operational_status</field>
</record>
<record model="ir.action.act_window.view" id="act_operational_status_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="operational_status_view_tree"/>
<field name="act_window" ref="act_operational_status"/>
</record>
<record model="ir.action.act_window.view" id="act_operational_status_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="operational_status_view_form"/>
<field name="act_window" ref="act_operational_status"/>
</record>
<menuitem
name="Operational Statuses"
sequence="20"
id="menu_operational_status"
parent="menu_trade_finance_configuration"
action="act_operational_status"/>
<record model="ir.model.access" id="access_operational_status">
<field name="model">trade_finance.operational_status</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_operational_status_admin">
<field name="model">trade_finance.operational_status</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- MARKET INDEX -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="market_index_view_tree">
<field name="model">trade_finance.market_index</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">market_index_tree</field>
</record>
<record model="ir.ui.view" id="market_index_view_form">
<field name="model">trade_finance.market_index</field>
<field name="type">form</field>
<field name="name">market_index_form</field>
</record>
<record model="ir.action.act_window" id="act_market_index">
<field name="name">Market Indexes</field>
<field name="res_model">trade_finance.market_index</field>
</record>
<record model="ir.action.act_window.view" id="act_market_index_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="market_index_view_tree"/>
<field name="act_window" ref="act_market_index"/>
</record>
<record model="ir.action.act_window.view" id="act_market_index_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="market_index_view_form"/>
<field name="act_window" ref="act_market_index"/>
</record>
<menuitem
name="Market Indexes"
sequence="10"
id="menu_market_index"
parent="menu_trade_finance_configuration"
action="act_market_index"/>
<record model="ir.model.access" id="access_market_index">
<field name="model">trade_finance.market_index</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_market_index_admin">
<field name="model">trade_finance.market_index</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- MARKET INDEX RATE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="market_index_rate_view_tree">
<field name="model">trade_finance.market_index_rate</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">market_index_rate_tree</field>
</record>
<record model="ir.ui.view" id="market_index_rate_view_form">
<field name="model">trade_finance.market_index_rate</field>
<field name="type">form</field>
<field name="name">market_index_rate_form</field>
</record>
<record model="ir.action.act_window" id="act_market_index_rate">
<field name="name">Market Index Rates</field>
<field name="res_model">trade_finance.market_index_rate</field>
</record>
<record model="ir.action.act_window.view" id="act_market_index_rate_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="market_index_rate_view_tree"/>
<field name="act_window" ref="act_market_index_rate"/>
</record>
<record model="ir.action.act_window.view" id="act_market_index_rate_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="market_index_rate_view_form"/>
<field name="act_window" ref="act_market_index_rate"/>
</record>
<menuitem
name="Market Index Rates"
sequence="10"
id="menu_market_index_rate"
parent="menu_trade_finance_market_data"
action="act_market_index_rate"/>
<record model="ir.model.access" id="access_market_index_rate">
<field name="model">trade_finance.market_index_rate</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_market_index_rate_admin">
<field name="model">trade_finance.market_index_rate</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- INDEX TERM -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="index_term_view_tree">
<field name="model">trade_finance.index_term</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">index_term_tree</field>
</record>
<record model="ir.ui.view" id="index_term_view_form">
<field name="model">trade_finance.index_term</field>
<field name="type">form</field>
<field name="name">index_term_form</field>
</record>
<record model="ir.action.act_window" id="act_index_term">
<field name="name">Index Terms</field>
<field name="res_model">trade_finance.index_term</field>
</record>
<record model="ir.action.act_window.view" id="act_index_term_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="index_term_view_tree"/>
<field name="act_window" ref="act_index_term"/>
</record>
<record model="ir.action.act_window.view" id="act_index_term_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="index_term_view_form"/>
<field name="act_window" ref="act_index_term"/>
</record>
<menuitem
name="Index Terms"
sequence="30"
id="menu_index_term"
parent="menu_trade_finance_configuration"
action="act_index_term"/>
<record model="ir.model.access" id="access_index_term">
<field name="model">trade_finance.index_term</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_index_term_admin">
<field name="model">trade_finance.index_term</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- INTEREST FORMULA -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="interest_formula_view_tree">
<field name="model">trade_finance.interest_formula</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">interest_formula_tree</field>
</record>
<record model="ir.ui.view" id="interest_formula_view_form">
<field name="model">trade_finance.interest_formula</field>
<field name="type">form</field>
<field name="name">interest_formula_form</field>
</record>
<record model="ir.action.act_window" id="act_interest_formula">
<field name="name">Interest Formulas</field>
<field name="res_model">trade_finance.interest_formula</field>
</record>
<record model="ir.action.act_window.view" id="act_interest_formula_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="interest_formula_view_tree"/>
<field name="act_window" ref="act_interest_formula"/>
</record>
<record model="ir.action.act_window.view" id="act_interest_formula_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="interest_formula_view_form"/>
<field name="act_window" ref="act_interest_formula"/>
</record>
<menuitem
name="Interest Formulas"
sequence="40"
id="menu_interest_formula"
parent="menu_trade_finance_configuration"
action="act_interest_formula"/>
<record model="ir.model.access" id="access_interest_formula">
<field name="model">trade_finance.interest_formula</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_interest_formula_admin">
<field name="model">trade_finance.interest_formula</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- HAIRCUT FORMULA -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="haircut_formula_view_tree">
<field name="model">trade_finance.haircut_formula</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">haircut_formula_tree</field>
</record>
<record model="ir.ui.view" id="haircut_formula_view_form">
<field name="model">trade_finance.haircut_formula</field>
<field name="type">form</field>
<field name="name">haircut_formula_form</field>
</record>
<record model="ir.action.act_window" id="act_haircut_formula">
<field name="name">Haircut Formulas</field>
<field name="res_model">trade_finance.haircut_formula</field>
</record>
<record model="ir.action.act_window.view" id="act_haircut_formula_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="haircut_formula_view_tree"/>
<field name="act_window" ref="act_haircut_formula"/>
</record>
<record model="ir.action.act_window.view" id="act_haircut_formula_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="haircut_formula_view_form"/>
<field name="act_window" ref="act_haircut_formula"/>
</record>
<menuitem
name="Haircut Formulas"
sequence="50"
id="menu_haircut_formula"
parent="menu_trade_finance_configuration"
action="act_haircut_formula"/>
<record model="ir.model.access" id="access_haircut_formula">
<field name="model">trade_finance.haircut_formula</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_haircut_formula_admin">
<field name="model">trade_finance.haircut_formula</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- VALUATION METHOD -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="valuation_method_view_tree">
<field name="model">trade_finance.valuation_method</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">valuation_method_tree</field>
</record>
<record model="ir.ui.view" id="valuation_method_view_form">
<field name="model">trade_finance.valuation_method</field>
<field name="type">form</field>
<field name="name">valuation_method_form</field>
</record>
<record model="ir.action.act_window" id="act_valuation_method">
<field name="name">Valuation Methods</field>
<field name="res_model">trade_finance.valuation_method</field>
</record>
<record model="ir.action.act_window.view" id="act_valuation_method_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="valuation_method_view_tree"/>
<field name="act_window" ref="act_valuation_method"/>
</record>
<record model="ir.action.act_window.view" id="act_valuation_method_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="valuation_method_view_form"/>
<field name="act_window" ref="act_valuation_method"/>
</record>
<menuitem
name="Valuation Methods"
sequence="60"
id="menu_valuation_method"
parent="menu_trade_finance_configuration"
action="act_valuation_method"/>
<record model="ir.model.access" id="access_valuation_method">
<field name="model">trade_finance.valuation_method</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_valuation_method_admin">
<field name="model">trade_finance.valuation_method</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- COLLATERAL TYPE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="collateral_type_view_tree">
<field name="model">trade_finance.collateral_type</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">collateral_type_tree</field>
</record>
<record model="ir.ui.view" id="collateral_type_view_form">
<field name="model">trade_finance.collateral_type</field>
<field name="type">form</field>
<field name="name">collateral_type_form</field>
</record>
<record model="ir.action.act_window" id="act_collateral_type">
<field name="name">Collateral Types</field>
<field name="res_model">trade_finance.collateral_type</field>
</record>
<record model="ir.action.act_window.view" id="act_collateral_type_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="collateral_type_view_tree"/>
<field name="act_window" ref="act_collateral_type"/>
</record>
<record model="ir.action.act_window.view" id="act_collateral_type_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="collateral_type_view_form"/>
<field name="act_window" ref="act_collateral_type"/>
</record>
<menuitem
name="Collateral Types"
sequence="70"
id="menu_collateral_type"
parent="menu_trade_finance_configuration"
action="act_collateral_type"/>
<record model="ir.model.access" id="access_collateral_type">
<field name="model">trade_finance.collateral_type</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_collateral_type_admin">
<field name="model">trade_finance.collateral_type</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- EVIDENCE TYPE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="evidence_type_view_tree">
<field name="model">trade_finance.evidence_type</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">evidence_type_tree</field>
</record>
<record model="ir.ui.view" id="evidence_type_view_form">
<field name="model">trade_finance.evidence_type</field>
<field name="type">form</field>
<field name="name">evidence_type_form</field>
</record>
<record model="ir.action.act_window" id="act_evidence_type">
<field name="name">Evidence Types</field>
<field name="res_model">trade_finance.evidence_type</field>
</record>
<record model="ir.action.act_window.view" id="act_evidence_type_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="evidence_type_view_tree"/>
<field name="act_window" ref="act_evidence_type"/>
</record>
<record model="ir.action.act_window.view" id="act_evidence_type_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="evidence_type_view_form"/>
<field name="act_window" ref="act_evidence_type"/>
</record>
<menuitem
name="Evidence Types"
sequence="80"
id="menu_evidence_type"
parent="menu_trade_finance_configuration"
action="act_evidence_type"/>
<record model="ir.model.access" id="access_evidence_type">
<field name="model">trade_finance.evidence_type</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_evidence_type_admin">
<field name="model">trade_finance.evidence_type</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- TEMPLATE SEGMENT -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="template_segment_view_tree">
<field name="model">trade_finance.template_segment</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">template_segment_tree</field>
</record>
<record model="ir.ui.view" id="template_segment_view_form">
<field name="model">trade_finance.template_segment</field>
<field name="type">form</field>
<field name="name">template_segment_form</field>
</record>
<record model="ir.action.act_window" id="act_template_segment">
<field name="name">Template Segments</field>
<field name="res_model">trade_finance.template_segment</field>
</record>
<record model="ir.action.act_window.view" id="act_template_segment_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="template_segment_view_tree"/>
<field name="act_window" ref="act_template_segment"/>
</record>
<record model="ir.action.act_window.view" id="act_template_segment_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="template_segment_view_form"/>
<field name="act_window" ref="act_template_segment"/>
</record>
<menuitem
name="Template Segments"
sequence="90"
id="menu_template_segment"
parent="menu_trade_finance_configuration"
action="act_template_segment"/>
<record model="ir.model.access" id="access_template_segment">
<field name="model">trade_finance.template_segment</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_template_segment_admin">
<field name="model">trade_finance.template_segment</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- EXECUTION TEMPLATE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="execution_template_view_tree">
<field name="model">trade_finance.execution_template</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">execution_template_tree</field>
</record>
<record model="ir.ui.view" id="execution_template_view_form">
<field name="model">trade_finance.execution_template</field>
<field name="type">form</field>
<field name="name">execution_template_form</field>
</record>
<record model="ir.action.act_window" id="act_execution_template">
<field name="name">Execution Templates</field>
<field name="res_model">trade_finance.execution_template</field>
</record>
<record model="ir.action.act_window.view" id="act_execution_template_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="execution_template_view_tree"/>
<field name="act_window" ref="act_execution_template"/>
</record>
<record model="ir.action.act_window.view" id="act_execution_template_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="execution_template_view_form"/>
<field name="act_window" ref="act_execution_template"/>
</record>
<menuitem
name="Execution Templates"
sequence="100"
id="menu_execution_template"
parent="menu_trade_finance_configuration"
action="act_execution_template"/>
<record model="ir.model.access" id="access_execution_template">
<field name="model">trade_finance.execution_template</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_execution_template_admin">
<field name="model">trade_finance.execution_template</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ExecutionTemplateLine — no separate menu (child of ExecutionTemplate) -->
<record model="ir.ui.view" id="execution_template_line_view_tree">
<field name="model">trade_finance.execution_template_line</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">execution_template_line_tree</field>
</record>
<record model="ir.model.access" id="access_execution_template_line">
<field name="model">trade_finance.execution_template_line</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_execution_template_line_admin">
<field name="model">trade_finance.execution_template_line</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- RECEIVABLE CATEGORY -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="receivable_category_view_tree">
<field name="model">trade_finance.receivable_category</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">receivable_category_tree</field>
</record>
<record model="ir.ui.view" id="receivable_category_view_form">
<field name="model">trade_finance.receivable_category</field>
<field name="type">form</field>
<field name="name">receivable_category_form</field>
</record>
<record model="ir.action.act_window" id="act_receivable_category">
<field name="name">Receivable Categories</field>
<field name="res_model">trade_finance.receivable_category</field>
</record>
<record model="ir.action.act_window.view" id="act_receivable_category_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="receivable_category_view_tree"/>
<field name="act_window" ref="act_receivable_category"/>
</record>
<record model="ir.action.act_window.view" id="act_receivable_category_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="receivable_category_view_form"/>
<field name="act_window" ref="act_receivable_category"/>
</record>
<menuitem
name="Receivable Categories"
sequence="110"
id="menu_receivable_category"
parent="menu_trade_finance_configuration"
action="act_receivable_category"/>
<record model="ir.model.access" id="access_receivable_category">
<field name="model">trade_finance.receivable_category</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_receivable_category_admin">
<field name="model">trade_finance.receivable_category</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- PAYMENT CONDITION TYPE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="payment_condition_type_view_tree">
<field name="model">trade_finance.payment_condition_type</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">payment_condition_type_tree</field>
</record>
<record model="ir.ui.view" id="payment_condition_type_view_form">
<field name="model">trade_finance.payment_condition_type</field>
<field name="type">form</field>
<field name="name">payment_condition_type_form</field>
</record>
<record model="ir.action.act_window" id="act_payment_condition_type">
<field name="name">Payment Condition Types</field>
<field name="res_model">trade_finance.payment_condition_type</field>
</record>
<record model="ir.action.act_window.view" id="act_payment_condition_type_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="payment_condition_type_view_tree"/>
<field name="act_window" ref="act_payment_condition_type"/>
</record>
<record model="ir.action.act_window.view" id="act_payment_condition_type_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="payment_condition_type_view_form"/>
<field name="act_window" ref="act_payment_condition_type"/>
</record>
<menuitem
name="Payment Condition Types"
sequence="120"
id="menu_payment_condition_type"
parent="menu_trade_finance_configuration"
action="act_payment_condition_type"/>
<record model="ir.model.access" id="access_payment_condition_type">
<field name="model">trade_finance.payment_condition_type</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_payment_condition_type_admin">
<field name="model">trade_finance.payment_condition_type</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- FX FEEDER -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="fx_feeder_view_tree">
<field name="model">trade_finance.fx_feeder</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">fx_feeder_tree</field>
</record>
<record model="ir.ui.view" id="fx_feeder_view_form">
<field name="model">trade_finance.fx_feeder</field>
<field name="type">form</field>
<field name="name">fx_feeder_form</field>
</record>
<record model="ir.action.act_window" id="act_fx_feeder">
<field name="name">FX Rate Feeders</field>
<field name="res_model">trade_finance.fx_feeder</field>
</record>
<record model="ir.action.act_window.view" id="act_fx_feeder_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="fx_feeder_view_tree"/>
<field name="act_window" ref="act_fx_feeder"/>
</record>
<record model="ir.action.act_window.view" id="act_fx_feeder_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="fx_feeder_view_form"/>
<field name="act_window" ref="act_fx_feeder"/>
</record>
<menuitem
name="FX Rate Feeders"
sequence="130"
id="menu_fx_feeder"
parent="menu_trade_finance_configuration"
action="act_fx_feeder"/>
<record model="ir.model.access" id="access_fx_feeder">
<field name="model">trade_finance.fx_feeder</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_fx_feeder_admin">
<field name="model">trade_finance.fx_feeder</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- BLOCKING REASON -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="blocking_reason_view_tree">
<field name="model">trade_finance.blocking_reason</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">blocking_reason_tree</field>
</record>
<record model="ir.ui.view" id="blocking_reason_view_form">
<field name="model">trade_finance.blocking_reason</field>
<field name="type">form</field>
<field name="name">blocking_reason_form</field>
</record>
<record model="ir.action.act_window" id="act_blocking_reason">
<field name="name">Blocking Reasons</field>
<field name="res_model">trade_finance.blocking_reason</field>
</record>
<record model="ir.action.act_window.view" id="act_blocking_reason_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="blocking_reason_view_tree"/>
<field name="act_window" ref="act_blocking_reason"/>
</record>
<record model="ir.action.act_window.view" id="act_blocking_reason_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="blocking_reason_view_form"/>
<field name="act_window" ref="act_blocking_reason"/>
</record>
<menuitem
name="Blocking Reasons"
sequence="140"
id="menu_blocking_reason"
parent="menu_trade_finance_configuration"
action="act_blocking_reason"/>
<record model="ir.model.access" id="access_blocking_reason">
<field name="model">trade_finance.blocking_reason</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_blocking_reason_admin">
<field name="model">trade_finance.blocking_reason</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<!-- ================================================================ -->
<!-- CHARGE TYPE -->
<!-- ================================================================ -->
<record model="ir.ui.view" id="charge_type_view_tree">
<field name="model">trade_finance.charge_type</field>
<field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="name">charge_type_tree</field>
</record>
<record model="ir.ui.view" id="charge_type_view_form">
<field name="model">trade_finance.charge_type</field>
<field name="type">form</field>
<field name="name">charge_type_form</field>
</record>
<record model="ir.action.act_window" id="act_charge_type">
<field name="name">Charge Types</field>
<field name="res_model">trade_finance.charge_type</field>
</record>
<record model="ir.action.act_window.view" id="act_charge_type_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="charge_type_view_tree"/>
<field name="act_window" ref="act_charge_type"/>
</record>
<record model="ir.action.act_window.view" id="act_charge_type_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="charge_type_view_form"/>
<field name="act_window" ref="act_charge_type"/>
</record>
<menuitem
name="Charge Types"
sequence="150"
id="menu_charge_type"
parent="menu_trade_finance_configuration"
action="act_charge_type"/>
<record model="ir.model.access" id="access_charge_type">
<field name="model">trade_finance.charge_type</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_charge_type_admin">
<field name="model">trade_finance.charge_type</field>
<field name="group" ref="group_trade_finance_admin"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,61 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered
__all__ = ['TemplateSegment', 'ExecutionTemplate', 'ExecutionTemplateLine']
class TemplateSegment(ModelSQL, ModelView):
'Template Segment'
__name__ = 'trade_finance.template_segment'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
from_place = fields.Many2One(
'stock.location', 'From Place',
help='Origin location / port of loading')
to_place = fields.Many2One(
'stock.location', 'To Place',
help='Destination location / port of discharge')
default_duration_days = fields.Integer(
'Default Duration (days)',
help='Default number of days for this segment')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class ExecutionTemplate(ModelSQL, ModelView):
'Execution Template'
__name__ = 'trade_finance.execution_template'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
description = fields.Text('Description')
lines = fields.One2Many(
'trade_finance.execution_template_line', 'template', 'Segments')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class ExecutionTemplateLine(sequence_ordered(), ModelSQL, ModelView):
'Execution Template Line'
__name__ = 'trade_finance.execution_template_line'
template = fields.Many2One(
'trade_finance.execution_template', 'Template',
required=True, ondelete='CASCADE')
segment = fields.Many2One(
'trade_finance.template_segment', 'Segment',
required=True, ondelete='RESTRICT')
duration_days = fields.Integer(
'Duration (days)',
help='Overrides the segment default duration for this template')

View File

@@ -0,0 +1,12 @@
[tryton]
version=7.2.7
depends:
ir
res
stock
price
bank
xml:
reference.xml
facility.xml
constraint_type.xml

View File

@@ -0,0 +1,50 @@
# This file is part of Tradon. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields
__all__ = ['ValuationMethod', 'HaircutFormula', 'CollateralType']
class ValuationMethod(ModelSQL, ModelView):
'Valuation Method'
__name__ = 'trade_finance.valuation_method'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
description = fields.Text('Description')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class HaircutFormula(ModelSQL, ModelView):
'Haircut Formula'
__name__ = 'trade_finance.haircut_formula'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
description = fields.Text('Description')
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True
class CollateralType(ModelSQL, ModelView):
'Collateral Type'
__name__ = 'trade_finance.collateral_type'
_rec_name = 'name'
code = fields.Char('Code', required=True)
name = fields.Char('Name', required=True)
active = fields.Boolean('Active')
@staticmethod
def default_active():
return True

View File

@@ -0,0 +1,8 @@
<form col="4">
<label name="code"/>
<field name="code"/>
<label name="name"/>
<field name="name"/>
<label name="active"/>
<field name="active"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="code"/>
<field name="name"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,10 @@
<form col="4">
<label name="code"/>
<field name="code"/>
<label name="name"/>
<field name="name"/>
<label name="active"/>
<field name="active"/>
<label name="description"/>
<field name="description" colspan="4"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="code"/>
<field name="name"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,8 @@
<form col="4">
<label name="code"/>
<field name="code"/>
<label name="name"/>
<field name="name"/>
<label name="active"/>
<field name="active"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="code"/>
<field name="name"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,12 @@
<form col="4">
<group id="main" col="4" colspan="4">
<label name="name"/>
<field name="name" colspan="3"/>
<label name="view_name"/>
<field name="view_name" colspan="3"/>
<label name="value_field"/>
<field name="value_field" colspan="3"/>
<label name="label_field"/>
<field name="label_field" colspan="3"/>
</group>
</form>

View File

@@ -0,0 +1,3 @@
<tree>
<field name="name"/>
</tree>

View File

@@ -0,0 +1,10 @@
<form col="4">
<label name="code"/>
<field name="code"/>
<label name="name"/>
<field name="name"/>
<label name="active"/>
<field name="active"/>
<label name="description"/>
<field name="description" colspan="4"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="code"/>
<field name="name"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,12 @@
<form col="4">
<label name="code"/>
<field name="code"/>
<label name="name"/>
<field name="name"/>
<label name="active"/>
<field name="active"/>
<label name="description"/>
<field name="description" colspan="4"/>
<newline/>
<field name="lines" colspan="4"/>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="sequence"/>
<field name="segment"/>
<field name="duration_days"/>
</tree>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="code"/>
<field name="name"/>
<field name="active"/>
</tree>

View File

@@ -0,0 +1,7 @@
<tree>
<field name="attribute"/>
<field name="value"/>
<field name="haircut_pct"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,6 @@
<tree>
<field name="name"/>
<field name="amount"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,10 @@
<form col="4">
<label name="constraint_type"/>
<field name="constraint_type" colspan="3"/>
<label name="date_from"/>
<field name="date_from"/>
<label name="date_to"/>
<field name="date_to"/>
<label name="is_exclusion"/>
<field name="is_exclusion"/>
</form>

View File

@@ -0,0 +1,6 @@
<tree>
<field name="constraint_type"/>
<field name="is_exclusion"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,6 @@
<tree>
<field name="name"/>
<field name="ratio_type"/>
<field name="threshold"/>
<field name="currency"/>
</tree>

View File

@@ -0,0 +1,8 @@
<tree>
<field name="currency"/>
<field name="valuation_method"/>
<field name="fx_haircut_formula"/>
<field name="fx_feeder"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,50 @@
<form col="6">
<group id="header" col="6" colspan="6">
<label name="name"/>
<field name="name" colspan="3"/>
<label name="status"/>
<field name="status" widget="selection"/>
<label name="tfe"/>
<field name="tfe" colspan="3"/>
<label name="commitment_status"/>
<field name="commitment_status"/>
<label name="currency"/>
<field name="currency"/>
<label name="fx_feeder"/>
<field name="fx_feeder"/>
<label name="fx_haircut_pct"/>
<field name="fx_haircut_pct"/>
<newline/>
<label name="date_from"/>
<field name="date_from"/>
<label name="date_to"/>
<field name="date_to"/>
<label name="is_tpa"/>
<field name="is_tpa"/>
<label name="broker"/>
<field name="broker"/>
<label name="broker_account"/>
<field name="broker_account" colspan="3"/>
<newline/>
<label name="description"/>
<field name="description" colspan="5"/>
</group>
<notebook colspan="6">
<page string="Limits" id="limits">
<field name="limits" colspan="6"
domain="[('parent', '=', None)]"/>
</page>
<page string="Currencies" id="currencies">
<field name="currencies" colspan="6"/>
</page>
<page string="Constraints" id="constraints">
<field name="constraints" colspan="6"/>
</page>
<page string="Caps" id="caps">
<field name="caps" colspan="6"/>
</page>
<page string="Covenants" id="covenants">
<field name="covenants" colspan="6"/>
</page>
</notebook>
</form>

View File

@@ -0,0 +1,7 @@
<tree>
<field name="bank_account"/>
<field name="currency"/>
<field name="is_default"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,11 @@
<tree>
<field name="cost_type"/>
<field name="spread"/>
<field name="index"/>
<field name="index_term"/>
<field name="interest_formula"/>
<field name="flat_amount"/>
<field name="flat_currency"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,7 @@
<tree>
<field name="attribute"/>
<field name="value"/>
<field name="variation_type"/>
<field name="variation_value"/>
<field name="variation_currency"/>
</tree>

View File

@@ -0,0 +1,8 @@
<tree>
<field name="currency"/>
<field name="haircut_pct"/>
<field name="fx_feeder"/>
<field name="valuation_method"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,44 @@
<form col="4">
<group id="header" col="4" colspan="4">
<label name="name"/>
<field name="name" colspan="3"/>
<label name="alternative_name"/>
<field name="alternative_name" colspan="3"/>
<label name="financing_type"/>
<field name="financing_type" colspan="3"/>
<label name="amount"/>
<field name="amount"/>
<label name="tenor"/>
<field name="tenor"/>
<label name="sequence"/>
<field name="sequence"/>
<newline/>
<label name="parent"/>
<field name="parent" colspan="3"/>
<label name="facility"/>
<field name="facility" colspan="3"/>
</group>
<notebook colspan="4">
<page string="Sub-Limits" id="children">
<field name="children" colspan="4"/>
</page>
<page string="Haircuts" id="haircuts">
<field name="haircuts" colspan="4"/>
</page>
<page string="Currencies" id="currencies">
<field name="currencies" colspan="4"/>
</page>
<page string="Costs" id="costs">
<field name="costs" colspan="4"/>
</page>
<page string="Operational Statuses" id="op_statuses">
<field name="op_statuses" colspan="4"/>
</page>
<page string="Bank Accounts" id="bank_accounts">
<field name="bank_accounts" colspan="4"/>
</page>
<page string="Constraints" id="limit_constraints">
<field name="constraints" colspan="4"/>
</page>
</notebook>
</form>

View File

@@ -0,0 +1,7 @@
<tree>
<field name="attribute"/>
<field name="value"/>
<field name="haircut_pct"/>
<field name="date_from"/>
<field name="date_to"/>
</tree>

View File

@@ -0,0 +1,4 @@
<tree>
<field name="operational_status"/>
<field name="evidence_type"/>
</tree>

View File

@@ -0,0 +1,7 @@
<tree expand="1">
<field name="name"/>
<field name="parent"/>
<field name="financing_type"/>
<field name="amount"/>
<field name="tenor"/>
</tree>

View File

@@ -0,0 +1,10 @@
<form col="4">
<group id="main" col="4" colspan="4">
<label name="code"/>
<field name="code"/>
<label name="active"/>
<field name="active"/>
<label name="name"/>
<field name="name" colspan="3"/>
</group>
</form>

View File

@@ -0,0 +1,5 @@
<tree>
<field name="code"/>
<field name="name"/>
<field name="active"/>
</tree>

Some files were not shown because too many files have changed in this diff Show More