From eaae2e5b40ebfc14b7314c2275b195898ad86271 Mon Sep 17 00:00:00 2001 From: "AzureAD\\SylvainDUVERNAY" Date: Thu, 7 May 2026 15:02:15 +0200 Subject: [PATCH] Document rules and enforce price value uniqueness --- modules/price/message.xml | 3 + modules/price/price_value.py | 36 +++- .../backlog/market-price-import-backlog.md | 79 ++++++++ modules/purchase_trade/docs/business-rules.md | 174 +++--------------- .../docs/documentation-management.md | 82 +++++++++ .../docs/rules/invoice-freight.md | 50 +++++ .../docs/rules/lot-navigation.md | 59 ++++++ .../purchase_trade/docs/rules/lot-quantity.md | 106 +++++++++++ .../docs/rules/market-price-import.md | 171 +++++++++++++++++ modules/purchase_trade/tests/test_module.py | 33 ++++ modules/trade_finance/AGENTS.md | 7 + 11 files changed, 647 insertions(+), 153 deletions(-) create mode 100644 modules/purchase_trade/docs/backlog/market-price-import-backlog.md create mode 100644 modules/purchase_trade/docs/documentation-management.md create mode 100644 modules/purchase_trade/docs/rules/invoice-freight.md create mode 100644 modules/purchase_trade/docs/rules/lot-navigation.md create mode 100644 modules/purchase_trade/docs/rules/lot-quantity.md create mode 100644 modules/purchase_trade/docs/rules/market-price-import.md create mode 100644 modules/trade_finance/AGENTS.md diff --git a/modules/price/message.xml b/modules/price/message.xml index 4340eff..db0bfc6 100755 --- a/modules/price/message.xml +++ b/modules/price/message.xml @@ -3,6 +3,9 @@ this repository contains the full copyright notices and license terms. --> + + Only one price value is allowed for each price index and price date. + The code on party must be unique. diff --git a/modules/price/price_value.py b/modules/price/price_value.py index 7b0e93e..d9567b3 100755 --- a/modules/price/price_value.py +++ b/modules/price/price_value.py @@ -9,7 +9,7 @@ from trytond.i18n import gettext from trytond.model import ( DeactivableMixin, Index, ModelSQL, ModelView, MultiValueMixin, Unique, ValueMixin, convert_from, fields, sequence_ordered) -from trytond.model.exceptions import AccessError +from trytond.model.exceptions import AccessError, ValidationError from trytond.pool import Pool from trytond.pyson import Bool, Eval from trytond.tools import is_full_text, lstrip_wildcard @@ -38,12 +38,34 @@ class PriceValue( price_value = fields.Float("Price value") open_price = fields.Float("Open price") low_price = fields.Float("Low price") - high_price = fields.Float("High price") - - def get_price_index(self, name): - if self.price: - return self.price.price_index - return None + high_price = fields.Float("High price") + + @classmethod + def validate(cls, price_values): + super().validate(price_values) + cls.check_unique_price_date(price_values) + + @classmethod + def check_unique_price_date(cls, price_values): + domains = [] + for price_value in price_values: + if not price_value.price or not price_value.price_date: + continue + domain = [ + ('price', '=', price_value.price.id), + ('price_date', '=', price_value.price_date), + ] + if price_value.id: + domain.append(('id', '!=', price_value.id)) + domains.append(['AND'] + domain) + if domains and cls.search(['OR'] + domains, limit=1): + raise ValidationError( + gettext('price.msg_price_value_unique')) + + def get_price_index(self, name): + if self.price: + return self.price.price_index + return None class PriceValueReport( ModelSQL, ModelView): diff --git a/modules/purchase_trade/docs/backlog/market-price-import-backlog.md b/modules/purchase_trade/docs/backlog/market-price-import-backlog.md new file mode 100644 index 0000000..48d0a8c --- /dev/null +++ b/modules/purchase_trade/docs/backlog/market-price-import-backlog.md @@ -0,0 +1,79 @@ +# Backlog - Market Price Import + +Scope: code changes related only to `BR-PT-004 - Market Price Import`. + +## PT-BL-001 - Enforce uniqueness at model level + +- Status: to be tested +- Priority: high +- Source: [rules/market-price-import.md](../rules/market-price-import.md) +- Requirement: enforce uniqueness of `(price, price_date)` at model level without adding a database constraint yet. +- Work done: + - added model-level validation on `price.price_value` + - added Tryton message `price.msg_price_value_unique` + - added targeted tests for duplicate create and duplicate write validation +- Validation status: + - `git diff --check` passed + - automated Python tests still need to be run in a working Tryton Python environment +- Expected code impact: + - `modules/purchase_trade/pricing.py` + - possibly `modules/price/price_value.py` if the validation belongs on the target model +- Expected tests: + - importing a row for an existing `(price, price_date)` skips or updates according to `Overwrite existing price` + - creating/editing duplicate `price.price_value` records at model level is rejected if validation is implemented on `price.price_value` + +## PT-BL-002 - Use locale-aware slash date parsing + +- Status: open +- Priority: medium +- Source: [rules/market-price-import.md](../rules/market-price-import.md) +- Requirement: prefer the user's/default locale date format for ambiguous slash dates, then fall back to the current parsing order only if no locale preference is available. +- Current behavior: `DD/MM/YYYY` is tried before `MM/DD/YYYY`. +- Expected code impact: + - `modules/purchase_trade/pricing.py` +- Expected tests: + - slash dates follow the configured/default locale when available + - slash dates keep a deterministic fallback when locale is unavailable + - existing accepted formats still work + +## PT-BL-003 - Reject missing price_value + +- Status: open +- Priority: high +- Source: [rules/market-price-import.md](../rules/market-price-import.md) +- Requirement: `price_value` is mandatory for imported rows. Missing `price_value` must be reported as a row error. +- Current behavior: empty `price_value` is imported as an empty value. +- Expected code impact: + - `modules/purchase_trade/pricing.py` +- Expected tests: + - missing `price_value` is reported in `errors` + - missing `high_price`, `low_price`, and `open_price` remain allowed + - valid `price_value` still imports and updates correctly + +## PT-BL-004 - Detect duplicate rows inside the same Excel file + +- Status: open +- Priority: high +- Source: [rules/market-price-import.md](../rules/market-price-import.md) +- Requirement: duplicate rows for the same `(price_index, price_date)` inside the same Excel file must be reported as row errors. The duplicate row must not import or update data. +- Expected code impact: + - `modules/purchase_trade/pricing.py` +- Expected tests: + - duplicate `(price_index, price_date)` rows in the same import produce row errors + - first occurrence behavior remains unchanged + - duplicate rows do not overwrite previous imported or updated values + +## PT-BL-005 - Categorize import result errors + +- Status: open +- Priority: medium +- Source: [rules/market-price-import.md](../rules/market-price-import.md) +- Requirement: distinguish business validation errors from file, parsing, and technical errors in the import result. +- Current behavior: row errors are grouped under one `Errors` section. +- Expected code impact: + - `modules/purchase_trade/pricing.py` + - `modules/purchase_trade/view/import_prices_result_form.xml` only if the result display needs more structure +- Expected tests: + - result message separates business validation errors from parsing/technical errors + - invalid workbook and missing required columns remain blocking `UserError` + - row-level errors still let the import continue diff --git a/modules/purchase_trade/docs/business-rules.md b/modules/purchase_trade/docs/business-rules.md index 8763545..08dcc6b 100644 --- a/modules/purchase_trade/docs/business-rules.md +++ b/modules/purchase_trade/docs/business-rules.md @@ -1,8 +1,8 @@ # Business Rules - Purchase Trade Statut: `draft` -Version: `v0.2` -Derniere mise a jour: `2026-03-27` +Version: `v0.3` +Derniere mise a jour: `2026-05-07` Owner metier: `a completer` Owner technique: `a completer` @@ -13,163 +13,45 @@ Owner technique: `a completer` - Modules impactes: - `purchase_trade` - `lot` + - `price` ## 2) Glossaire - `Purchase Line`: ligne d'achat. - `quantity_theorical`: quantite theorique contractuelle de la ligne. - `Virtual Lot`: lot unique de type `virtual` rattache a une `purchase.line`. -- `lot.qt`: table des quantites ouvertes, matchées ou shippées par lot. +- `Physical Lot`: lot physique de type `physic` utilise comme pont entre achat, vente et shipment. +- `lot.qt`: table des quantites ouvertes, matchees ou shippees par lot. - `lot.qt ouvert`: enregistrement `lot.qt` avec `lot_p = virtual lot`, `lot_s = None` et sans shipment. +- `price.price`: index de prix marche. +- `price.price_value`: valeur datee d'un index de prix marche. -## 3) Regles metier +## 3) Catalogue des regles -### BR-PT-001 - Ajustement de la quantite theorique apres creation du contrat +| ID | Titre | Domaine | Detail | +| --- | --- | --- | --- | +| BR-PT-001 | Ajustement de la quantite theorique apres creation du contrat | Lot / Purchase | [rules/lot-quantity.md](rules/lot-quantity.md) | +| BR-PT-002 | Le lot physique est le pont metier entre purchase, sale et shipment | Lot / Navigation | [rules/lot-navigation.md](rules/lot-navigation.md) | +| BR-PT-003 | Le freight amount des templates facture vient du fee de shipment | Invoice / Freight | [rules/invoice-freight.md](rules/invoice-freight.md) | +| BR-PT-004 | Market Price Import | Pricing | [rules/market-price-import.md](rules/market-price-import.md) | -- Intent: conserver la coherence entre la quantite theorique de la ligne d'achat, le lot virtuel associe et les quantites ouvertes stockees dans `lot.qt`. -- Description: - - Quand `purchase.line.quantity_theorical` est modifiee apres creation du contrat, le systeme doit recalculer le delta entre l'ancienne et la nouvelle valeur. - - La regle s'applique au lot unique de type `virtual` rattache a la `purchase.line`. -- Conditions d'entree: - - Une `purchase.line` existe deja. - - Son champ `quantity_theorical` est modifie via `write`. - - Un lot `virtual` est rattache a la ligne. -- Resultat attendu: - - Si `delta > 0`: - - augmenter la quantite courante du lot `virtual` via `set_current_quantity` pour conserver l'historique `lot.qt.hist` - - augmenter le `lot.qt` ouvert existant - - si aucun `lot.qt` ouvert n'existe, en creer un nouveau avec le delta - - Si `delta < 0`: - - diminuer le `lot.qt` ouvert uniquement si la quantite ouverte disponible est suffisante - - diminuer la quantite courante du lot `virtual` du meme delta - - si aucun `lot.qt` ouvert n'existe ou si sa quantite est insuffisante, bloquer avec l'erreur `Please unlink or unmatch lot` -- Definition du `lot.qt` ouvert: - - `lot_p = virtual lot` - - `lot_s = None` - - `lot_shipment_in = None` - - `lot_shipment_internal = None` - - `lot_shipment_out = None` -- Exceptions: - - si aucun lot `virtual` n'est trouve sur la ligne, la regle ne fait rien -- Priorite: - - `bloquante` -- Source: - - `Decision metier documentee dans les commentaires de purchase_trade.purchase.Line.write` +## 4) Convention pour les nouvelles regles -### BR-PT-002 - Le lot physique est le pont metier entre purchase, sale et shipment +Voir aussi [documentation-management.md](documentation-management.md) pour le workflow complet de gestion documentaire. -- Intent: disposer d'un chemin unique et stable pour retrouver les informations logistiques et de facturation reliees a un contrat d'achat ou de vente. -- Description: - - Le lot physique (`lot_type = physic`) porte simultanement le lien vers: - - la `purchase.line` via `lot.line` - - la `sale.line` via `lot.sale_line` - - le shipment via `lot.lot_shipment_in` / `lot.lot_shipment_internal` / `lot.lot_shipment_out` - - Pour toute logique qui doit naviguer entre achat, vente, shipment et facture, il faut privilegier ce lot physique comme source de verite. -- Resultat attendu: - - depuis une facture d'achat: - - remonter a la `purchase.line` - - puis au lot physique de la ligne - - puis au shipment et aux donnees logistiques associees - - depuis une facture de vente: - - remonter a la `sale.line` - - puis au lot physique matchant qui porte aussi la `purchase.line` - - puis au shipment et aux donnees logistiques associees -- Cas d'usage typiques: - - recuperer `bl_date`, `bl_number`, `controller`, `from_location`, `to_location` - - retrouver une facture provisoire liee au lot - - retrouver des fees rattaches au shipment -- Priorite: - - `structurante` +Ajouter une ligne au catalogue puis creer une fiche detaillee dans `docs/rules/`. -### BR-PT-003 - Le freight amount des templates facture vient du fee de shipment +Structure recommandee: -- Intent: afficher dans les documents facture la vraie valeur de fret maritime rattachee au shipment du lot physique. -- Description: - - Le `FREIGHT VALUE` d'une facture ne doit pas etre pris sur la facture elle-meme. - - Il doit etre calcule a partir du `fee.fee` rattache au shipment (`shipment_in`) du lot physique relie a la facture. -- Regle de navigation: - - retrouver le lot physique pertinent depuis la facture - - retrouver son shipment - - chercher le `fee.fee` avec: - - `shipment_in = shipment.id` - - `product.name = 'Maritime freight'` - - utiliser `fee.get_amount()` comme montant de fret -- Portee: - - s'applique aussi bien aux factures d'achat qu'aux factures de vente - - cote vente, la remontee doit passer par le lot physique qui fait le lien entre `purchase.line` et `sale.line` -- Priorite: - - `importante` +- Intent +- Scope +- Inputs +- Expected Behavior +- Edge Cases +- Impacted Files +- Tests +- Open Questions -## 4) Exemples concrets - -### Exemple E1 - Augmentation simple - -- Donnees: - - `ancienne quantity_theorical = 100` - - `nouvelle quantity_theorical = 120` - - `lot.qt ouvert = 40` -- Attendu: - - lot `virtual` augmente de `20` - - `lot.qt ouvert` passe de `40` a `60` - -### Exemple E2 - Augmentation sans lot.qt ouvert - -- Donnees: - - `ancienne quantity_theorical = 100` - - `nouvelle quantity_theorical = 110` - - aucun `lot.qt` ouvert -- Attendu: - - lot `virtual` augmente de `10` - - creation d'un `lot.qt` ouvert a `10` - -### Exemple E3 - Diminution possible - -- Donnees: - - `ancienne quantity_theorical = 100` - - `nouvelle quantity_theorical = 90` - - `lot.qt ouvert = 25` -- Attendu: - - lot `virtual` diminue de `10` - - `lot.qt ouvert` passe de `25` a `15` - -### Exemple E4 - Diminution impossible - -- Donnees: - - `ancienne quantity_theorical = 100` - - `nouvelle quantity_theorical = 80` - - `lot.qt ouvert = 5` -- Attendu: - - blocage avec `Please unlink or unmatch lot` - -## 5) Impact code attendu - -- Fichiers Python concernes: - - `modules/purchase_trade/purchase.py` - - `modules/purchase_trade/lot.py` - -## 6) Strategie de tests - -Pour cette regle, couvrir au minimum: - -- augmentation avec `lot.qt` ouvert existant -- augmentation sans `lot.qt` ouvert -- diminution possible -- diminution impossible avec erreur - - -### BR-PT-004 - Market Price Import - -- Goal: import market prices from an Excel file -- Description: after having selected an Excel file with the following columns: - - price_index: name of the market - - price_date: date of the price - - high price: high price - - low price: low price - - open price: open price - - price value: price value -- Expected behavior: - - Check if price_index exists prior to impport related prices - - If price_index does not exist, check if option "Create price index is missing" has been checked in the input screen. If checked then try to create first the price index (table: price_price). If option is not checked, then ignore the price_index and mark it as "skipped" in the output results of the import. - - Check if price_date for the price_index already exists. - - If price_date does not exist, then import the price. If price_date already exists then check if option "Overwrite existing price" is checked in the input screen. If checked, update the price for price_index and price_date. If option is not checked, then ignore the price_date and mark as "skipped" in the output. +Les regles courtes peuvent rester dans ce catalogue temporairement, mais toute regle avec impact code, tests, edge cases ou flux transverse doit avoir son fichier dedie. +Dans les sections `Open Questions`, prefixer chaque question avec `Q:` et chaque reponse documentee avec `A:`. diff --git a/modules/purchase_trade/docs/documentation-management.md b/modules/purchase_trade/docs/documentation-management.md new file mode 100644 index 0000000..24471fe --- /dev/null +++ b/modules/purchase_trade/docs/documentation-management.md @@ -0,0 +1,82 @@ +# Documentation Management - Purchase Trade + +This guide records the documentation workflow to apply for future `purchase_trade` developments. + +## Rule Documentation Structure + +- Keep `business-rules.md` as the entry point and rule catalog. +- Store detailed business rules in `docs/rules/`, one file per substantial rule. +- Use stable business rule IDs such as `BR-PT-004`. +- Add every new detailed rule to the catalog in `business-rules.md`. +- Keep rule files focused on business behavior, expected outcomes, edge cases, impacted files, tests, and open questions. + +## Rule File Template + +Use this structure for detailed rule files when applicable: + +- Intent +- Scope +- Inputs +- Date and Numeric Parsing, when relevant +- Expected Behavior +- Defaults or Derived Values, when relevant +- Result Reporting, when relevant +- Edge Cases +- Impacted Files +- Tests +- Open Questions + +## Open Questions + +- In `Open Questions`, prefix every question with `Q:`. +- Prefix every documented answer with `A:`. +- When an answer differs from current code behavior, add a short `Comment:` explaining that a code change is required. +- Once all questions have answers, keep the section as decision history unless the rule becomes noisy. + +Example: + +```md +- Q: Should missing `price_value` be allowed? +- A: Missing `price_value` is not allowed. Report the row as an error. +- Comment: current code allows empty `price_value`. Implementing this answer requires a code change. +``` + +## Backlog From Documentation + +- When answered questions imply code changes, create a dedicated backlog file for the feature or rule. +- Store all backlog files in `docs/backlog/`. +- Name backlog files by feature, for example `docs/backlog/market-price-import-backlog.md`. +- Do not use a generic module backlog when the changes belong to one feature. +- After coding a backlog item, update that item with the work done and mark its `Status` as `to be tested` until the targeted validation has run successfully in the proper environment. +- After committing implemented backlog work, record the implementation commit hash in the relevant backlog item for future reference. +- Each backlog item should include: + - stable ID + - status + - implementation commit, once committed + - priority + - source rule file + - requirement + - work done, once implemented + - validation status, once attempted + - current behavior, when useful + - expected code impact + - expected tests + +## Review Workflow + +For future development work: + +1. Read the relevant rule file before changing code. +2. Review the current code and tests related to the rule. +3. Update the rule documentation if code review reveals missing behavior, edge cases, defaults, or test gaps. +4. Record unresolved decisions as `Q:` entries. +5. Record accepted decisions as `A:` entries. +6. Convert accepted decisions that require code changes into backlog items. +7. Implement code only after the requested documentation/specification step is complete, or after explicit approval when the user asks to wait. + +## Scope Discipline + +- Keep documentation changes close to the affected module and feature. +- Do not mix unrelated features in one rule file or one backlog file. +- Link or list impacted files so future agents can work in a narrow code scope. +- Keep tests listed near the rule they validate. diff --git a/modules/purchase_trade/docs/rules/invoice-freight.md b/modules/purchase_trade/docs/rules/invoice-freight.md new file mode 100644 index 0000000..eea2bab --- /dev/null +++ b/modules/purchase_trade/docs/rules/invoice-freight.md @@ -0,0 +1,50 @@ +# BR-PT-003 - Le freight amount des templates facture vient du fee de shipment + +## Intent + +Afficher dans les documents facture la vraie valeur de fret maritime rattachee au shipment du lot physique. + +## Scope + +- Domaine: `purchase_trade` +- Flux: templates facture achat et vente +- Priorite: `importante` + +## Expected Behavior + +Le `FREIGHT VALUE` d'une facture ne doit pas etre pris sur la facture elle-meme. + +Il doit etre calcule a partir du `fee.fee` rattache au shipment (`shipment_in`) du lot physique relie a la facture. + +Regle de navigation: + +- retrouver le lot physique pertinent depuis la facture +- retrouver son shipment +- chercher le `fee.fee` avec: + - `shipment_in = shipment.id` + - `product.name = 'Maritime freight'` +- utiliser `fee.get_amount()` comme montant de fret + +La regle s'applique aussi bien aux factures d'achat qu'aux factures de vente. + +Cote vente, la remontee doit passer par le lot physique qui fait le lien entre `purchase.line` et `sale.line`. + +## Impacted Files + +- `modules/purchase_trade/invoice.py` +- `modules/purchase_trade/fee.py` +- `modules/purchase_trade/lot.py` +- Templates facture `.fodt` concernes par `FREIGHT VALUE` + +## Tests + +Couvrir les changements au plus proche du flux modifie: + +- facture achat avec lot physique, shipment et fee `Maritime freight` +- facture vente qui remonte au lot physique puis au shipment +- cas sans fee maritime +- cas sans lot physique pertinent + +## Open Questions + +- Preciser les templates exacts quand une demande cible un document facture particulier. diff --git a/modules/purchase_trade/docs/rules/lot-navigation.md b/modules/purchase_trade/docs/rules/lot-navigation.md new file mode 100644 index 0000000..b1feb68 --- /dev/null +++ b/modules/purchase_trade/docs/rules/lot-navigation.md @@ -0,0 +1,59 @@ +# BR-PT-002 - Le lot physique est le pont metier entre purchase, sale et shipment + +## Intent + +Disposer d'un chemin unique et stable pour retrouver les informations logistiques et de facturation reliees a un contrat d'achat ou de vente. + +## Scope + +- Domaine: `purchase_trade` +- Flux: navigation metier entre achat, vente, shipment et facture +- Priorite: `structurante` + +## Expected Behavior + +Le lot physique (`lot_type = physic`) porte simultanement le lien vers: + +- la `purchase.line` via `lot.line` +- la `sale.line` via `lot.sale_line` +- le shipment via `lot.lot_shipment_in` / `lot.lot_shipment_internal` / `lot.lot_shipment_out` + +Pour toute logique qui doit naviguer entre achat, vente, shipment et facture, il faut privilegier ce lot physique comme source de verite. + +Depuis une facture d'achat: + +- remonter a la `purchase.line` +- puis au lot physique de la ligne +- puis au shipment et aux donnees logistiques associees + +Depuis une facture de vente: + +- remonter a la `sale.line` +- puis au lot physique matchant qui porte aussi la `purchase.line` +- puis au shipment et aux donnees logistiques associees + +## Typical Use Cases + +- recuperer `bl_date`, `bl_number`, `controller`, `from_location`, `to_location` +- retrouver une facture provisoire liee au lot +- retrouver des fees rattaches au shipment + +## Impacted Files + +- `modules/purchase_trade/lot.py` +- `modules/purchase_trade/purchase.py` +- `modules/purchase_trade/sale.py` +- `modules/purchase_trade/invoice.py` +- Templates facture relies aux donnees logistiques, si le flux documentaire est concerne. + +## Tests + +Couvrir les changements au plus proche du flux modifie: + +- navigation achat vers lot physique puis shipment +- navigation vente vers lot physique puis shipment +- cas sans lot physique pertinent + +## Open Questions + +- Documenter au cas par cas les champs exposes au reporting facture quand ils dependent de cette navigation. diff --git a/modules/purchase_trade/docs/rules/lot-quantity.md b/modules/purchase_trade/docs/rules/lot-quantity.md new file mode 100644 index 0000000..5297488 --- /dev/null +++ b/modules/purchase_trade/docs/rules/lot-quantity.md @@ -0,0 +1,106 @@ +# BR-PT-001 - Ajustement de la quantite theorique apres creation du contrat + +## Intent + +Conserver la coherence entre la quantite theorique de la ligne d'achat, le lot virtuel associe et les quantites ouvertes stockees dans `lot.qt`. + +## Scope + +- Domaine: `purchase_trade` +- Flux: modification de `purchase.line.quantity_theorical` apres creation du contrat +- Priorite: `bloquante` + +## Inputs + +- Une `purchase.line` existe deja. +- Son champ `quantity_theorical` est modifie via `write`. +- Un lot unique de type `virtual` est rattache a la ligne. + +## Expected Behavior + +Quand `purchase.line.quantity_theorical` est modifiee apres creation du contrat, le systeme doit recalculer le delta entre l'ancienne et la nouvelle valeur. + +La regle s'applique au lot unique de type `virtual` rattache a la `purchase.line`. + +Si `delta > 0`: + +- augmenter la quantite courante du lot `virtual` via `set_current_quantity` pour conserver l'historique `lot.qt.hist` +- augmenter le `lot.qt` ouvert existant +- si aucun `lot.qt` ouvert n'existe, en creer un nouveau avec le delta + +Si `delta < 0`: + +- diminuer le `lot.qt` ouvert uniquement si la quantite ouverte disponible est suffisante +- diminuer la quantite courante du lot `virtual` du meme delta +- si aucun `lot.qt` ouvert n'existe ou si sa quantite est insuffisante, bloquer avec l'erreur `Please unlink or unmatch lot` + +Definition du `lot.qt` ouvert: + +- `lot_p = virtual lot` +- `lot_s = None` +- `lot_shipment_in = None` +- `lot_shipment_internal = None` +- `lot_shipment_out = None` + +## Edge Cases + +- Si aucun lot `virtual` n'est trouve sur la ligne, la regle ne fait rien. + +## Examples + +### Exemple E1 - Augmentation simple + +- Donnees: + - `ancienne quantity_theorical = 100` + - `nouvelle quantity_theorical = 120` + - `lot.qt ouvert = 40` +- Attendu: + - lot `virtual` augmente de `20` + - `lot.qt ouvert` passe de `40` a `60` + +### Exemple E2 - Augmentation sans lot.qt ouvert + +- Donnees: + - `ancienne quantity_theorical = 100` + - `nouvelle quantity_theorical = 110` + - aucun `lot.qt` ouvert +- Attendu: + - lot `virtual` augmente de `10` + - creation d'un `lot.qt` ouvert a `10` + +### Exemple E3 - Diminution possible + +- Donnees: + - `ancienne quantity_theorical = 100` + - `nouvelle quantity_theorical = 90` + - `lot.qt ouvert = 25` +- Attendu: + - lot `virtual` diminue de `10` + - `lot.qt ouvert` passe de `25` a `15` + +### Exemple E4 - Diminution impossible + +- Donnees: + - `ancienne quantity_theorical = 100` + - `nouvelle quantity_theorical = 80` + - `lot.qt ouvert = 5` +- Attendu: + - blocage avec `Please unlink or unmatch lot` + +## Impacted Files + +- `modules/purchase_trade/purchase.py` +- `modules/purchase_trade/lot.py` + +## Tests + +Couvrir au minimum: + +- augmentation avec `lot.qt` ouvert existant +- augmentation sans `lot.qt` ouvert +- diminution possible +- diminution impossible avec erreur + +## Source + +- Decision metier documentee dans les commentaires de `purchase_trade.purchase.Line.write`. diff --git a/modules/purchase_trade/docs/rules/market-price-import.md b/modules/purchase_trade/docs/rules/market-price-import.md new file mode 100644 index 0000000..7173f10 --- /dev/null +++ b/modules/purchase_trade/docs/rules/market-price-import.md @@ -0,0 +1,171 @@ +# BR-PT-004 - Market Price Import + +## Intent + +Import dated market prices from an `.xlsx` Excel file into `price.price_value`, using `price.price` as the market price index. + +## Scope + +- Wizard: `purchase_trade.import_prices` +- Input model: `purchase_trade.import_prices.start` +- Result model: `purchase_trade.import_prices.result` +- Target models: + - `price.price` + - `price.price_value` +- Menu entry: under `price.menu_price` + +## Inputs + +The wizard reads only the first worksheet of an `.xlsx` file. + +The first row is treated as the header row. Header names are normalized by lowercasing and removing non-alphanumeric characters, so labels such as `price_index`, `price index`, and `Price Index` map to the same field. + +Required columns: + +- `price_index` +- `price_date` +- `high_price` +- `low_price` +- `open_price` +- `price_value` + +The wizard options are: + +- `Create price index if missing` +- `Overwrite existing price` + +## Date and Numeric Parsing + +Accepted `price_date` values: + +- Excel serial date numbers +- `YYYY-MM-DD` +- `DD/MM/YYYY` +- `MM/DD/YYYY` + +Numeric price fields accept decimal commas or decimal points. Empty numeric cells are imported as empty values. Invalid numeric values are reported as row errors. + +## Expected Behavior + +For each non-empty data row, starting from Excel row 2: + +1. Trim and validate `price_index`. +2. Parse `price_date`. +3. Search `price.price` by exact `price_index`. +4. If the price index is missing: + - create it when `Create price index if missing` is checked + - otherwise skip the row with `price_index missing` +5. Search `price.price_value` by `(price, price_date)`. +6. If an existing price value is found: + - update it when `Overwrite existing price` is checked + - otherwise skip the row with `price_date already exists` +7. If no existing price value is found, create a new `price.price_value`. + +## Created Price Index Defaults + +When the wizard creates a missing `price.price`, it sets: + +- `price_index = imported price_index` +- `price_desc = imported price_index` +- `price_curve_type = future` + +It also tries to default these references when matching records exist: + +- `price_type`: `price.fixtype` where `name = Market price` +- `price_currency`: `currency.currency` where `code = USD` +- `price_calendar`: `price.calendar` where `name = Argus EU` +- `price_unit`: `product.uom` where `name = Mt` + +If the `price_index` contains a `YYYY-MM` style period, the wizard derives a `product.month`: + +- pattern accepted in the name: `YYYY-MM`, `YYYY/MM`, `YYYY_MM`, `YYYY.MM`, or `YYYY MM` +- month name format: `MONYY`, for example `JUL26` +- if no matching `product.month` exists, it is created with `is_cotation = True` + +## Result Reporting + +The result screen always shows counts and detail sections for: + +- created price indexes +- imported prices +- updated existing prices +- skipped records +- errors + +Row-level errors do not stop the whole import; the wizard records the error and continues with the next row. + +## Edge Cases + +- Missing `price_index`: skipped. +- Missing `price_date`: skipped. +- Invalid `.xlsx` file: blocking `UserError`. +- Missing required columns: blocking `UserError`. +- Invalid date: row error. +- Invalid numeric value: row error. +- Existing `(price, price_date)` without overwrite option: skipped. +- Existing `(price, price_date)` with overwrite option: updated. +- Empty rows are ignored. + +## Impacted Files + +Direct `purchase_trade` files: + +- `modules/purchase_trade/pricing.py` +- `modules/purchase_trade/pricing.xml` +- `modules/purchase_trade/view/import_prices_start_form.xml` +- `modules/purchase_trade/view/import_prices_result_form.xml` +- `modules/purchase_trade/__init__.py` +- `modules/purchase_trade/tryton.cfg` +- `modules/purchase_trade/tests/test_module.py` + +External model dependencies: + +- `modules/price/price.py` +- `modules/price/price_value.py` +- `modules/price/view/price_value_form.xml` + +## Tests + +Existing focused tests cover: + +- missing price index skipped when creation is disabled +- missing price index created when creation is enabled +- default fields on newly created price indexes +- period reuse/creation from `price_index` +- existing `price_date` skipped when overwrite is disabled +- existing `price_date` updated when overwrite is enabled +- invalid row values collected as errors +- result screen formatting + +Recommended additional tests: + +- `.xlsx` header normalization +- missing required columns +- Excel serial date parsing +- empty row ignored +- invalid workbook raises `UserError` + +## Open Questions + +- Q: Should `(price, price_date)` be enforced unique at model/database level? +- A: Enforce uniqueness at model level for now. Do not add a database constraint yet. + +- Q: Should created price index defaults remain hardcoded to `Market price`, `USD`, `Argus EU`, and `Mt`? +- A: Yes. Keep these defaults hardcoded for this import. + +- Q: Should ambiguous slash dates prefer `DD/MM/YYYY` over `MM/DD/YYYY`, as currently implemented? +- A: Prefer the user's/default locale date format when possible. Fall back to the current order only if no locale preference is available. +- Comment: current code tries `DD/MM/YYYY` before `MM/DD/YYYY` and does not inspect locale. Implementing this answer requires a code change. + +- Q: Should missing `price_value` be allowed, or should it skip/error while high/low/open remain optional? +- A: Missing `price_value` is not allowed. Report the row as an error. `high_price`, `low_price`, and `open_price` remain optional. +- Comment: current code allows empty `price_value` and imports it as an empty value. Implementing this answer requires a code change. + +- Q: Should duplicate rows for the same `(price_index, price_date)` inside the same Excel file be treated as an error, skipped after the first row, or resolved by the overwrite option? +- A: Report duplicate rows as row errors and do not import or update the duplicate row. + +- Q: When `Create price index if missing` is enabled, should missing default reference records (`Market price`, `USD`, `Argus EU`, `Mt`) block index creation or remain optional as currently implemented? +- A: Remain optional. Create the price index with the reference records that can be found. + +- Q: Should the import result distinguish business validation errors from technical parsing errors? +- A: Yes. Distinguish business validation errors from file, parsing, and technical errors in the import result. diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 98e8db9..7b3d946 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -6,7 +6,9 @@ from datetime import date from types import SimpleNamespace from unittest.mock import Mock, patch +from trytond.model.exceptions import ValidationError from trytond.pool import Pool +from trytond.modules.price.price_value import PriceValue from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.modules.purchase_trade.pricing import ImportPrices @@ -306,6 +308,37 @@ class PurchaseTradeTestCase(ModuleTestCase): self.assertIn('Errors:', message) self.assertIn('Row 2 - LME Copper / 2026-03-27', message) + def test_price_value_unique_rejects_duplicate_create(self): + 'price value uniqueness rejects duplicate model creation' + price_value = SimpleNamespace( + id=None, + price=SimpleNamespace(id=1), + price_date=date(2026, 3, 27), + ) + + with patch.object( + PriceValue, 'search', + return_value=[SimpleNamespace(id=2)]): + with self.assertRaises(ValidationError): + PriceValue.check_unique_price_date([price_value]) + + def test_price_value_unique_rejects_duplicate_write(self): + 'price value uniqueness excludes current record on write' + price_value = SimpleNamespace( + id=2, + price=SimpleNamespace(id=1), + price_date=date(2026, 3, 27), + ) + + with patch.object( + PriceValue, 'search', + return_value=[SimpleNamespace(id=3)]) as search: + with self.assertRaises(ValidationError): + PriceValue.check_unique_price_date([price_value]) + + domain = search.call_args[0][0] + self.assertIn(('id', '!=', 2), domain[1]) + class _FakePriceModel: diff --git a/modules/trade_finance/AGENTS.md b/modules/trade_finance/AGENTS.md new file mode 100644 index 0000000..7889051 --- /dev/null +++ b/modules/trade_finance/AGENTS.md @@ -0,0 +1,7 @@ +# AGENTS.md + +Instructions locales pour les agents qui interviennent dans `modules/trade_finance/`. + +- `trade_finance` est un module additionnel de Tryton standard. +- Le traiter comme un module standard/upstream Tryton: privilegier les patterns Tryton existants, garder les changements generiques, et eviter d'y introduire de la logique metier specifique a un client sauf demande explicite. +- Limiter les modifications a ce module et a ses tests proches quand la demande concerne `trade_finance`.