Compare commits
66 Commits
dev
...
4d94aa78ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d94aa78ed | |||
| da01249f66 | |||
|
|
5cbb57c657 | ||
| 50a8c6328f | |||
| eaa5c8b544 | |||
| 78e9e06a8b | |||
| 9f06398b2c | |||
| 51a84f1f2e | |||
| 00330008d1 | |||
| 3480eb8a7a | |||
| 5179d98289 | |||
| 2109d7a3e4 | |||
| 1f350e6207 | |||
| 7722292482 | |||
| ec359f6b8a | |||
| 845b9cf749 | |||
| 48b941b109 | |||
| 18ece66cdb | |||
| acfc2fe88a | |||
| 888b880bd6 | |||
| 1f62ae91dd | |||
| 05e68636ad | |||
| bfb9bb3188 | |||
| b78e64f9f1 | |||
| 199b8aec12 | |||
| 1757075f2b | |||
| 172d38479d | |||
| 4902368b15 | |||
| 7b4f757cb5 | |||
| b37f132cdf | |||
| 15f791bd92 | |||
| 58cd66e543 | |||
| cc6ce82ec1 | |||
| 11526ef3ee | |||
| 6d52317804 | |||
| a99efcfc5b | |||
| 0d5cf7dffc | |||
| 613b679908 | |||
| 51ced23ab8 | |||
| 2958e1fb9e | |||
| 346a34951d | |||
| b644aea007 | |||
| 5dbaba5f32 | |||
| d133665fc7 | |||
| c2cb2a874c | |||
| 408970c339 | |||
| ea2627c9ae | |||
| ac988a714a | |||
| 97eae6e4a6 | |||
| 3976b387d7 | |||
| 9b8e8127a1 | |||
| f53a9bce27 | |||
| a7753b974f | |||
| c687828ba5 | |||
| 5054b64cd0 | |||
| 06922973b7 | |||
| 18ebf7f06c | |||
| 44c4560f24 | |||
| 7643bf21fb | |||
| 97677025d7 | |||
| 02fe5b3e5d | |||
| 6e529deca0 | |||
| efee365fc6 | |||
| 6bf245ac64 | |||
| 238869989a | |||
|
|
806e374ceb |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.pyc
|
||||
@@ -38,13 +38,18 @@ 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 `"` et `'` 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()`.
|
||||
|
||||
|
||||
1
debug.log
Normal file
1
debug.log
Normal file
@@ -0,0 +1 @@
|
||||
[0407/143111.471:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: Accès refusé. (0x5)
|
||||
5
deployment/README.md
Normal file
5
deployment/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Deployment Notes
|
||||
|
||||
- Runbook onboarding SSH: `deployment/runbooks/vps-onboarding.md`
|
||||
- VPS 46.202.173.47 credentials: `deployment/vps/46.202.173.47-credentials.md`
|
||||
- VPS 46.202.173.47 quickstart: `deployment/vps/46.202.173.47-quickstart.md`
|
||||
57
deployment/runbooks/vps-onboarding.md
Normal file
57
deployment/runbooks/vps-onboarding.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Procedure - Ajouter un nouveau VPS (SSH)
|
||||
|
||||
Date de reference: 2026-04-07
|
||||
|
||||
## 1) Preparation locale (Windows)
|
||||
|
||||
1. Creer le dossier SSH local si absent:
|
||||
`New-Item -ItemType Directory -Force $env:USERPROFILE\.ssh`
|
||||
|
||||
2. Generer une cle dediee VPS:
|
||||
`ssh-keygen -t ed25519 -C "vps-deploy" -f $env:USERPROFILE\.ssh\vps_deploy_key`
|
||||
|
||||
3. Lire la cle publique:
|
||||
`Get-Content $env:USERPROFILE\.ssh\vps_deploy_key.pub`
|
||||
|
||||
## 2) Installer la cle sur le VPS
|
||||
|
||||
1. Se connecter au VPS avec mot de passe (premiere fois):
|
||||
`ssh <user>@<ip_vps>`
|
||||
|
||||
2. Sur le VPS, preparer le dossier SSH:
|
||||
`mkdir -p ~/.ssh`
|
||||
`chmod 700 ~/.ssh`
|
||||
|
||||
3. Ajouter la cle publique (une ligne complete):
|
||||
`echo "ssh-ed25519 ... vps-deploy" >> ~/.ssh/authorized_keys`
|
||||
`chmod 600 ~/.ssh/authorized_keys`
|
||||
|
||||
## 3) Tester la connexion par cle
|
||||
|
||||
Depuis Windows:
|
||||
`ssh -i $env:USERPROFILE\.ssh\vps_deploy_key <user>@<ip_vps>`
|
||||
|
||||
## 4) Test operationnel minimal
|
||||
|
||||
Creer un dossier distant:
|
||||
`ssh -i $env:USERPROFILE\.ssh\vps_deploy_key <user>@<ip_vps> "mkdir -p ~/test_codex_deploy && ls -ld ~/test_codex_deploy"`
|
||||
|
||||
## 5) Durcissement recommande
|
||||
|
||||
- Desactiver l'authentification par mot de passe apres validation de la cle.
|
||||
- Utiliser une cle dediee par environnement (dev/staging/prod).
|
||||
- Documenter user + IP + chemin de cle dans une fiche VPS separee.
|
||||
|
||||
## 6) Note importante - Contrainte sandbox Codex
|
||||
|
||||
- Si une commande SSH/SCP echoue avec un message proche de:
|
||||
- `Identity file ... not accessible: Permission denied`
|
||||
- Cause probable:
|
||||
- la session est en sandbox et ne peut pas lire la cle locale dans
|
||||
`C:\Users\<user>\.ssh\...`.
|
||||
- Action:
|
||||
- relancer la commande en mode `require_escalated` (hors sandbox) pour
|
||||
autoriser l'acces a la cle locale.
|
||||
- Exemple observe le 2026-04-07:
|
||||
- creation du dossier `/root/test` sur `46.202.173.47` reussie uniquement
|
||||
apres escalation.
|
||||
36
deployment/vps/46.202.173.47-credentials.md
Normal file
36
deployment/vps/46.202.173.47-credentials.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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`
|
||||
39
deployment/vps/46.202.173.47-quickstart.md
Normal file
39
deployment/vps/46.202.173.47-quickstart.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Commandes Rapides - VPS 46.202.173.47
|
||||
|
||||
Date de reference: 2026-04-07
|
||||
|
||||
## Cles SSH par utilisateur
|
||||
|
||||
| Utilisateur | Fichier cle locale |
|
||||
|---|---|
|
||||
| Laurent Barontini | `$env:USERPROFILE\.ssh\vps_deploy_key` |
|
||||
| Sylvain Duvernay | `$env:USERPROFILE\.ssh\id_ed25519` |
|
||||
|
||||
> Les commandes ci-dessous utilisent `id_ed25519` (Sylvain Duvernay).
|
||||
|
||||
## 1) Test SSH
|
||||
|
||||
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47 "echo ok"`
|
||||
|
||||
## 2) Creer un dossier test distant
|
||||
|
||||
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47 "mkdir -p ~/test_codex_deploy && ls -ld ~/test_codex_deploy"`
|
||||
|
||||
## 3) Lister home distant
|
||||
|
||||
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47 "ls -la ~"`
|
||||
|
||||
## 4) Copier un fichier local vers le VPS
|
||||
|
||||
`scp -i $env:USERPROFILE\.ssh\id_ed25519 .\local.txt <user>@46.202.173.47:~/local.txt`
|
||||
|
||||
## 5) Recuperer un fichier du VPS
|
||||
|
||||
`scp -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47:~/remote.txt .\remote.txt`
|
||||
|
||||
## 6) Depannage sandbox (Codex)
|
||||
|
||||
- Symptome:
|
||||
- `Identity file ... not accessible: Permission denied`
|
||||
- Correctif:
|
||||
- relancer la commande SSH/SCP en mode escalade (`require_escalated`).
|
||||
@@ -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">Provisional Invoice</field>
|
||||
<field name="name">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">Final Invoice</field>
|
||||
<field name="name">CN/DN</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="report_name">account.invoice</field>
|
||||
<field name="report">account_invoice/invoice_ict_final.fodt</field>
|
||||
|
||||
@@ -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>Provisional Invoice</dc:title>
|
||||
<dc:title>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">Provisional Invoice</text:p>
|
||||
<text:p text:style-name="P22">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,15 +3930,20 @@
|
||||
<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"><format_number(invoice.report_lbs, invoice.party.lang) if invoice.report_lbs != '' else ''></text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text"><format_number(invoice.report_net, invoice.party.lang) if invoice.report_net != '' else ''></text:placeholder> MTS)</text:p>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><invoice.report_description_upper or invoice.report_product_description></text:placeholder><text:s/>CROP <text:placeholder text:placeholder-type="text"><invoice.report_crop_name></text:placeholder></text:p>
|
||||
<text:p text:style-name="P26">QUANTITY: <text:placeholder text:placeholder-type="text"><format_number(invoice.report_lbs, invoice.party.lang) if invoice.report_lbs != '' else ''></text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text"><format_number(invoice.report_net, invoice.party.lang) if invoice.report_net != '' else ''></text:placeholder> <text:placeholder text:placeholder-type="text"><invoice.report_weight_unit_upper></text:placeholder>)</text:p>
|
||||
<text:p text:style-name="P26"/>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><invoice.report_description_upper or invoice.report_product_description></text:placeholder><text:placeholder text:placeholder-type="text"><' CROP ' + invoice.report_crop_name if invoice.report_crop_name else ''></text:placeholder></text:p>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><invoice.report_attributes_name></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text"><invoice.report_rate_currency_upper></text:placeholder><text:s/><text:placeholder text:placeholder-type="text"><invoice.report_rate_value></text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text"><invoice.report_rate_unit_upper></text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text"><invoice.report_rate_price_words></text:placeholder>) <text:placeholder text:placeholder-type="text"><invoice.report_rate_pricing_text></text:placeholder></text:p>
|
||||
<text:p text:style-name="P26"><text:placeholder text:placeholder-type="text"><for each="block in invoice.report_trade_blocks"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P26"><text:placeholder text:placeholder-type="text"><block[0]></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text"><block[1]></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18"/>
|
||||
<text:p text:style-name="P26"><text:placeholder text:placeholder-type="text"></for></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"><invoice.report_incoterm></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">PROFORMA INVOICE NO. <text:placeholder text:placeholder-type="text"><invoice.report_proforma_invoice_number></text:placeholder><text:s/>DATED <text:placeholder text:placeholder-type="text"><format_date(invoice.report_proforma_invoice_date, invoice.party.lang) if invoice.report_proforma_invoice_date else ''></text:placeholder>.</text:p>
|
||||
<text:p text:style-name="P26"/>
|
||||
<text:p text:style-name="P26"/>
|
||||
<text:p text:style-name="P12"/>
|
||||
</table:table-cell>
|
||||
@@ -3952,10 +3957,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 KGS</text:p>
|
||||
<text:p text:style-name="P41"><text:s text:c="13"/>Gross <text:placeholder text:placeholder-type="text"><invoice.report_weight_unit_upper></text:placeholder></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 KGS</text:p>
|
||||
<text:p text:style-name="P15"><text:s text:c="13"/>NET <text:placeholder text:placeholder-type="text"><invoice.report_weight_unit_upper></text:placeholder></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>
|
||||
@@ -4009,7 +4014,9 @@
|
||||
<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">At <text:placeholder text:placeholder-type="text"><invoice.report_rate_currency_upper></text:placeholder><text:s/><text:placeholder text:placeholder-type="text"><invoice.report_rate_value></text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text"><invoice.report_rate_unit_upper></text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text"><invoice.report_rate_price_words></text:placeholder>) <text:placeholder text:placeholder-type="text"><invoice.report_rate_pricing_text></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text"><for each="line in invoice.report_rate_lines.splitlines()"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14">At <text:placeholder text:placeholder-type="text"><line></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"/>
|
||||
<text:p text:style-name="P14">FREIGHT VALUE: <text:placeholder text:placeholder-type="text"><invoice.report_freight_currency_symbol></text:placeholder><text:s/><text:placeholder text:placeholder-type="text"><format_number(invoice.report_freight_amount, invoice.party.lang) if invoice.report_freight_amount != '' else ''></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"/>
|
||||
|
||||
@@ -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>Final Invoice</dc:title>
|
||||
<dc:title>Credit / Debit Note</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">Final Invoice</text:p>
|
||||
<text:p text:style-name="P23"><text:placeholder text:placeholder-type="text"><invoice.report_note_title></text:placeholder></text:p>
|
||||
</table:table-cell>
|
||||
<table:table-cell table:style-name="Tableau3.A1" office:value-type="string">
|
||||
<text:p text:style-name="P23"/>
|
||||
@@ -3916,23 +3916,19 @@
|
||||
<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"><text:placeholder text:placeholder-type="text"><for each="line in invoice.lines"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P28"><text:placeholder text:placeholder-type="text"><if test="line.type == 'line'"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P22"><text:placeholder text:placeholder-type="text"><if test="line.report_description_upper"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P22"><text:placeholder text:placeholder-type="text"><line.report_description_upper></text:placeholder></text:p>
|
||||
<text:p text:style-name="P22"><text:placeholder text:placeholder-type="text"></if></text:placeholder></text:p>
|
||||
<text:p text:style-name="P27">QUANTITY <text:placeholder text:placeholder-type="text"><format_number(line.report_lbs, invoice.party.lang) if line.report_lbs != '' else ''></text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text"><format_number(line.report_net, invoice.party.lang) if line.report_net != '' else ''></text:placeholder> MTS)</text:p>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><line.report_product_description or line.product_name or ''></text:placeholder><text:s/>CROP <text:placeholder text:placeholder-type="text"><line.report_crop_name></text:placeholder></text:p>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><line.report_attributes_name></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text"><line.report_rate_currency_upper></text:placeholder><text:s/><text:placeholder text:placeholder-type="text"><line.report_rate_value></text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text"><line.report_rate_unit_upper></text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text"><line.report_rate_price_words></text:placeholder>) <text:placeholder text:placeholder-type="text"><line.report_rate_pricing_text></text:placeholder></text:p>
|
||||
<text:p text:style-name="P27">QUANTITY: <text:placeholder text:placeholder-type="text"><format_number(invoice.report_lbs, invoice.party.lang) if invoice.report_lbs != '' else ''></text:placeholder><text:s/>LBS (<text:placeholder text:placeholder-type="text"><format_number(invoice.report_net, invoice.party.lang) if invoice.report_net != '' else ''></text:placeholder> <text:placeholder text:placeholder-type="text"><invoice.report_weight_unit_upper></text:placeholder>)</text:p>
|
||||
<text:p text:style-name="P27"/>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><invoice.report_description_upper or invoice.report_product_description></text:placeholder><text:placeholder text:placeholder-type="text"><' CROP ' + invoice.report_crop_name if invoice.report_crop_name else ''></text:placeholder></text:p>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"><invoice.report_attributes_name></text:placeholder></text:p>
|
||||
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text"><for each="block in invoice.report_trade_blocks"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text"><block[0]></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18">At <text:placeholder text:placeholder-type="text"><block[1]></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18"/>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"></if></text:placeholder></text:p>
|
||||
<text:p text:style-name="P18"/>
|
||||
<text:p text:style-name="P21"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
<text:p text:style-name="P27"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
<text:p text:style-name="P21"/>
|
||||
<text:p text:style-name="P34"><text:placeholder text:placeholder-type="text"><invoice.report_incoterm></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">PROFORMA INVOICE NO. <text:placeholder text:placeholder-type="text"><invoice.report_proforma_invoice_number></text:placeholder><text:s/>DATED <text:placeholder text:placeholder-type="text"><format_date(invoice.report_proforma_invoice_date, invoice.party.lang) if invoice.report_proforma_invoice_date else ''></text:placeholder>.</text:p>
|
||||
<text:p text:style-name="P27"/>
|
||||
<text:p text:style-name="P27"/>
|
||||
<text:p text:style-name="P12"/>
|
||||
</table:table-cell>
|
||||
@@ -3946,10 +3942,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 KGS</text:p>
|
||||
<text:p text:style-name="P43"><text:s text:c="13"/>Gross <text:placeholder text:placeholder-type="text"><invoice.report_weight_unit_upper></text:placeholder></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 KGS</text:p>
|
||||
<text:p text:style-name="P15"><text:s text:c="13"/>NET <text:placeholder text:placeholder-type="text"><invoice.report_weight_unit_upper></text:placeholder></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>
|
||||
@@ -4003,7 +3999,9 @@
|
||||
<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">At <text:placeholder text:placeholder-type="text"><invoice.report_rate_currency_upper></text:placeholder><text:s/><text:placeholder text:placeholder-type="text"><invoice.report_rate_value></text:placeholder><text:s/>PER <text:placeholder text:placeholder-type="text"><invoice.report_rate_unit_upper></text:placeholder><text:s/>(<text:placeholder text:placeholder-type="text"><invoice.report_rate_price_words></text:placeholder>) <text:placeholder text:placeholder-type="text"><invoice.report_rate_pricing_text></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text"><for each="line in invoice.report_positive_rate_lines.splitlines()"></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14">At <text:placeholder text:placeholder-type="text"><line></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"><text:placeholder text:placeholder-type="text"></for></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"><invoice.report_freight_currency_symbol></text:placeholder><text:s/><text:placeholder text:placeholder-type="text"><format_number(invoice.report_freight_amount, invoice.party.lang) if invoice.report_freight_amount != '' else ''></text:placeholder></text:p>
|
||||
<text:p text:style-name="P14"/>
|
||||
|
||||
1708
modules/account_invoice/invoice_melya.fodt
Normal file
1708
modules/account_invoice/invoice_melya.fodt
Normal file
File diff suppressed because it is too large
Load Diff
1872
modules/account_invoice/payment_order.fodt
Normal file
1872
modules/account_invoice/payment_order.fodt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -248,29 +248,10 @@ 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)
|
||||
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"
|
||||
doc.notes = (
|
||||
(doc.notes or "")
|
||||
+ "Global WR linked to shipment. "
|
||||
+ "Create remote lot WRs from the weight report form.\n")
|
||||
|
||||
# if cls.rule_set.ocr_required:[]
|
||||
# cls.run_ocr([doc])
|
||||
@@ -293,4 +274,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()
|
||||
|
||||
168
modules/purchase_trade/AGENTS.md
Normal file
168
modules/purchase_trade/AGENTS.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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.
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
from trytond.pool import Pool
|
||||
|
||||
from . import (
|
||||
account,
|
||||
purchase,
|
||||
from . import (
|
||||
account,
|
||||
configuration,
|
||||
purchase,
|
||||
sale,
|
||||
global_reporting,
|
||||
stock,
|
||||
@@ -54,9 +55,10 @@ def register():
|
||||
incoming.ImportSwift,
|
||||
lc.LCMT700,
|
||||
lc.LCMessage,
|
||||
lc.CreateLCStart,
|
||||
global_reporting.GRConfiguration,
|
||||
module='purchase_trade', type_='model')
|
||||
lc.CreateLCStart,
|
||||
global_reporting.GRConfiguration,
|
||||
configuration.Configuration,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
incoming.ImportSwift,
|
||||
incoming.PrepareDocuments,
|
||||
@@ -105,10 +107,13 @@ def register():
|
||||
purchase.FeeLots,
|
||||
valuation.Valuation,
|
||||
valuation.ValuationLine,
|
||||
valuation.ValuationDyn,
|
||||
valuation.ValuationReport,
|
||||
valuation.ValuationReportContext,
|
||||
derivative.Derivative,
|
||||
valuation.ValuationDyn,
|
||||
valuation.ValuationReport,
|
||||
valuation.ValuationReportContext,
|
||||
valuation.ValuationProcessDimension,
|
||||
valuation.ValuationProcessStart,
|
||||
valuation.ValuationProcessResult,
|
||||
derivative.Derivative,
|
||||
derivative.DerivativeMatch,
|
||||
derivative.MatchWizardStart,
|
||||
derivative.DerivativeReport,
|
||||
@@ -258,14 +263,23 @@ def register():
|
||||
purchase.InvoicePayment,
|
||||
stock.ImportSoFWizard,
|
||||
dashboard.BotWizard,
|
||||
dashboard.DashboardLoader,
|
||||
forex.ForexReport,
|
||||
purchase.PnlReport,
|
||||
purchase.PositionReport,
|
||||
derivative.DerivativeMatchWizard,
|
||||
module='purchase', type_='wizard')
|
||||
Pool.register(
|
||||
sale.SaleCreatePurchase,
|
||||
sale.SaleAllocationsWizard,
|
||||
module='sale', type_='wizard')
|
||||
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,
|
||||
stock.ShipmentPackingListReport,
|
||||
module='purchase_trade', type_='report')
|
||||
|
||||
|
||||
19
modules/purchase_trade/configuration.py
Normal file
19
modules/purchase_trade/configuration.py
Normal file
@@ -0,0 +1,19 @@
|
||||
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")
|
||||
shipment_packing_list_report_template = fields.Char("Packing List Template")
|
||||
48
modules/purchase_trade/configuration.xml
Normal file
48
modules/purchase_trade/configuration.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<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>
|
||||
@@ -1,8 +1,8 @@
|
||||
# Business Rules - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.2`
|
||||
Derniere mise a jour: `2026-03-27`
|
||||
Version: `v0.4`
|
||||
Derniere mise a jour: `2026-04-02`
|
||||
Owner metier: `a completer`
|
||||
Owner technique: `a completer`
|
||||
|
||||
@@ -100,6 +100,290 @@ 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
|
||||
@@ -146,6 +430,8 @@ 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
|
||||
|
||||
@@ -155,3 +441,11 @@ 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`
|
||||
|
||||
417
modules/purchase_trade/docs/template-properties.md
Normal file
417
modules/purchase_trade/docs/template-properties.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# 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
|
||||
@@ -1,8 +1,8 @@
|
||||
# Template Rules - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.2`
|
||||
Derniere mise a jour: `2026-03-27`
|
||||
Version: `v0.4`
|
||||
Derniere mise a jour: `2026-04-07`
|
||||
|
||||
## 1) Scope
|
||||
|
||||
@@ -16,6 +16,12 @@ Derniere mise a jour: `2026-03-27`
|
||||
|
||||
- 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
|
||||
|
||||
@@ -92,6 +98,135 @@ Derniere mise a jour: `2026-03-27`
|
||||
- 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`
|
||||
- Dans la section `Shipment`, les templates metier attendus sont:
|
||||
- `Shipping`
|
||||
- `Insurance`
|
||||
- `Packing List`
|
||||
- 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 `<records[0].report_* ...>`
|
||||
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 `<...>` directement dans un `text:span`,
|
||||
`text:p`, `text:h`, etc.
|
||||
- Exemple:
|
||||
- incorrect:
|
||||
`PAYMENT ORDER <records[0].report_payment_order_document_reference or ''>`
|
||||
- correct:
|
||||
`PAYMENT ORDER <text:placeholder text:placeholder-type="text"><records[0].report_payment_order_document_reference or ''></text:placeholder>`
|
||||
|
||||
### TR-007 - Pour une facture trade, privilegier le lot physique comme chemin de navigation
|
||||
|
||||
@@ -120,6 +255,44 @@ Derniere mise a jour: `2026-03-27`
|
||||
- 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.
|
||||
|
||||
@@ -1,17 +1,52 @@
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import date as dt_date
|
||||
|
||||
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
|
||||
@@ -46,6 +81,52 @@ 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')
|
||||
@@ -68,6 +149,158 @@ 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()
|
||||
@@ -119,10 +352,17 @@ 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.lines[0].description or '').upper()
|
||||
return self._clean_report_description(self.lines[0].description)
|
||||
return ''
|
||||
|
||||
@property
|
||||
@@ -146,6 +386,39 @@ 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()
|
||||
@@ -181,6 +454,66 @@ 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()
|
||||
@@ -188,6 +521,16 @@ 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()
|
||||
@@ -199,6 +542,39 @@ 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
|
||||
@@ -212,9 +588,10 @@ class Invoice(metaclass=PoolMeta):
|
||||
|
||||
@property
|
||||
def report_gross(self):
|
||||
sale = self._get_report_sale()
|
||||
if sale and sale.report_gross != '':
|
||||
return sale.report_gross
|
||||
if self.lines:
|
||||
return sum(
|
||||
Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||||
for line in self._get_report_invoice_lines())
|
||||
line = self._get_report_trade_line()
|
||||
if line and line.lots:
|
||||
return sum(
|
||||
@@ -225,9 +602,10 @@ class Invoice(metaclass=PoolMeta):
|
||||
|
||||
@property
|
||||
def report_net(self):
|
||||
trade = self._get_report_trade()
|
||||
if trade and getattr(trade, 'report_net', '') != '':
|
||||
return trade.report_net
|
||||
if self.lines:
|
||||
return sum(
|
||||
Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||||
for line in self._get_report_invoice_lines())
|
||||
line = self._get_report_trade_line()
|
||||
if line and line.lots:
|
||||
return sum(
|
||||
@@ -245,6 +623,21 @@ 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()
|
||||
@@ -366,9 +759,18 @@ 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 (self.description or '').upper()
|
||||
return Invoice._clean_report_description(self.description)
|
||||
|
||||
@property
|
||||
def report_rate_currency_upper(self):
|
||||
@@ -380,6 +782,11 @@ 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
|
||||
@@ -392,6 +799,12 @@ 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
|
||||
@@ -428,3 +841,96 @@ 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')
|
||||
|
||||
16
modules/purchase_trade/invoice.xml
Normal file
16
modules/purchase_trade/invoice.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
@@ -2774,40 +2774,54 @@ 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 = []
|
||||
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):
|
||||
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):
|
||||
# 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([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'
|
||||
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'
|
||||
|
||||
def default_message(self, fields):
|
||||
return {
|
||||
@@ -3154,37 +3168,55 @@ 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 = 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,
|
||||
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,
|
||||
'product': product,
|
||||
'shipment_in': sh_in,
|
||||
'shipment_internal': sh_int,
|
||||
@@ -3359,4 +3391,4 @@ class ContractDetail(ModelView):
|
||||
if self.del_period:
|
||||
self.from_del = self.del_period.beg_date
|
||||
self.to_del = self.del_period.end_date
|
||||
|
||||
|
||||
|
||||
@@ -7,17 +7,74 @@ 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')
|
||||
|
||||
def get_percent(self,name):
|
||||
return 2
|
||||
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()
|
||||
|
||||
class PartyExecutionSla(ModelSQL,ModelView):
|
||||
"Party Execution Sla"
|
||||
@@ -55,8 +112,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 %")
|
||||
@@ -65,13 +122,25 @@ 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_sla_cost(self,location):
|
||||
if self.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:
|
||||
for sla in self.sla:
|
||||
SlaPlace = Pool().get('party.execution.place')
|
||||
sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)])
|
||||
@@ -111,4 +180,4 @@ class Party(metaclass=PoolMeta):
|
||||
pc.category = cat[0].id
|
||||
PartyCategory.save([pc])
|
||||
return p
|
||||
|
||||
|
||||
|
||||
@@ -384,16 +384,22 @@ 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
|
||||
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 ''
|
||||
|
||||
@property
|
||||
def report_payment_date(self):
|
||||
@@ -543,14 +549,16 @@ 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
|
||||
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])
|
||||
#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])
|
||||
if line.price_type == 'efp':
|
||||
if line.derivatives:
|
||||
for d in line.derivatives:
|
||||
@@ -1018,10 +1026,21 @@ class QualityAnalysis(ModelSQL,ModelView):
|
||||
))
|
||||
return " | ".join(filter(None, values))
|
||||
|
||||
class Line(metaclass=PoolMeta):
|
||||
__name__ = 'purchase.line'
|
||||
|
||||
quantity_theorical = fields.Numeric("Contractual Qt", digits='unit', readonly=False)
|
||||
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)
|
||||
price_type = fields.Selection([
|
||||
('cash', 'Cash Price'),
|
||||
('priced', 'Priced'),
|
||||
@@ -1076,18 +1095,23 @@ 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')),
|
||||
})
|
||||
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')),
|
||||
})
|
||||
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'])
|
||||
premium = fields.Numeric("Premium/Discount",digits='unit')
|
||||
fee_ = fields.Many2One('fee.fee',"Fee")
|
||||
fee_ = fields.Many2One('fee.fee',"Fee")
|
||||
pricing_rule = fields.Text("Pricing description")
|
||||
|
||||
attributes = fields.Dict(
|
||||
'product.attribute', 'Attributes',
|
||||
@@ -1125,9 +1149,16 @@ class Line(metaclass=PoolMeta):
|
||||
('umpire', 'Umpire'),
|
||||
], "Type")
|
||||
|
||||
@classmethod
|
||||
def default_finished(cls):
|
||||
return False
|
||||
@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
|
||||
|
||||
|
||||
@fields.depends('product')
|
||||
@@ -1204,49 +1235,148 @@ 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_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:
|
||||
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:
|
||||
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)
|
||||
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)
|
||||
|
||||
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
|
||||
@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()
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
|
||||
@@ -319,102 +319,296 @@ class Sale(metaclass=PoolMeta):
|
||||
def default_tol_min(cls):
|
||||
return 0
|
||||
|
||||
@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
|
||||
@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
|
||||
else:
|
||||
''
|
||||
|
||||
@property
|
||||
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
|
||||
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
|
||||
|
||||
@property
|
||||
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 ''
|
||||
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
|
||||
|
||||
@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
|
||||
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 ''
|
||||
|
||||
@property
|
||||
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
|
||||
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
|
||||
|
||||
@property
|
||||
def report_shipment(self):
|
||||
@@ -547,12 +741,14 @@ 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
|
||||
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 == '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 == 'efp':
|
||||
if line.derivatives:
|
||||
for d in line.derivatives:
|
||||
@@ -564,13 +760,24 @@ class PriceComposition(metaclass=PoolMeta):
|
||||
|
||||
sale_line = fields.Many2One('sale.line',"Sale line")
|
||||
|
||||
class SaleLine(metaclass=PoolMeta):
|
||||
__name__ = 'sale.line'
|
||||
|
||||
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||
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")
|
||||
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=True)
|
||||
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=False)
|
||||
premium = fields.Numeric("Premium/Discount",digits='unit')
|
||||
price_type = fields.Selection([
|
||||
('cash', 'Cash Price'),
|
||||
@@ -621,16 +828,20 @@ 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')),
|
||||
})
|
||||
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')),
|
||||
})
|
||||
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'])
|
||||
premium = fields.Numeric("Premium/Discount",digits='unit')
|
||||
fee_ = fields.Many2One('fee.fee',"Fee")
|
||||
|
||||
@@ -671,14 +882,22 @@ class SaleLine(metaclass=PoolMeta):
|
||||
pricing_rule = self.pricing_rule
|
||||
return pricing_rule
|
||||
|
||||
@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
|
||||
@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)
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_attribute_set(self, name=None):
|
||||
@@ -757,44 +976,141 @@ 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_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:
|
||||
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:
|
||||
for d in self.derivatives:
|
||||
return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True)
|
||||
return self.get_price()
|
||||
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()
|
||||
|
||||
def check_from_to(self,tr):
|
||||
if tr.pricing_period:
|
||||
@@ -929,28 +1245,77 @@ class SaleLine(metaclass=PoolMeta):
|
||||
Pricing.save([p])
|
||||
index += 1
|
||||
|
||||
# @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 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 delete(cls, lines):
|
||||
@@ -989,14 +1354,15 @@ 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')
|
||||
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')
|
||||
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:
|
||||
for tr in pc.triggers:
|
||||
line.check_from_to(tr)
|
||||
line.check_pricing()
|
||||
@@ -1033,18 +1399,22 @@ class SaleLine(metaclass=PoolMeta):
|
||||
fl.sale_line = line.id
|
||||
FeeLots.save([fl])
|
||||
|
||||
#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)
|
||||
#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)
|
||||
|
||||
class SaleCreatePurchase(Wizard):
|
||||
"Create mirror purchase"
|
||||
@@ -1182,11 +1552,12 @@ 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'),
|
||||
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'),
|
||||
Avg(val.mtm_price).as_('r_mtm_price'),
|
||||
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,
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from trytond.pool import Pool
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.exceptions import UserError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,16 +36,11 @@ class ContractFactory:
|
||||
Date = pool.get('ir.date')
|
||||
|
||||
created = []
|
||||
|
||||
base_contract = (
|
||||
ct.lot.sale_line.sale
|
||||
if type_ == 'Purchase'
|
||||
else ct.lot.line.purchase
|
||||
)
|
||||
sources = cls._get_sources(ct, type_)
|
||||
base_contract = cls._get_base_contract(sources, ct, type_)
|
||||
|
||||
for c in contracts:
|
||||
contract = Purchase() if type_ == 'Purchase' else Sale()
|
||||
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
|
||||
|
||||
# ---------- CONTRACT ----------
|
||||
parts = c.currency_unit.split("_")
|
||||
@@ -79,33 +75,34 @@ class ContractFactory:
|
||||
|
||||
contract.save()
|
||||
|
||||
# ---------- 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_sources = cls._get_line_sources(c, sources, ct)
|
||||
for source in line_sources:
|
||||
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
|
||||
|
||||
if type_ == 'Purchase':
|
||||
line.purchase = contract.id
|
||||
else:
|
||||
line.sale = contract.id
|
||||
# ---------- 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)
|
||||
|
||||
cls._apply_price(line, c, parts)
|
||||
if type_ == 'Purchase':
|
||||
line.purchase = contract.id
|
||||
else:
|
||||
line.sale = contract.id
|
||||
|
||||
line.del_period = c.del_period
|
||||
line.from_del = c.from_del
|
||||
line.to_del = c.to_del
|
||||
cls._apply_price(line, c, parts)
|
||||
cls._apply_delivery(line, c, source)
|
||||
|
||||
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, ct, type_)
|
||||
if ct.matched:
|
||||
cls._create_lot(line, c, source, type_)
|
||||
|
||||
created.append(contract)
|
||||
|
||||
@@ -155,12 +152,124 @@ 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, ct, type_):
|
||||
def _create_lot(cls, line, c, source, type_):
|
||||
pool = Pool()
|
||||
Lot = pool.get('lot.lot')
|
||||
LotQtHist = pool.get('lot.qt.hist')
|
||||
@@ -192,10 +301,9 @@ class ContractFactory:
|
||||
|
||||
lot.save()
|
||||
|
||||
vlot = ct.lot
|
||||
shipment_origin = cls._get_shipment_origin(ct)
|
||||
|
||||
qt = c.quantity
|
||||
vlot = source['lot']
|
||||
shipment_origin = source.get('shipment_origin')
|
||||
qt = source['quantity']
|
||||
|
||||
if type_ == 'Purchase':
|
||||
if not lot.updateVirtualPart(qt, shipment_origin, vlot):
|
||||
|
||||
@@ -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,8 +23,10 @@ import io
|
||||
import base64
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
import html
|
||||
import re
|
||||
import html
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.modules.stock.shipment import SupplierShipping as BaseSupplierShipping
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -387,8 +389,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')
|
||||
@@ -439,9 +441,10 @@ class ShipmentIn(metaclass=PoolMeta):
|
||||
'shipment',
|
||||
'Container'
|
||||
)
|
||||
shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
|
||||
controller = fields.Many2One('party.party',"Controller")
|
||||
controller_target = fields.Char("Targeted controller")
|
||||
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")
|
||||
send_instruction = fields.Boolean("Send instruction")
|
||||
instructions = fields.Text("Instructions")
|
||||
add_bl = fields.Boolean("Add BL")
|
||||
@@ -459,9 +462,317 @@ class ShipmentIn(metaclass=PoolMeta):
|
||||
'send': {},
|
||||
})
|
||||
|
||||
def get_vessel_type(self,name=None):
|
||||
if self.vessel:
|
||||
return self.vessel.vessel_type
|
||||
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')
|
||||
|
||||
@staticmethod
|
||||
def _format_report_quantity(value, digits='0.001'):
|
||||
if value in (None, ''):
|
||||
return ''
|
||||
quantity = Decimal(str(value or 0)).quantize(Decimal(digits))
|
||||
text = format(quantity, 'f')
|
||||
return text.rstrip('0').rstrip('.') or '0'
|
||||
|
||||
def _get_report_trade(self):
|
||||
line = self._get_report_trade_line()
|
||||
if not line:
|
||||
return None
|
||||
return getattr(line, 'sale', None) or getattr(line, 'purchase', None)
|
||||
|
||||
def _get_report_weight_totals(self):
|
||||
net = Decimal('0')
|
||||
gross = Decimal('0')
|
||||
for move in (self.incoming_moves or self.moves or []):
|
||||
lot = getattr(move, 'lot', None)
|
||||
if lot:
|
||||
lot_net = (
|
||||
lot.get_current_quantity()
|
||||
if hasattr(lot, 'get_current_quantity')
|
||||
else lot.get_current_quantity_converted()
|
||||
if hasattr(lot, 'get_current_quantity_converted')
|
||||
else getattr(move, 'quantity', 0)
|
||||
)
|
||||
lot_gross = (
|
||||
lot.get_current_gross_quantity()
|
||||
if hasattr(lot, 'get_current_gross_quantity')
|
||||
else lot_net
|
||||
)
|
||||
net += Decimal(str(lot_net or 0))
|
||||
gross += Decimal(str(lot_gross or 0))
|
||||
else:
|
||||
quantity = Decimal(str(getattr(move, 'quantity', 0) or 0))
|
||||
net += quantity
|
||||
gross += quantity
|
||||
return net, gross
|
||||
|
||||
@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)
|
||||
|
||||
@property
|
||||
def report_packing_product_class(self):
|
||||
return self.report_product_name
|
||||
|
||||
@property
|
||||
def report_packing_contract_number(self):
|
||||
trade = self._get_report_trade()
|
||||
return (
|
||||
getattr(trade, 'reference', None)
|
||||
or getattr(trade, 'number', None)
|
||||
or self.reference
|
||||
or self.number
|
||||
or '')
|
||||
|
||||
@property
|
||||
def report_packing_invoice_qty(self):
|
||||
quantity = self.quantity if self.quantity not in (None, '') else 0
|
||||
return self._format_report_quantity(quantity)
|
||||
|
||||
@property
|
||||
def report_packing_invoice_qty_unit(self):
|
||||
unit = self.unit
|
||||
return (
|
||||
getattr(unit, 'symbol', None)
|
||||
or getattr(unit, 'rec_name', None)
|
||||
or '')
|
||||
|
||||
@property
|
||||
def report_packing_origin(self):
|
||||
trade = self._get_report_trade()
|
||||
return (
|
||||
getattr(trade, 'product_origin', None)
|
||||
or getattr(self.from_location, 'name', None)
|
||||
or '')
|
||||
|
||||
@property
|
||||
def report_packing_product(self):
|
||||
return self.report_product_name
|
||||
|
||||
@property
|
||||
def report_packing_counterparty_name(self):
|
||||
trade = self._get_report_trade()
|
||||
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_packing_ship_name(self):
|
||||
if self.vessel and self.vessel.vessel_name:
|
||||
return self.vessel.vessel_name
|
||||
return self.transport_type or ''
|
||||
|
||||
@property
|
||||
def report_packing_loading_port(self):
|
||||
return getattr(self.from_location, 'name', '') or ''
|
||||
|
||||
@property
|
||||
def report_packing_destination_port(self):
|
||||
return getattr(self.to_location, 'name', '') or ''
|
||||
|
||||
@property
|
||||
def report_packing_chunk_number(self):
|
||||
return self.bl_number or self.number or ''
|
||||
|
||||
@property
|
||||
def report_packing_chunk_date(self):
|
||||
if self.bl_date:
|
||||
return self.bl_date.strftime('%d-%m-%Y')
|
||||
return ''
|
||||
|
||||
@property
|
||||
def report_packing_gross_weight(self):
|
||||
_, gross = self._get_report_weight_totals()
|
||||
return self._format_report_quantity(gross)
|
||||
|
||||
@property
|
||||
def report_packing_net_weight(self):
|
||||
net, _ = self._get_report_weight_totals()
|
||||
return self._format_report_quantity(net)
|
||||
|
||||
def get_rec_name(self, name=None):
|
||||
if self.number:
|
||||
@@ -487,16 +798,26 @@ 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])
|
||||
for c in controllers:
|
||||
if c.party.IsAvailableForControl(self):
|
||||
return c.party
|
||||
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_instructions_html(self,inv_date,inv_nb):
|
||||
vessel = self.vessel.vessel_name if self.vessel else ""
|
||||
@@ -1878,7 +2199,7 @@ class Revaluate(Wizard):
|
||||
|
||||
return 'end'
|
||||
|
||||
class RevaluateStart(ModelView):
|
||||
class RevaluateStart(ModelView):
|
||||
"Revaluate"
|
||||
__name__ = 'account.revaluate.start'
|
||||
revaluation_date = fields.Date(
|
||||
@@ -1892,5 +2213,73 @@ class RevaluateStart(ModelView):
|
||||
return Date.today()
|
||||
|
||||
@classmethod
|
||||
def default_delete_after(cls):
|
||||
return False
|
||||
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')
|
||||
|
||||
|
||||
class ShipmentPackingListReport(ShipmentTemplateReportMixin, BaseSupplierShipping):
|
||||
__name__ = 'stock.shipment.in.packing_list'
|
||||
|
||||
@classmethod
|
||||
def _resolve_configured_report_path(cls, action):
|
||||
return cls._resolve_template_path(
|
||||
'shipment_packing_list_report_template', 'stock')
|
||||
|
||||
@@ -56,10 +56,34 @@ 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.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.report" id="report_shipment_in_packing_list">
|
||||
<field name="name">Packing List</field>
|
||||
<field name="model">stock.shipment.in</field>
|
||||
<field name="report_name">stock.shipment.in.packing_list</field>
|
||||
<field name="report">stock/packing_list.fodt</field>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="report_shipment_in_packing_list_keyword">
|
||||
<field name="keyword">form_print</field>
|
||||
<field name="model">stock.shipment.in,-1</field>
|
||||
<field name="action" ref="report_shipment_in_packing_list"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.wizard" id="act_update_sof">
|
||||
<field name="name">Update with SoF PDF</field>
|
||||
@@ -126,4 +150,4 @@ this repository contains the full copyright notices and license terms. -->
|
||||
id="menu_revaluate"/>
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
</tryton>
|
||||
|
||||
@@ -6,6 +6,9 @@ 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):
|
||||
@@ -70,5 +73,815 @@ 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')
|
||||
packing_report = Pool().get('stock.shipment.in.packing_list', 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',
|
||||
shipment_packing_list_report_template='packing_list_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')
|
||||
self.assertEqual(
|
||||
packing_report._resolve_configured_report_path({
|
||||
'name': 'Packing List',
|
||||
'report': 'stock/packing_list.fodt',
|
||||
}),
|
||||
'stock/packing_list_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
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[tryton]
|
||||
version=7.2.7
|
||||
depends:
|
||||
ir
|
||||
purchase
|
||||
depends:
|
||||
ir
|
||||
price
|
||||
purchase
|
||||
sale
|
||||
account_invoice
|
||||
stock
|
||||
@@ -10,12 +11,13 @@ depends:
|
||||
lot
|
||||
document_incoming
|
||||
incoterm
|
||||
xml:
|
||||
purchase.xml
|
||||
sale.xml
|
||||
lot.xml
|
||||
pricing.xml
|
||||
stock.xml
|
||||
xml:
|
||||
purchase.xml
|
||||
sale.xml
|
||||
lot.xml
|
||||
pricing.xml
|
||||
configuration.xml
|
||||
stock.xml
|
||||
workflow.xml
|
||||
lc.xml
|
||||
optional.xml
|
||||
@@ -31,7 +33,8 @@ xml:
|
||||
global_reporting.xml
|
||||
derivative.xml
|
||||
valuation.xml
|
||||
weight_report.xml
|
||||
dimension.xml
|
||||
backtoback.xml
|
||||
account.xml
|
||||
weight_report.xml
|
||||
dimension.xml
|
||||
backtoback.xml
|
||||
invoice.xml
|
||||
account.xml
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,25 @@
|
||||
<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>
|
||||
@@ -36,6 +56,10 @@
|
||||
<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"/>
|
||||
@@ -43,9 +67,18 @@
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="purchase_trade.menu_global_reporting"
|
||||
sequence="120"
|
||||
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"
|
||||
action="act_valuation_form"
|
||||
id="menu_valuation_form"/>
|
||||
</data>
|
||||
</tryton>
|
||||
</tryton>
|
||||
|
||||
5
modules/purchase_trade/view/configuration_form.xml
Normal file
5
modules/purchase_trade/view/configuration_form.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0"?>
|
||||
<form col="4">
|
||||
<label name="pricing_rule"/>
|
||||
<field name="pricing_rule" colspan="3"/>
|
||||
</form>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<tree editable="1">
|
||||
<tree>
|
||||
<field name="r_lot_type" width="80">
|
||||
<prefix name="qt_icon"/>
|
||||
</field>
|
||||
|
||||
@@ -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="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/>
|
||||
<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/>
|
||||
<label name="tol_min"/>
|
||||
<field name="tol_min"/>
|
||||
<label name="tol_min_v"/>
|
||||
@@ -93,11 +93,15 @@ 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>
|
||||
</notebook>
|
||||
</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="Estimated dates" col="4" id="estimated">
|
||||
<field name="estimated_date" />
|
||||
</page>
|
||||
@@ -105,4 +109,4 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="optional" />
|
||||
</page>
|
||||
</xpath>
|
||||
</data>
|
||||
</data>
|
||||
|
||||
@@ -7,4 +7,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="from_del"/>
|
||||
<field name="to_del"/>
|
||||
</xpath>
|
||||
</data>
|
||||
<xpath expr="//field[@name='unit_price']" position="after">
|
||||
<field name="premium"/>
|
||||
</xpath>
|
||||
</data>
|
||||
|
||||
@@ -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="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/>
|
||||
<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/>
|
||||
<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>
|
||||
|
||||
@@ -7,4 +7,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="from_del"/>
|
||||
<field name="to_del"/>
|
||||
</xpath>
|
||||
</data>
|
||||
<xpath expr="//field[@name='unit_price']" position="after">
|
||||
<field name="premium"/>
|
||||
</xpath>
|
||||
</data>
|
||||
|
||||
@@ -137,5 +137,9 @@ 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>
|
||||
|
||||
34
modules/purchase_trade/view/template_configuration_form.xml
Normal file
34
modules/purchase_trade/view/template_configuration_form.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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"/>
|
||||
<label name="shipment_packing_list_report_template"/>
|
||||
<field name="shipment_packing_list_report_template" colspan="3"/>
|
||||
</form>
|
||||
@@ -12,5 +12,6 @@
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<form col="4">
|
||||
<label name="dimension"/>
|
||||
<field name="dimension"/>
|
||||
<label name="value"/>
|
||||
<field name="value"/>
|
||||
</form>
|
||||
@@ -0,0 +1,4 @@
|
||||
<tree editable="1">
|
||||
<field name="dimension"/>
|
||||
<field name="value"/>
|
||||
</tree>
|
||||
@@ -0,0 +1,3 @@
|
||||
<form>
|
||||
<field name="message"/>
|
||||
</form>
|
||||
18
modules/purchase_trade/view/valuation_process_start_form.xml
Normal file
18
modules/purchase_trade/view/valuation_process_start_form.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
@@ -11,5 +11,6 @@ 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>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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>
|
||||
|
||||
|
||||
@@ -79,9 +79,16 @@
|
||||
<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>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
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__)
|
||||
@@ -49,21 +52,127 @@ 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({
|
||||
# 'import_json': {},
|
||||
# 'export_json': {},
|
||||
# })
|
||||
cls._buttons.update({
|
||||
'create_remote_weight_reports': {},
|
||||
})
|
||||
|
||||
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):
|
||||
@@ -200,4 +309,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]
|
||||
|
||||
@@ -103,18 +103,12 @@
|
||||
</record> -->
|
||||
|
||||
<!-- Model Buttons -->
|
||||
<!-- <record model="ir.model.button" id="weight_report_import_button">
|
||||
<record model="ir.model.button" id="weight_report_create_remote_button">
|
||||
<field name="model">weight.report</field>
|
||||
<field name="name">import_json</field>
|
||||
<field name="string">Import JSON</field>
|
||||
<field name="name">create_remote_weight_reports</field>
|
||||
<field name="string">Create Remote WRs</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"
|
||||
@@ -136,4 +130,4 @@
|
||||
sequence="30"
|
||||
id="menu_gr_weight_reports"/> -->
|
||||
</data>
|
||||
</tryton>
|
||||
</tryton>
|
||||
|
||||
@@ -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"><sale.crop.name></text:placeholder>
|
||||
<text:placeholder text:placeholder-type="text"><sale.report_crop_name></text:placeholder>
|
||||
</text:span>
|
||||
</text:p>
|
||||
</table:table-cell>
|
||||
@@ -4091,10 +4091,19 @@
|
||||
<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"><sum(line.quantity for line in sale.lines)></text:placeholder><text:s /><text:placeholder text:placeholder-type="text"><sale.lines[0].unit.rec_name.upper() if sale.lines and sale.lines[0].unit else ''></text:placeholder><text:s /><text:span text:style-name="T23">(</text:span><text:span text:style-name="T23"><text:placeholder text:placeholder-type="text"><sale.report_qt></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"><sale.report_total_quantity></text:placeholder><text:s /><text:placeholder text:placeholder-type="text"><sale.report_quantity_unit_upper></text:placeholder><text:s /><text:span text:style-name="T23">(</text:span><text:span text:style-name="T23"><text:placeholder text:placeholder-type="text"><sale.report_qt></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"><sale.report_nb_bale></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P39">
|
||||
<text:placeholder text:placeholder-type="text"><for each="line in sale.report_quantity_lines.splitlines()"></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P39">
|
||||
<text:placeholder text:placeholder-type="text"><line></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P39">
|
||||
<text:placeholder text:placeholder-type="text"></for></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P39" />
|
||||
</table:table-cell>
|
||||
</table:table-row>
|
||||
@@ -4110,27 +4119,13 @@
|
||||
</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"><sale.lines[0].linked_currency.rec_name.upper() if sale.lines[0].linked_currency else sale.currency.rec_name.upper()></text:placeholder>
|
||||
<text:s />
|
||||
<text:placeholder text:placeholder-type="text"><sale.lines[0].linked_price if sale.lines[0].linked_price else sale.lines[0].unit_price></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"><sale.lines[0].linked_unit.rec_name.upper() if sale.lines[0].linked_unit else sale.lines[0].unit.rec_name.upper()></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"><sale.report_price></text:placeholder>
|
||||
</text:span>
|
||||
<text:span text:style-name="T23">) </text:span>
|
||||
<text:span text:style-name="T23">
|
||||
<text:placeholder text:placeholder-type="text"><sale.lines[0].get_pricing_text></text:placeholder>
|
||||
</text:span>
|
||||
<text:placeholder text:placeholder-type="text"><for each="line in sale.report_price_lines.splitlines()"></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P56">
|
||||
<text:placeholder text:placeholder-type="text"><line></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P56">
|
||||
<text:placeholder text:placeholder-type="text"></for></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P37" />
|
||||
</table:table-cell>
|
||||
@@ -4199,12 +4194,18 @@
|
||||
</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"><for each="line in sale.report_shipment_periods.splitlines()"></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P36">
|
||||
<text:s />
|
||||
<text:span text:style-name="T24">
|
||||
<text:placeholder text:placeholder-type="text"><sale.lines[0].del_period.description if sale.lines[0].del_period else ''></text:placeholder>
|
||||
<text:placeholder text:placeholder-type="text"><line></text:placeholder>
|
||||
</text:span>
|
||||
</text:p>
|
||||
<text:p text:style-name="P36">
|
||||
<text:placeholder text:placeholder-type="text"></for></text:placeholder>
|
||||
</text:p>
|
||||
<text:p text:style-name="P34" />
|
||||
</table:table-cell>
|
||||
</table:table-row>
|
||||
@@ -4389,4 +4390,4 @@
|
||||
</text:p>
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document>
|
||||
</office:document>
|
||||
|
||||
@@ -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"><sale.lines[0].product.name if sale.lines and sale.lines[0].product else ''></text:placeholder></text:span></text:p>
|
||||
<text:p text:style-name="P30" loext:marker-style-name="T26"/>
|
||||
<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"><sale.report_product_name or ''></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"><sale.report_product_description or ''></text:placeholder></text:span></text:p>
|
||||
<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"><sale.lines[0].del_period.month_name if sale.lines and sale.lines[0].del_period else ''></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"><sale.report_delivery_period_description or ''></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"><sale.tol_min></text:placeholder><text:s/>%</text:p>
|
||||
<text:p text:style-name="P2"/>
|
||||
@@ -1803,6 +1803,13 @@
|
||||
<text:p text:style-name="P2"/>
|
||||
<text:p text:style-name="P7">PAYMENT TERMS:<text:tab/><text:placeholder text:placeholder-type="text"><sale.payment_term.description if sale.payment_term else ''></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"/>
|
||||
@@ -1814,4 +1821,4 @@
|
||||
<text:p text:style-name="Standard"><text:placeholder text:placeholder-type="text"></for></text:placeholder></text:p>
|
||||
</office:text>
|
||||
</office:body>
|
||||
</office:document>
|
||||
</office:document>
|
||||
|
||||
1074
modules/stock/insurance.fodt
Normal file
1074
modules/stock/insurance.fodt
Normal file
File diff suppressed because it is too large
Load Diff
1698
modules/stock/packing_list.fodt
Normal file
1698
modules/stock/packing_list.fodt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user