diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.en.md b/modules/purchase_trade/docs/business/lots-and-quantities.en.md
index f2eb23f..909c4f1 100644
--- a/modules/purchase_trade/docs/business/lots-and-quantities.en.md
+++ b/modules/purchase_trade/docs/business/lots-and-quantities.en.md
@@ -392,6 +392,18 @@ them with Python guards and SQL diagnostics.
Lot.validate creates the open lot.qt through createVirtualPart.
+If the line is created_by_code:
+
+- it comes from a business workflow (
Create contracts, matching, etc.);
+
+Lot.validate does not create an automatic open lot.qt;
+
+- the workflow attaches or splits its source
lot.qt;
+
+- the blocking check applies only after the workflow is stable.
+
+
+
### Updating `quantity_theorical`
@@ -615,6 +627,18 @@ them with Python guards and SQL diagnostics.
+Trigger rule:
+
+- block inconsistent final states;
+
+- do not block internal transient workflow states;
+
+- use
Lot.skip_quantity_consistency() only around a sequence that restores the invariants afterwards;
+
+- call an explicit final check after the sequence.
+
+
+
Non-zero orphan lot.qt block:
lot.qt.validate
@@ -631,10 +655,20 @@ them with Python guards and SQL diagnostics.
- shipping / unshipping;
+- matched
Create contracts;
+
- weighing.
+The created_by_code case is intentionally excluded from the immediate Lot.validate check:
+
+- the virtual lot is saved before the matched
lot.qt is attached;
+
+- the invariant is checked by the final workflow guard.
+
+
+
### SQL Diagnostic
diff --git a/modules/purchase_trade/docs/business/lots-and-quantities.md b/modules/purchase_trade/docs/business/lots-and-quantities.md
index 8468e7f..0af9dad 100644
--- a/modules/purchase_trade/docs/business/lots-and-quantities.md
+++ b/modules/purchase_trade/docs/business/lots-and-quantities.md
@@ -391,6 +391,18 @@ puis les sécuriser par des checks Python et des diagnostics SQL.
Lot.validate crée le lot.qt ouvert via createVirtualPart.
+Si la ligne est created_by_code :
+
+- elle provient d'un workflow métier (
Create contracts, matching, etc.) ;
+
+Lot.validate ne crée pas de lot.qt ouvert automatique ;
+
+- le workflow rattache ou subdivise son
lot.qt source ;
+
+- le check bloquant s'applique seulement après stabilisation du workflow.
+
+
+
### Modification de `quantity_theorical`
@@ -614,6 +626,18 @@ puis les sécuriser par des checks Python et des diagnostics SQL.
+Règle de déclenchement :
+
+- bloquer les états finaux incohérents ;
+
+- ne pas bloquer les états transitoires internes d'un workflow ;
+
+- utiliser
Lot.skip_quantity_consistency() uniquement autour d'une séquence qui rétablit ensuite les invariants ;
+
+- appeler un check final explicite après la séquence.
+
+
+
Blocage lot.qt orphelin non zéro :
lot.qt.validate
@@ -630,10 +654,20 @@ puis les sécuriser par des checks Python et des diagnostics SQL.
- shipping / unshipping ;
+Create contracts en mode matched ;
+
- weighing.
+Le cas created_by_code est volontairement exclu du check immédiat Lot.validate :
+
+- le lot virtuel est sauvegardé avant que le
lot.qt matched soit rattaché ;
+
+- l'invariant est contrôlé par le check final du workflow.
+
+
+
### Diagnostic SQL
diff --git a/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md b/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md
index 6218d1d..5088a9d 100644
--- a/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md
+++ b/modules/purchase_trade/docs_source/business/lots-and-quantities.en.md
@@ -161,6 +161,11 @@ quantity_theorical
- create one `virtual` lot.
- The virtual lot receives a first `lot.qt.hist` entry.
- `Lot.validate` creates the open `lot.qt` through `createVirtualPart`.
+- If the line is `created_by_code`:
+ - it comes from a business workflow (`Create contracts`, matching, etc.);
+ - `Lot.validate` does not create an automatic open `lot.qt`;
+ - the workflow attaches or splits its source `lot.qt`;
+ - the blocking check applies only after the workflow is stable.
### Updating `quantity_theorical`
@@ -266,6 +271,11 @@ free_quantity = target_quantity - sum(already matched or shipped lot.qt)
- Central check:
- `lot.lot.assert_lines_quantity_consistency()`
+- Trigger rule:
+ - block inconsistent final states;
+ - do not block internal transient workflow states;
+ - use `Lot.skip_quantity_consistency()` only around a sequence that restores the invariants afterwards;
+ - call an explicit final check after the sequence.
- Non-zero orphan `lot.qt` block:
- `lot.qt.validate`
- Called after:
@@ -273,7 +283,11 @@ free_quantity = target_quantity - sum(already matched or shipped lot.qt)
- physical lot creation / deletion;
- matching / unmatching;
- shipping / unshipping;
+ - matched `Create contracts`;
- weighing.
+- The `created_by_code` case is intentionally excluded from the immediate `Lot.validate` check:
+ - the virtual lot is saved before the matched `lot.qt` is attached;
+ - the invariant is checked by the final workflow guard.
### SQL Diagnostic
diff --git a/modules/purchase_trade/docs_source/business/lots-and-quantities.md b/modules/purchase_trade/docs_source/business/lots-and-quantities.md
index 61f5f14..4ba5d14 100644
--- a/modules/purchase_trade/docs_source/business/lots-and-quantities.md
+++ b/modules/purchase_trade/docs_source/business/lots-and-quantities.md
@@ -160,6 +160,11 @@ quantity_theorical
- création d'un lot `virtual`.
- Le lot virtuel reçoit une première entrée `lot.qt.hist`.
- `Lot.validate` crée le `lot.qt` ouvert via `createVirtualPart`.
+- Si la ligne est `created_by_code` :
+ - elle provient d'un workflow métier (`Create contracts`, matching, etc.) ;
+ - `Lot.validate` ne crée pas de `lot.qt` ouvert automatique ;
+ - le workflow rattache ou subdivise son `lot.qt` source ;
+ - le check bloquant s'applique seulement après stabilisation du workflow.
### Modification de `quantity_theorical`
@@ -264,6 +269,11 @@ free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)
- Check central :
- `lot.lot.assert_lines_quantity_consistency()`
+- Règle de déclenchement :
+ - bloquer les états finaux incohérents ;
+ - ne pas bloquer les états transitoires internes d'un workflow ;
+ - utiliser `Lot.skip_quantity_consistency()` uniquement autour d'une séquence qui rétablit ensuite les invariants ;
+ - appeler un check final explicite après la séquence.
- Blocage `lot.qt` orphelin non zéro :
- `lot.qt.validate`
- Appels après :
@@ -271,7 +281,11 @@ free_quantity = target_quantity - somme(lot.qt déjà matchés ou shippés)
- création / suppression de lots physiques ;
- matching / unmatching ;
- shipping / unshipping ;
+ - `Create contracts` en mode matched ;
- weighing.
+- Le cas `created_by_code` est volontairement exclu du check immédiat `Lot.validate` :
+ - le lot virtuel est sauvegardé avant que le `lot.qt` matched soit rattaché ;
+ - l'invariant est contrôlé par le check final du workflow.
### Diagnostic SQL
diff --git a/modules/purchase_trade/lot.py b/modules/purchase_trade/lot.py
index d02ddde..fd9c1be 100755
--- a/modules/purchase_trade/lot.py
+++ b/modules/purchase_trade/lot.py
@@ -880,6 +880,9 @@ class Lot(metaclass=PoolMeta):
lqt = LotQt.search([('lot_p','=',lot.id)])
if len(lqt)==0 and not lot.line.created_by_code:
lot.createVirtualPart(lot.lot_quantity,None,None)
+ # Wizard-created matched lines attach/split lot.qt after
+ # the virtual lot save; the final workflow check covers
+ # the complete state.
if not lot.line.created_by_code:
virtual_lots_to_check.append(lot)
@@ -887,6 +890,8 @@ class Lot(metaclass=PoolMeta):
lqt = LotQt.search([('lot_s','=',lot.id)])
if len(lqt)==0 and not lot.sale_line.created_by_code:
lot.createVirtualPart(lot.lot_quantity,None,lot.id,'only sale')
+ # Same rule as purchase side: do not check the transient
+ # save of a virtual lot created by matching code.
if not lot.sale_line.created_by_code:
virtual_lots_to_check.append(lot)
diff --git a/modules/purchase_trade/service.py b/modules/purchase_trade/service.py
index 3115324..236abb6 100644
--- a/modules/purchase_trade/service.py
+++ b/modules/purchase_trade/service.py
@@ -331,30 +331,8 @@ class ContractFactory:
lqh.gross_quantity = round(lot.lot_quantity, 5)
lot.lot_hist = [lqh]
- logger.info(
- "CREATE_CONTRACT_LOT_BEFORE_SAVE type=%s line=%s qty=%s "
- "source_lot=%s source_lqt=%s shipment=%s skip_before=%s",
- type_,
- getattr(line, 'id', None),
- getattr(lot, 'lot_quantity', None),
- getattr(source.get('lot'), 'id', None),
- getattr(source.get('lqt'), 'id', None),
- source.get('shipment_origin'),
- Transaction().context.get(
- '_purchase_trade_skip_quantity_consistency'))
-
with Lot.skip_quantity_consistency():
lot.save()
- logger.info(
- "CREATE_CONTRACT_LOT_SAVED type=%s line=%s lot=%s qty=%s "
- "source_lot=%s source_lqt=%s shipment=%s",
- type_,
- getattr(line, 'id', None),
- getattr(lot, 'id', None),
- getattr(lot, 'lot_quantity', None),
- getattr(source.get('lot'), 'id', None),
- getattr(source.get('lqt'), 'id', None),
- source.get('shipment_origin'))
vlot = source['lot']
shipment_origin = source.get('shipment_origin')
@@ -363,14 +341,6 @@ class ContractFactory:
if type_ == 'Purchase':
if cls._attach_or_split_source_lot_qt(source, lot, type_):
return
- logger.info(
- "CREATE_CONTRACT_LOT_FALLBACK type=%s new_lot=%s "
- "source_lot=%s qty=%s shipment=%s",
- type_,
- getattr(lot, 'id', None),
- getattr(vlot, 'id', None),
- qt,
- shipment_origin)
if not lot.updateVirtualPart(qt, shipment_origin, vlot):
lot.createVirtualPart(qt, shipment_origin, vlot)
@@ -379,14 +349,6 @@ class ContractFactory:
else:
if cls._attach_or_split_source_lot_qt(source, lot, type_):
return
- logger.info(
- "CREATE_CONTRACT_LOT_FALLBACK type=%s new_lot=%s "
- "source_lot=%s qty=%s shipment=%s",
- type_,
- getattr(lot, 'id', None),
- getattr(vlot, 'id', None),
- qt,
- shipment_origin)
if not vlot.updateVirtualPart(qt, shipment_origin, lot):
vlot.createVirtualPart(qt, shipment_origin, lot)
@@ -404,35 +366,12 @@ class ContractFactory:
if not source_lqt:
source_lqt = cls._find_source_lot_qt(source, type_)
if not source_lqt:
- logger.info(
- "CREATE_CONTRACT_ATTACH_SKIP no source_lqt type=%s "
- "new_lot=%s source_lot=%s qty=%s shipment=%s",
- type_,
- getattr(new_virtual_lot, 'id', None),
- getattr(source.get('lot'), 'id', None),
- quantity,
- source.get('shipment_origin'))
return False
remaining = cls._normalize_quantity(source_lqt.lot_quantity) - quantity
if remaining < 0:
- logger.info(
- "CREATE_CONTRACT_ATTACH_SKIP insufficient source_lqt=%s "
- "available=%s qty=%s",
- getattr(source_lqt, 'id', None),
- getattr(source_lqt, 'lot_quantity', None),
- quantity)
return False
- logger.info(
- "CREATE_CONTRACT_ATTACH type=%s source_lqt=%s new_lot=%s "
- "source_lot=%s qty=%s remaining=%s",
- type_,
- getattr(source_lqt, 'id', None),
- getattr(new_virtual_lot, 'id', None),
- getattr(source.get('lot'), 'id', None),
- quantity,
- remaining)
if remaining:
matched_lqt = LotQt()
matched_lqt.lot_p = (
@@ -495,22 +434,6 @@ class ContractFactory:
for lqt in lqts:
if cls._normalize_quantity(lqt.lot_quantity) >= quantity:
return lqt
- logger.info(
- "CREATE_CONTRACT_FIND_SOURCE_LQT_EMPTY type=%s lot=%s qty=%s "
- "shipment=%s candidates=%s",
- type_,
- getattr(lot, 'id', None),
- quantity,
- source.get('shipment_origin'),
- [
- (
- getattr(lqt, 'id', None),
- getattr(lqt, 'lot_quantity', None),
- getattr(getattr(lqt, 'lot_p', None), 'id', None),
- getattr(getattr(lqt, 'lot_s', None), 'id', None),
- )
- for lqt in lqts
- ])
return None
@staticmethod