Document rules and enforce price value uniqueness
This commit is contained in:
50
modules/purchase_trade/docs/rules/invoice-freight.md
Normal file
50
modules/purchase_trade/docs/rules/invoice-freight.md
Normal 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.
|
||||
59
modules/purchase_trade/docs/rules/lot-navigation.md
Normal file
59
modules/purchase_trade/docs/rules/lot-navigation.md
Normal 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.
|
||||
106
modules/purchase_trade/docs/rules/lot-quantity.md
Normal file
106
modules/purchase_trade/docs/rules/lot-quantity.md
Normal 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`.
|
||||
171
modules/purchase_trade/docs/rules/market-price-import.md
Normal file
171
modules/purchase_trade/docs/rules/market-price-import.md
Normal 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.
|
||||
Reference in New Issue
Block a user