Document rules and enforce price value uniqueness
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data grouped="1">
|
||||
<record model="ir.message" id="msg_price_value_unique">
|
||||
<field name="text">Only one price value is allowed for each price index and price date.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_party_code_unique">
|
||||
<field name="text">The code on party must be unique.</field>
|
||||
</record>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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:`.
|
||||
|
||||
82
modules/purchase_trade/docs/documentation-management.md
Normal file
82
modules/purchase_trade/docs/documentation-management.md
Normal file
@@ -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.
|
||||
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.
|
||||
@@ -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:
|
||||
|
||||
|
||||
7
modules/trade_finance/AGENTS.md
Normal file
7
modules/trade_finance/AGENTS.md
Normal file
@@ -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`.
|
||||
Reference in New Issue
Block a user