Document rules and enforce price value uniqueness

This commit is contained in:
AzureAD\SylvainDUVERNAY
2026-05-07 15:02:15 +02:00
parent 8fb6d681a0
commit eaae2e5b40
11 changed files with 647 additions and 153 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.