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: + +
  • ### Updating `quantity_theorical` @@ -615,6 +627,18 @@ them with Python guards and SQL diagnostics. +
  • Trigger rule: + +
  • Non-zero orphan lot.qt block:
  • +
  • The created_by_code case is intentionally excluded from the immediate Lot.validate check: + +
  • ### 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 : + +
  • ### 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 : + +
  • Blocage lot.qt orphelin non zéro :
  • +
  • Le cas created_by_code est volontairement exclu du check immédiat Lot.validate : + +
  • ### 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