From 086eb52c835ba4de4d1ee641cb9a78ae8a99ae0a Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Thu, 27 Nov 2025 10:00:38 +0700 Subject: [PATCH] first commit --- README.md | 153 ++++++ __init__.py | 3 + __manifest__.py | 45 ++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 234 bytes __pycache__/__manifest__.cpython-312.pyc | Bin 0 -> 1667 bytes models/__init__.py | 4 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 287 bytes .../__pycache__/quality_check.cpython-312.pyc | Bin 0 -> 8313 bytes .../stock_move_line.cpython-312.pyc | Bin 0 -> 5452 bytes models/quality_check.py | 197 +++++++ models/stock_move_line.py | 138 +++++ security/ir.model.access.csv | 1 + tests/__init__.py | 11 + tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 668 bytes .../test_basic_functionality.cpython-312.pyc | Bin 0 -> 3484 bytes .../test_edge_cases.cpython-312.pyc | Bin 0 -> 18283 bytes .../test_integration.cpython-312.pyc | Bin 0 -> 14677 bytes ...t_property_lot_propagation.cpython-312.pyc | Bin 0 -> 11955 bytes .../test_property_lot_update.cpython-312.pyc | Bin 0 -> 12682 bytes ...erty_non_receipt_operation.cpython-312.pyc | Bin 0 -> 19554 bytes ...property_receipt_operation.cpython-312.pyc | Bin 0 -> 17062 bytes ...rty_relationship_integrity.cpython-312.pyc | Bin 0 -> 16476 bytes ...roperty_state_preservation.cpython-312.pyc | Bin 0 -> 15167 bytes tests/test_basic_functionality.py | 42 ++ tests/test_edge_cases.py | 399 ++++++++++++++ tests/test_integration.py | 305 +++++++++++ tests/test_property_lot_propagation.py | 254 +++++++++ tests/test_property_lot_update.py | 282 ++++++++++ tests/test_property_non_receipt_operation.py | 500 ++++++++++++++++++ tests/test_property_receipt_operation.py | 421 +++++++++++++++ tests/test_property_relationship_integrity.py | 399 ++++++++++++++ tests/test_property_state_preservation.py | 369 +++++++++++++ 32 files changed, 3523 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/__manifest__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/quality_check.cpython-312.pyc create mode 100644 models/__pycache__/stock_move_line.cpython-312.pyc create mode 100644 models/quality_check.py create mode 100644 models/stock_move_line.py create mode 100644 security/ir.model.access.csv create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/test_basic_functionality.cpython-312.pyc create mode 100644 tests/__pycache__/test_edge_cases.cpython-312.pyc create mode 100644 tests/__pycache__/test_integration.cpython-312.pyc create mode 100644 tests/__pycache__/test_property_lot_propagation.cpython-312.pyc create mode 100644 tests/__pycache__/test_property_lot_update.cpython-312.pyc create mode 100644 tests/__pycache__/test_property_non_receipt_operation.cpython-312.pyc create mode 100644 tests/__pycache__/test_property_receipt_operation.cpython-312.pyc create mode 100644 tests/__pycache__/test_property_relationship_integrity.cpython-312.pyc create mode 100644 tests/__pycache__/test_property_state_preservation.cpython-312.pyc create mode 100644 tests/test_basic_functionality.py create mode 100644 tests/test_edge_cases.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_property_lot_propagation.py create mode 100644 tests/test_property_lot_update.py create mode 100644 tests/test_property_non_receipt_operation.py create mode 100644 tests/test_property_receipt_operation.py create mode 100644 tests/test_property_relationship_integrity.py create mode 100644 tests/test_property_state_preservation.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b6bffa --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# Quality Check Lot Preserve + +## Overview + +This module modifies the standard Odoo 18 quality check behavior for receipt operations to preserve quality check states when lot numbers are assigned after quality checks have been completed. + +## Problem Statement + +In standard Odoo, when a quality check is completed before a lot number is assigned, the quality check automatically resets to "Todo" state when the lot number is later assigned. This creates inefficiency in warehouse operations where: + +1. Quality checks are performed immediately upon receipt +2. Lot numbers are generated or assigned later in the process +3. Quality checks must be re-done unnecessarily + +## Solution + +This module provides the following functionality: + +### 1. State Preservation +- Quality check states (Pass, Fail, In Progress) are preserved when lot numbers are assigned +- Applies only to receipt operations (incoming shipments) +- Other operation types (manufacturing, internal transfers, delivery) continue with standard Odoo behavior + +### 2. Automatic Lot Number Synchronization +- Lot numbers assigned to stock move lines are automatically copied to related quality checks +- Lot number changes are synchronized in real-time +- Maintains traceability between inventory and quality records + +### 3. Selective Application +- Custom behavior applies only to receipt operations +- Standard Odoo workflows remain unchanged for other operation types +- Seamless integration with existing quality control processes + +## Installation + +1. Copy this module to your Odoo addons directory (e.g., `customaddons/quality_check_lot_preserve`) +2. Update the apps list in Odoo +3. Install the "Quality Check Lot Preserve" module + +## Dependencies + +- `stock` - Odoo Inventory Management +- `quality_control` - Odoo Quality Control (Enterprise module) + +**Note:** This module requires Odoo Enterprise edition as it depends on the `quality_control` module. + +## Usage + +### Typical Workflow + +1. **Receive Products**: Create a purchase order and validate the receipt +2. **Perform Quality Checks**: Complete quality checks immediately upon receipt (before lot assignment) +3. **Assign Lot Numbers**: Generate or manually assign lot numbers to the stock move lines +4. **Result**: Quality check states remain unchanged, and lot numbers are automatically copied to quality checks + +### Example Scenario + +**Before this module:** +- Receive 100 units of Product A +- Complete quality check → Status: "Pass" +- Assign lot number "LOT001" +- Quality check resets → Status: "Todo" ❌ +- Must re-do quality check + +**With this module:** +- Receive 100 units of Product A +- Complete quality check → Status: "Pass" +- Assign lot number "LOT001" +- Quality check remains → Status: "Pass" ✓ +- Lot number automatically copied to quality check ✓ + +## Technical Details + +### Extended Models + +#### stock.move.line +- Monitors `lot_id` and `lot_name` field changes +- Identifies related quality checks for receipt operations +- Triggers quality check lot number updates + +#### quality.check +- Prevents state resets during lot assignment for receipt operations +- Accepts lot number updates from stock move lines +- Maintains standard behavior for non-receipt operations + +### Operation Type Detection + +The module identifies receipt operations by checking: +- `picking_type_code == 'incoming'` +- Fallback to `picking_id.picking_type_id.code` + +### Data Flow + +``` +Stock Move Line (Lot Assigned) + ↓ +Detect Change (write method) + ↓ +Find Related Quality Checks + ↓ +Check Operation Type (Receipt?) + ↓ Yes +Copy Lot Number + Preserve State + ↓ +Update Quality Check +``` + +### Security and Access Rights + +This module extends existing Odoo models (`stock.move.line` and `quality.check`) without creating new models or data structures. Therefore: + +- **No new access rights are required** +- Access rights are inherited from the base modules (`stock` and `quality_control`) +- Users with existing quality control and inventory permissions can use this module +- The `security/ir.model.access.csv` file is present but empty (only contains the header row) + +The module respects all existing Odoo security rules and group permissions. + +## Configuration + +No additional configuration is required. The module works automatically once installed. + +## Compatibility + +- **Odoo Version**: 18.0 +- **Edition**: Enterprise (requires quality_control module) +- **Database**: PostgreSQL + +## Limitations + +- Only applies to receipt operations (incoming shipments) +- Requires the quality_control Enterprise module +- Does not modify historical quality check records + +## Support + +For issues, questions, or feature requests, please contact your Odoo implementation partner or system administrator. + +## License + +LGPL-3 + +## Author + +Your Company + +## Version History + +### 1.0.0 (Initial Release) +- State preservation for quality checks during lot assignment +- Automatic lot number synchronization +- Receipt operation filtering +- Full integration with standard Odoo quality control workflows diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..f73c6af --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Quality Check Lot Preserve', + 'version': '18.0.1.0.0', + 'category': 'Inventory/Quality', + 'summary': 'Preserve quality check states when lot numbers are assigned during receipt operations', + 'description': """ +Quality Check Lot Preservation +=============================== + +This module modifies the standard Odoo 18 quality check behavior for receipt operations. + +Key Features: +------------- +* Preserves quality check states when lot numbers are assigned after quality checks are completed +* Automatically copies lot numbers from stock move lines to related quality checks +* Maintains synchronization when lot numbers are changed +* Applies only to receipt operations, leaving other workflows unchanged + +Use Case: +--------- +In standard Odoo, when a quality check is completed before a lot number is assigned, +the quality check resets to "Todo" state when the lot number is later assigned. +This module prevents that reset and automatically updates the quality check with the lot number. + +Technical Details: +------------------ +* Extends stock.move.line to detect lot number changes +* Extends quality.check to prevent state resets for receipt operations +* Maintains full compatibility with standard Odoo quality control workflows + """, + 'author': 'Your Company', + 'website': 'https://www.yourcompany.com', + 'license': 'LGPL-3', + 'depends': [ + 'stock', + 'quality_control', + ], + 'data': [ + 'security/ir.model.access.csv', + ], + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61d7d610c708632c9e75f40e4a0ad5db37fe4349 GIT binary patch literal 234 zcmXv}F=_)b5Y*n0!x#){@`b=P66su+kQW5kX@nvzoQQOLmUIEVBwujek{`HDl`ddN z;VOHOOtUis!_ItcHfzDSI^KH8_j3{dX#U9gn#r4R!a)!bo&ZE``t}Rh?%~u$av1Vs z-<(8h*?Sk~C3!(LP(+pI0h6n;8cWX)(M)EPmZW!TU&}zF<+vh8bhlM@&V{0Jj>fDG zvkAUbZ3nb3I#AJ5!i?z^mDYarMQb_L%WB63=79HU#b1|FdD-;c0>Dov;3jn}e*yNL BKOX=9 literal 0 HcmV?d00001 diff --git a/__pycache__/__manifest__.cpython-312.pyc b/__pycache__/__manifest__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c68d33c151932732d06cce040b8be62f56d4448 GIT binary patch literal 1667 zcma)6(T*E66rD+Sv+3-nY3WLADM(h4KwGwxE<8|G1qm$_tyrK)yAM1?j(wA{I%7|5 zPr}TP@WMOdTlj%!o{;!~Nc&a^!S&2+Hc1P@krPL@kMBL_-1~d47kD`S{`CX#wBvby zx<6OrX0y0_fQz3!?s1>Dc$;tWZQkKKyvuJ)T0EHee3$Rxr}s91q*Xc}G&hQitApfagTX2cW2ps0W)Vmd zBc9BX5oklG211Py6A7H~Oba<78W@Pw5|x6cRw!xcUv19|{xz!VQ1ID*k03aYgdvII zSq$z+j0HwyBOn-@%oOV@D1eT8Bm)cOHYD_t0Q=@ zK}nu(eMiG_5z zD@-RwVfe-a2>C4)(&9A4RpuOx<2y|Ag~5s3i_MWq$8 zugOg57vosX49W1N8Yj3gKu)NE6*_}cxh~aF9SdELo}kK0a>xi!HT2jD9qz!D=Z*qr zfOW4rtJ{j5`{#QNnn_K( zS-IAb64nKbmzr!EIvSSVToY>s8fS4_DT4kPiK?+wiLIC2QUfKeR`Inf0)nI1`%z_@ zoS^Bc%!_-GwW)bB7|dp~FvlGyIBtaaTXfF8et!1wlXAOCyzDo~Lslaf?;2p(X>2|a z8foDWhm~b(nW0!LK9n>>Y zy4;z;$Owx!W5GZgTyRL82IUq?Ov_!YCyO;oM=_MU)Kv;XJBU4JiD9$--EW@0jZOQ~ zb?{f8vJ4YRDCgJ-2g_t!TOFb=hA2zbng_#SLZujkvBP1Q=C8U>>t^@4-bWlCr#Z#1 zMceoNMaSzM&inV~x89%k$BWj%es}S)*MD!`KbrU7{-fKUckeE?+qXK4z`OC`qTT9s X@XG$d{LaCA|L*05|B>I)Zz0-0_|HD# literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..1bd1be7 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import stock_move_line +from . import quality_check diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..160066d8d2924dbf613d7756ddbf5613c09b3977 GIT binary patch literal 287 zcmY*TyGjH>5bWMPe8K7mD2Bo&JD3@W_yr=ET{4U_13TQz^g6S9Fkj*?=(qR>7nm5C z35Ur=_JRghP+eVAQ~lO7N5D9Kxo{6WKj!5R)vwr|v3Ldp20a8^BF?&ET`enwQn!mw zy^EHu=4n#ijYaC->eXFaZy?p2P79`2*fPV;i=KiNdFpL4r7KF) z<84rbik+s;8F636o}wmW1sS5tE1@Uu3d$IYS^Vpeo<=z&n{8TK5%|VuA*G9Ml(HMj f?%^;tV$o00 literal 0 HcmV?d00001 diff --git a/models/__pycache__/quality_check.cpython-312.pyc b/models/__pycache__/quality_check.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07996a62f6b728faa17633eeace99172c1a6fc63 GIT binary patch literal 8313 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 OKmNGzUkv@AB>orH?`Jgt literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_move_line.cpython-312.pyc b/models/__pycache__/stock_move_line.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d9566767cce1def44b76351e3bf7ca9afa7bb66 GIT binary patch literal 5452 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 B%w+%o literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_basic_functionality.cpython-312.pyc b/tests/__pycache__/test_basic_functionality.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f948bdd5b33fc956b9c69648c5c5746150518308 GIT binary patch literal 3484 zcmdT{&2JM&6rb_uI>e;3f!J+aHY!cKLD-~}5LF1Fqy#FJ6i5)rh>kY96WL(zrZejl zyHv^niS$tQQ~^>YI6`yjf6!~CUb0G%(^U^ud+H4}P@$aqW_BIhsS~&|Ql8(=yf<>nW02*{Wj1sOMEAeBXQ1LqS{NQ-;>rYU#EbY3(jDwbSKFBP3?ZCv)Lylh{msslD$ zb!>H>vf^x2bqZ81+h(Oi)sn57T-7a8HEagG)G64|L2nefW8juOi$jXP^%{up@g3xB z4myqFpquF1T?j1&BN)D~#Wu}06%(gHzllhs(Y1|TD6ne)fvQLgPj2*Z$2P8K%l16Y zmWmeLNPba~r_LG$YFyW1UX)knC}YA8bn0B3^TO1c_mWe>^DH=i3VvSqdEakbx`n(5 zr=?jhL5NY(IVXgRgQ*J}U*!sRndW${SA}b{4BMnR z+qCWB(VS6%O_p`jv@MibQ%5lsf{$);JL6(4cn{b-zbQ6E6k568lZuniie?Iz~yTIaf8T zMQS;52N1iuqizWR5zMrt8oH&EY!gA3s)3u|44WN}znGiE@R zwrx0$?1ba?eqwEY6vP%3vu-B4d^o?VTxmmb$W0H`(}&&k;h%Dkm9hVWD3YtB>W`pE zs>1Kqc1>7pX+^XYkos#3lPs&;e{HG>JB=5=a+nPwgm-u%@ zf@ivRVy7^Cv40Bi<&z*5(32S2o38f`y1j$--XXVl=-~$|z1hVC#N&58-}L+te~?%{ zIJS}+U+X*nG>j8d7}8L4WMUwPG2Ivqm8IGV+{@BJFY3M zkJWbZ-Ilr7TW~YAk+wedr6YMC@l7}D3V4MeuxrP5n6INpgBk-3YKUTGmf7Xj2Tix& z8LW-%Fn2(ZZJk?)A~SIY4$s~eVn2umI~rx3UpKWe-U&J1Ut^RumeU;ucXeb^_>$V7urWI ztfZ#a`mPAxT=jXQb$JSr6$T+*7ZK$H)ajsi6Pc$(?EznLLQLBrM3#rFPh6cSmYLWz z6aTMx$)JoiXsJYqhFK5v%17>`ELUW8EQNSQ_&Xq;-$K7d&OVKVqa*)xg`*Q#$Z?SI z=q3E`>xpQvN9i@Gg}ptZ+RX|qI8?&rvTZT(*vRomNag{#)`oK2KgS7{g>mupaFKkT YgfYg?VhCrS#tz`C_%3;d#IN-H52hoE%>V!Z literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_edge_cases.cpython-312.pyc b/tests/__pycache__/test_edge_cases.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f72b85ced7f662d1c23cac526bea024880e6325 GIT binary patch literal 18283 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 ph&AV?2G^a literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_property_lot_propagation.cpython-312.pyc b/tests/__pycache__/test_property_lot_propagation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0421402ad7f4c212c81d639ac51fbb05a39e32b9 GIT binary patch literal 11955 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 F|G$lvb9n#& literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_property_non_receipt_operation.cpython-312.pyc b/tests/__pycache__/test_property_non_receipt_operation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5619805cb3483183343d3e06eaa2ec5ff2af1958 GIT binary patch literal 19554 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_property_receipt_operation.cpython-312.pyc b/tests/__pycache__/test_property_receipt_operation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1212dc69686ad695967194ea0251655d76df057e GIT binary patch literal 17062 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

7wCxA=?I*PDCw_n9 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�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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_property_state_preservation.cpython-312.pyc b/tests/__pycache__/test_property_state_preservation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c111a9fa04fbd16e76594da8d8258a9a47d8d084 GIT binary patch literal 15167 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 0) + + # Create quality checks for each move line + quality_checks = [] + for move_line in move_lines: + qc = self._create_quality_check_for_move_line(move_line, state='pass') + quality_checks.append(qc) + + # Select the first move line to assign a lot number + target_move_line = move_lines[0] + target_quality_check = quality_checks[0] + + # Create and assign a lot number to the target move line + lot = self._create_lot(target_move_line.product_id, f'LOT-{lot_name_seed}') + + # Record initial lot_id values for all quality checks + initial_lot_ids = {qc.id: qc.lot_id.id for qc in quality_checks} + + # Assign the lot to the target move line + target_move_line.write({'lot_id': lot.id}) + + # Refresh quality checks from database + for qc in quality_checks: + qc.invalidate_recordset() + + # PROPERTY VERIFICATION: + # 1. The target quality check should have the lot number assigned (Requirement 2.1) + target_quality_check.invalidate_recordset() + self.assertEqual( + target_quality_check.lot_id.id, + lot.id, + f"Quality check {target_quality_check.id} should have lot {lot.name} assigned" + ) + + # 2. Other quality checks should NOT have this lot number (Requirement 2.3) + for i, qc in enumerate(quality_checks[1:], start=1): + qc.invalidate_recordset() + # The lot_id should remain unchanged (either False or the initial value) + self.assertEqual( + qc.lot_id.id, + initial_lot_ids[qc.id], + f"Unrelated quality check {qc.id} (for product {qc.product_id.name}) " + f"should not have lot {lot.name} assigned" + ) + + # Additionally, if a lot was assigned, it should not be the target lot + if qc.lot_id: + self.assertNotEqual( + qc.lot_id.id, + lot.id, + f"Unrelated quality check {qc.id} should not have the target lot assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_lot_propagation_single_check(self, lot_name_seed, quality_state): + """ + Simplified property test: For any single quality check on a receipt operation, + when a lot number is assigned to its move line, the lot number should be + copied to the quality check. + + Validates: Requirement 2.1 + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, moves = self._create_receipt_picking([product]) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # Create and assign a lot number + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + + # Assign the lot to the move line + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: The quality check should have the lot number assigned + self.assertEqual( + quality_check.lot_id.id, + lot.id, + f"Quality check should have lot {lot.name} assigned" + ) diff --git a/tests/test_property_lot_update.py b/tests/test_property_lot_update.py new file mode 100644 index 0000000..23e9a8b --- /dev/null +++ b/tests/test_property_lot_update.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import TransactionCase +from hypothesis import given, strategies as st, settings, assume + + +class TestLotNumberUpdateSynchronization(TransactionCase): + """ + Property-based tests for lot number update synchronization. + + Feature: quality-check-lot-preserve, Property 3: Lot number update synchronization + + Property: For any quality check with an assigned lot number, when the lot number + on the related stock move line is changed, the quality check should be updated + with the new lot number. + + Validates: Requirements 3.1 + """ + + def setUp(self): + super(TestLotNumberUpdateSynchronization, self).setUp() + + # Get required models + self.StockMoveLine = self.env['stock.move.line'] + self.QualityCheck = self.env['quality.check'] + self.StockPicking = self.env['stock.picking'] + self.StockMove = self.env['stock.move'] + self.ProductProduct = self.env['product.product'] + self.StockLocation = self.env['stock.location'] + self.StockPickingType = self.env['stock.picking.type'] + self.StockLot = self.env['stock.lot'] + self.QualityPoint = self.env['quality.point'] + + # Get or create locations + self.supplier_location = self.env.ref('stock.stock_location_suppliers') + self.stock_location = self.env.ref('stock.stock_location_stock') + + # Get or create receipt picking type + self.receipt_picking_type = self.env['stock.picking.type'].search([ + ('code', '=', 'incoming') + ], limit=1) + + if not self.receipt_picking_type: + self.receipt_picking_type = self.env['stock.picking.type'].create({ + 'name': 'Receipts', + 'code': 'incoming', + 'sequence_code': 'IN', + 'warehouse_id': self.env['stock.warehouse'].search([], limit=1).id, + }) + + def _create_product_with_tracking(self, name): + """Helper to create a product with lot tracking enabled""" + return self.ProductProduct.create({ + 'name': name, + 'type': 'consu', + 'tracking': 'lot', + }) + + def _create_receipt_picking(self, product): + """Helper to create a receipt picking with a move line for the given product""" + picking = self.StockPicking.create({ + 'picking_type_id': self.receipt_picking_type.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + move = self.StockMove.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 10.0, + 'product_uom': product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + # Confirm the picking to create move lines + picking.action_confirm() + + return picking, move + + def _create_lot(self, product, lot_name): + """Helper to create a lot for a product""" + return self.StockLot.create({ + 'name': lot_name, + 'product_id': product.id, + 'company_id': self.env.company.id, + }) + + def _create_quality_check_for_move_line(self, move_line, state='none'): + """Helper to create a quality check linked to a move line""" + # Get or create a quality team + quality_team = self.env['quality.alert.team'].search([], limit=1) + if not quality_team: + quality_team = self.env['quality.alert.team'].create({ + 'name': 'Test Quality Team', + }) + + # Create a quality point for the product if needed + quality_point = self.QualityPoint.search([ + ('product_ids', 'in', [move_line.product_id.id]), + ('picking_type_ids', 'in', [self.receipt_picking_type.id]), + ], limit=1) + + if not quality_point: + # Get a test type - use passfail which should always exist + test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False) + if not test_type: + # If no test type exists, create a minimal one + test_type = self.env['quality.point.test_type'].create({ + 'name': 'Pass/Fail Test', + 'technical_name': 'passfail', + }) + + quality_point = self.QualityPoint.create({ + 'title': f'Quality Check for {move_line.product_id.name}', + 'product_ids': [(4, move_line.product_id.id)], + 'picking_type_ids': [(4, self.receipt_picking_type.id)], + 'test_type_id': test_type.id, + 'team_id': quality_team.id, + }) + + return self.QualityCheck.create({ + 'product_id': move_line.product_id.id, + 'picking_id': move_line.picking_id.id, + 'move_line_id': move_line.id, + 'quality_state': state, + 'point_id': quality_point.id, + 'team_id': quality_team.id, + }) + + @settings(max_examples=100, deadline=None) + @given( + initial_lot_seed=st.integers(min_value=1, max_value=1000000), + updated_lot_seed=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_lot_update_synchronization(self, initial_lot_seed, updated_lot_seed, quality_state): + """ + Property: For any quality check with an assigned lot number, when the lot number + on the related stock move line is changed, the quality check should be updated + with the new lot number. + + This test verifies Requirement 3.1: + - When a lot number on a stock move line is changed after initial assignment, + the related quality check is updated with the new lot number + + Test strategy: + 1. Create a receipt with a quality check + 2. Assign an initial lot number to the move line + 3. Verify the quality check receives the initial lot number + 4. Change the lot number on the move line to a different lot + 5. Verify the quality check is updated with the new lot number + """ + # Ensure the two lot numbers are different + assume(initial_lot_seed != updated_lot_seed) + + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {initial_lot_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check with the specified state + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # Record the initial quality check state + initial_state = quality_check.quality_state + + # Create and assign the initial lot number + initial_lot = self._create_lot(product, f'LOT-INITIAL-{initial_lot_seed}') + move_line.write({'lot_id': initial_lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # Verify the quality check received the initial lot number + self.assertEqual( + quality_check.lot_id.id, + initial_lot.id, + f"Quality check should have initial lot {initial_lot.name} assigned" + ) + + # Create a new lot number + updated_lot = self._create_lot(product, f'LOT-UPDATED-{updated_lot_seed}') + + # Change the lot number on the move line + move_line.write({'lot_id': updated_lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # 1. The quality check should be updated with the new lot number (Requirement 3.1) + self.assertEqual( + quality_check.lot_id.id, + updated_lot.id, + f"Quality check should be updated with new lot {updated_lot.name} " + f"(was {initial_lot.name})" + ) + + # 2. The quality check state should remain unchanged (Requirement 3.2) + self.assertEqual( + quality_check.quality_state, + initial_state, + f"Quality check state should remain {initial_state} after lot update" + ) + + # 3. The relationship between quality check and move line should remain intact (Requirement 3.3) + self.assertEqual( + quality_check.move_line_id.id, + move_line.id, + "Quality check should still be linked to the same move line" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_seed_1=st.integers(min_value=1, max_value=1000000), + lot_seed_2=st.integers(min_value=1, max_value=1000000), + lot_seed_3=st.integers(min_value=1, max_value=1000000), + ) + def test_property_multiple_lot_updates(self, lot_seed_1, lot_seed_2, lot_seed_3): + """ + Property: For any quality check, multiple consecutive lot number changes + on the move line should result in the quality check always reflecting + the most recent lot number. + + This test verifies that the synchronization works correctly even with + multiple updates in sequence. + """ + # Ensure all lot numbers are different + assume(lot_seed_1 != lot_seed_2) + assume(lot_seed_2 != lot_seed_3) + assume(lot_seed_1 != lot_seed_3) + + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_seed_1}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line(move_line, state='pass') + + # Create three different lots + lot_1 = self._create_lot(product, f'LOT-1-{lot_seed_1}') + lot_2 = self._create_lot(product, f'LOT-2-{lot_seed_2}') + lot_3 = self._create_lot(product, f'LOT-3-{lot_seed_3}') + + # First update: assign lot_1 + move_line.write({'lot_id': lot_1.id}) + quality_check.invalidate_recordset() + self.assertEqual(quality_check.lot_id.id, lot_1.id) + + # Second update: change to lot_2 + move_line.write({'lot_id': lot_2.id}) + quality_check.invalidate_recordset() + self.assertEqual(quality_check.lot_id.id, lot_2.id) + + # Third update: change to lot_3 + move_line.write({'lot_id': lot_3.id}) + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: Quality check should have the final lot number + self.assertEqual( + quality_check.lot_id.id, + lot_3.id, + f"Quality check should have the final lot {lot_3.name} after multiple updates" + ) + + # State should still be 'pass' + self.assertEqual( + quality_check.quality_state, + 'pass', + "Quality check state should remain 'pass' after multiple lot updates" + ) diff --git a/tests/test_property_non_receipt_operation.py b/tests/test_property_non_receipt_operation.py new file mode 100644 index 0000000..e002040 --- /dev/null +++ b/tests/test_property_non_receipt_operation.py @@ -0,0 +1,500 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import TransactionCase +from hypothesis import given, strategies as st, settings + + +class TestNonReceiptOperationBehavior(TransactionCase): + """ + Property-based tests for non-receipt operation standard behavior. + + Feature: quality-check-lot-preserve, Property 6: Non-receipt operation standard behavior + + Property: For any quality check associated with non-receipt operations (manufacturing, internal, outgoing), + the standard Odoo behavior (state reset on lot assignment) should occur. + + Validates: Requirements 4.2 + """ + + def setUp(self): + super(TestNonReceiptOperationBehavior, self).setUp() + + # Get required models + self.StockMoveLine = self.env['stock.move.line'] + self.QualityCheck = self.env['quality.check'] + self.StockPicking = self.env['stock.picking'] + self.StockMove = self.env['stock.move'] + self.ProductProduct = self.env['product.product'] + self.StockLocation = self.env['stock.location'] + self.StockPickingType = self.env['stock.picking.type'] + self.StockLot = self.env['stock.lot'] + self.QualityPoint = self.env['quality.point'] + + # Get or create locations + self.stock_location = self.env.ref('stock.stock_location_stock') + self.customer_location = self.env.ref('stock.stock_location_customers') + + # Get or create different picking types for non-receipt operations + self.internal_picking_type = self._get_or_create_picking_type('internal', 'Internal Transfers') + self.outgoing_picking_type = self._get_or_create_picking_type('outgoing', 'Delivery Orders') + + def _get_or_create_picking_type(self, code, name): + """Helper to get or create a picking type""" + picking_type = self.env['stock.picking.type'].search([ + ('code', '=', code) + ], limit=1) + + if not picking_type: + warehouse = self.env['stock.warehouse'].search([], limit=1) + if not warehouse: + # Create a minimal warehouse if none exists + warehouse = self.env['stock.warehouse'].create({ + 'name': 'Test Warehouse', + 'code': 'TEST', + }) + + picking_type = self.env['stock.picking.type'].create({ + 'name': name, + 'code': code, + 'sequence_code': code.upper()[:5], + 'warehouse_id': warehouse.id, + }) + + return picking_type + + def _create_product_with_tracking(self, name): + """Helper to create a product with lot tracking enabled""" + return self.ProductProduct.create({ + 'name': name, + 'type': 'consu', # 'consu' for consumable/storable product in Odoo 18 + 'tracking': 'lot', + }) + + def _create_picking(self, picking_type, product, location_id, location_dest_id): + """Helper to create a picking with a move line for the given product""" + picking = self.StockPicking.create({ + 'picking_type_id': picking_type.id, + 'location_id': location_id.id, + 'location_dest_id': location_dest_id.id, + }) + + move = self.StockMove.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 10.0, + 'product_uom': product.uom_id.id, + 'picking_id': picking.id, + 'location_id': location_id.id, + 'location_dest_id': location_dest_id.id, + }) + + # Confirm the picking to create move lines + picking.action_confirm() + + return picking, move + + def _create_lot(self, product, lot_name): + """Helper to create a lot for a product""" + return self.StockLot.create({ + 'name': lot_name, + 'product_id': product.id, + 'company_id': self.env.company.id, + }) + + def _create_quality_check_for_move_line(self, move_line, picking_type, state='none'): + """Helper to create a quality check linked to a move line""" + # Get or create a quality team + quality_team = self.env['quality.alert.team'].search([], limit=1) + if not quality_team: + quality_team = self.env['quality.alert.team'].create({ + 'name': 'Test Quality Team', + }) + + # Create a quality point for the product if needed + quality_point = self.QualityPoint.search([ + ('product_ids', 'in', [move_line.product_id.id]), + ('picking_type_ids', 'in', [picking_type.id]), + ], limit=1) + + if not quality_point: + # Get a test type - use passfail which should always exist + test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False) + if not test_type: + # If no test type exists, create a minimal one + test_type = self.env['quality.point.test_type'].create({ + 'name': 'Pass/Fail Test', + 'technical_name': 'passfail', + }) + + quality_point = self.QualityPoint.create({ + 'title': f'Quality Check for {move_line.product_id.name}', + 'product_ids': [(4, move_line.product_id.id)], + 'picking_type_ids': [(4, picking_type.id)], + 'test_type_id': test_type.id, + 'team_id': quality_team.id, + }) + + return self.QualityCheck.create({ + 'product_id': move_line.product_id.id, + 'picking_id': move_line.picking_id.id, + 'move_line_id': move_line.id, + 'quality_state': state, + 'point_id': quality_point.id, + 'team_id': quality_team.id, + }) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + operation_type=st.sampled_from(['internal', 'outgoing']), + ) + def test_property_non_receipt_operation_not_identified_as_receipt(self, lot_name_seed, operation_type): + """ + Property: For any quality check associated with non-receipt operations, + the system should correctly identify it as NOT a receipt operation. + + This test verifies Requirement 4.2: + - When a quality check is associated with non-receipt operations (manufacturing, internal transfers, delivery), + the system should follow standard Odoo quality check behavior + + Test strategy: + 1. Create a non-receipt operation (internal or outgoing) + 2. Create a quality check for that operation + 3. Verify that: + a) The quality check is NOT identified as a receipt operation + b) The preservation behavior is NOT activated (_should_preserve_state returns False) + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Select the appropriate picking type based on operation_type + if operation_type == 'internal': + picking_type = self.internal_picking_type + location_id = self.stock_location + location_dest_id = self.stock_location # Internal transfer within same location + else: # outgoing + picking_type = self.outgoing_picking_type + location_id = self.stock_location + location_dest_id = self.customer_location + + # Create non-receipt picking + picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) + + # Verify this is NOT a receipt operation + self.assertNotEqual( + picking.picking_type_id.code, + 'incoming', + f"Picking should NOT be a receipt operation (code is '{picking.picking_type_id.code}')" + ) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Verify the move line is NOT identified as a receipt operation + self.assertFalse( + move_line._is_receipt_operation(), + f"Move line should NOT be identified as a receipt operation for {operation_type} operations" + ) + + # Create quality check + quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') + + # PROPERTY VERIFICATION PART 1: Non-receipt operation detection + # The quality check should NOT be identified as a receipt operation + self.assertFalse( + quality_check._is_receipt_operation(), + f"Quality check should NOT be identified as a receipt operation for {operation_type} operations" + ) + + # PROPERTY VERIFICATION PART 2: Preservation behavior NOT activated + # The quality check should indicate that state should NOT be preserved + self.assertFalse( + quality_check._should_preserve_state(), + f"Quality check should indicate that state preservation is NOT active for {operation_type} operations" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + operation_type=st.sampled_from(['internal', 'outgoing']), + ) + def test_property_non_receipt_operation_no_automatic_lot_propagation(self, lot_name_seed, operation_type): + """ + Property: For any non-receipt operation, when a lot is assigned to a stock move line, + the lot should NOT be automatically propagated to quality checks by our module. + + This verifies that our custom lot propagation logic only applies to receipt operations. + + Validates: Requirement 4.2 (standard behavior for non-receipt operations) + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Select the appropriate picking type based on operation_type + if operation_type == 'internal': + picking_type = self.internal_picking_type + location_id = self.stock_location + location_dest_id = self.stock_location + else: # outgoing + picking_type = self.outgoing_picking_type + location_id = self.stock_location + location_dest_id = self.customer_location + + # Create non-receipt picking + picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check WITHOUT a lot assigned + quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') + + # Verify no lot is assigned initially + self.assertFalse( + quality_check.lot_id, + "Quality check should not have a lot assigned initially" + ) + + # Create and assign a lot to the move line + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: Our custom lot propagation should NOT occur for non-receipt operations + # The quality check's lot_id should remain unset (or be set by standard Odoo behavior, not our module) + # We verify that our module's _update_quality_check_lot method was NOT triggered + # by checking that the quality check is correctly identified as non-receipt + self.assertFalse( + quality_check._is_receipt_operation(), + f"Quality check should be identified as non-receipt operation for {operation_type}" + ) + + # Our module should not have propagated the lot for non-receipt operations + # Note: Standard Odoo may or may not propagate the lot depending on its own logic, + # but our module should not interfere with non-receipt operations + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + operation_type=st.sampled_from(['internal', 'outgoing']), + ) + def test_property_non_receipt_operation_standard_behavior_preserved(self, lot_name_seed, operation_type): + """ + Property: For any non-receipt operation, the module should not interfere with + standard Odoo quality check behavior. + + This test verifies that our module's overrides correctly delegate to standard + behavior for non-receipt operations. + + Validates: Requirement 4.2 + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Select the appropriate picking type based on operation_type + if operation_type == 'internal': + picking_type = self.internal_picking_type + location_id = self.stock_location + location_dest_id = self.stock_location + else: # outgoing + picking_type = self.outgoing_picking_type + location_id = self.stock_location + location_dest_id = self.customer_location + + # Create non-receipt picking + picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') + + # PROPERTY VERIFICATION: The module should correctly identify this as non-receipt + # and delegate to standard behavior + self.assertFalse( + quality_check._is_receipt_operation(), + f"Quality check should be identified as non-receipt for {operation_type}" + ) + + # Verify that _should_preserve_state returns False for non-receipt operations + self.assertFalse( + quality_check._should_preserve_state(), + f"State preservation should NOT be active for {operation_type} operations" + ) + + # Verify that _update_lot_from_lot_line delegates to parent for non-receipt operations + # This method should return the result of super() for non-receipt operations + # We can't easily test the return value, but we can verify the operation type detection works + self.assertEqual( + picking.picking_type_id.code, + operation_type, + f"Picking type code should be '{operation_type}'" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + ) + def test_property_non_receipt_internal_operation_detection(self, lot_name_seed): + """ + Property: For any internal transfer operation, the system should correctly + identify it as a non-receipt operation and not apply lot preservation behavior. + + Validates: Requirement 4.2 (internal transfers use standard behavior) + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create internal transfer + picking, move = self._create_picking( + self.internal_picking_type, + product, + self.stock_location, + self.stock_location + ) + + # Verify this is an internal operation + self.assertEqual( + picking.picking_type_id.code, + 'internal', + "Picking should be an internal transfer" + ) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line( + move_line, + self.internal_picking_type, + state='pass' + ) + + # PROPERTY VERIFICATION: Internal operations should not trigger preservation behavior + self.assertFalse( + quality_check._is_receipt_operation(), + "Internal transfer should NOT be identified as receipt operation" + ) + + self.assertFalse( + quality_check._should_preserve_state(), + "State preservation should NOT be active for internal transfers" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + ) + def test_property_non_receipt_outgoing_operation_detection(self, lot_name_seed): + """ + Property: For any outgoing/delivery operation, the system should correctly + identify it as a non-receipt operation and not apply lot preservation behavior. + + Validates: Requirement 4.2 (delivery operations use standard behavior) + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create outgoing/delivery operation + picking, move = self._create_picking( + self.outgoing_picking_type, + product, + self.stock_location, + self.customer_location + ) + + # Verify this is an outgoing operation + self.assertEqual( + picking.picking_type_id.code, + 'outgoing', + "Picking should be an outgoing/delivery operation" + ) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line( + move_line, + self.outgoing_picking_type, + state='pass' + ) + + # PROPERTY VERIFICATION: Outgoing operations should not trigger preservation behavior + self.assertFalse( + quality_check._is_receipt_operation(), + "Outgoing/delivery operation should NOT be identified as receipt operation" + ) + + self.assertFalse( + quality_check._should_preserve_state(), + "State preservation should NOT be active for outgoing/delivery operations" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + operation_type=st.sampled_from(['internal', 'outgoing']), + ) + def test_property_non_receipt_operation_type_consistency(self, lot_name_seed, operation_type): + """ + Property: For any non-receipt operation, the operation type detection should be + consistent across multiple checks and throughout the quality check lifecycle. + + Validates: Requirement 4.2 + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Select the appropriate picking type + if operation_type == 'internal': + picking_type = self.internal_picking_type + location_id = self.stock_location + location_dest_id = self.stock_location + else: # outgoing + picking_type = self.outgoing_picking_type + location_id = self.stock_location + location_dest_id = self.customer_location + + # Create non-receipt picking + picking, move = self._create_picking(picking_type, product, location_id, location_dest_id) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line(move_line, picking_type, state='pass') + + # PROPERTY VERIFICATION: Operation type detection should be consistent + + # Check 1: Initial detection + is_receipt_1 = quality_check._is_receipt_operation() + self.assertFalse(is_receipt_1, f"Should not be receipt operation (check 1) for {operation_type}") + + # Check 2: After invalidating cache + quality_check.invalidate_recordset() + is_receipt_2 = quality_check._is_receipt_operation() + self.assertFalse(is_receipt_2, f"Should not be receipt operation (check 2) for {operation_type}") + + # Check 3: Consistency between checks + self.assertEqual( + is_receipt_1, + is_receipt_2, + "Operation type detection should be consistent across multiple checks" + ) + + # Check 4: _should_preserve_state should also be consistent + should_preserve_1 = quality_check._should_preserve_state() + quality_check.invalidate_recordset() + should_preserve_2 = quality_check._should_preserve_state() + + self.assertFalse(should_preserve_1, "Should not preserve state for non-receipt operations") + self.assertFalse(should_preserve_2, "Should not preserve state for non-receipt operations") + self.assertEqual( + should_preserve_1, + should_preserve_2, + "State preservation detection should be consistent" + ) diff --git a/tests/test_property_receipt_operation.py b/tests/test_property_receipt_operation.py new file mode 100644 index 0000000..596cc95 --- /dev/null +++ b/tests/test_property_receipt_operation.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import TransactionCase +from hypothesis import given, strategies as st, settings, assume + + +class TestReceiptOperationBehavior(TransactionCase): + """ + Property-based tests for receipt operation behavior activation. + + Feature: quality-check-lot-preserve, Property 5: Receipt operation behavior activation + + Property: For any quality check associated with a receipt operation (picking type code = 'incoming'), + the lot preservation behavior should be applied when lot numbers are assigned. + + Validates: Requirements 4.1 + """ + + def setUp(self): + super(TestReceiptOperationBehavior, self).setUp() + + # Get required models + self.StockMoveLine = self.env['stock.move.line'] + self.QualityCheck = self.env['quality.check'] + self.StockPicking = self.env['stock.picking'] + self.StockMove = self.env['stock.move'] + self.ProductProduct = self.env['product.product'] + self.StockLocation = self.env['stock.location'] + self.StockPickingType = self.env['stock.picking.type'] + self.StockLot = self.env['stock.lot'] + self.QualityPoint = self.env['quality.point'] + + # Get or create locations + self.supplier_location = self.env.ref('stock.stock_location_suppliers') + self.stock_location = self.env.ref('stock.stock_location_stock') + + # Get or create receipt picking type + self.receipt_picking_type = self.env['stock.picking.type'].search([ + ('code', '=', 'incoming') + ], limit=1) + + if not self.receipt_picking_type: + self.receipt_picking_type = self.env['stock.picking.type'].create({ + 'name': 'Receipts', + 'code': 'incoming', + 'sequence_code': 'IN', + 'warehouse_id': self.env['stock.warehouse'].search([], limit=1).id, + }) + + def _create_product_with_tracking(self, name): + """Helper to create a product with lot tracking enabled""" + return self.ProductProduct.create({ + 'name': name, + 'type': 'consu', # 'consu' for consumable/storable product in Odoo 18 + 'tracking': 'lot', + }) + + def _create_receipt_picking(self, product): + """Helper to create a receipt picking with a move line for the given product""" + picking = self.StockPicking.create({ + 'picking_type_id': self.receipt_picking_type.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + move = self.StockMove.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 10.0, + 'product_uom': product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + # Confirm the picking to create move lines + picking.action_confirm() + + return picking, move + + def _create_lot(self, product, lot_name): + """Helper to create a lot for a product""" + return self.StockLot.create({ + 'name': lot_name, + 'product_id': product.id, + 'company_id': self.env.company.id, + }) + + def _create_quality_check_for_move_line(self, move_line, state='none'): + """Helper to create a quality check linked to a move line""" + # Get or create a quality team + quality_team = self.env['quality.alert.team'].search([], limit=1) + if not quality_team: + quality_team = self.env['quality.alert.team'].create({ + 'name': 'Test Quality Team', + }) + + # Create a quality point for the product if needed + quality_point = self.QualityPoint.search([ + ('product_ids', 'in', [move_line.product_id.id]), + ('picking_type_ids', 'in', [self.receipt_picking_type.id]), + ], limit=1) + + if not quality_point: + # Get a test type - use passfail which should always exist + test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False) + if not test_type: + # If no test type exists, create a minimal one + test_type = self.env['quality.point.test_type'].create({ + 'name': 'Pass/Fail Test', + 'technical_name': 'passfail', + }) + + quality_point = self.QualityPoint.create({ + 'title': f'Quality Check for {move_line.product_id.name}', + 'product_ids': [(4, move_line.product_id.id)], + 'picking_type_ids': [(4, self.receipt_picking_type.id)], + 'test_type_id': test_type.id, + 'team_id': quality_team.id, + }) + + return self.QualityCheck.create({ + 'product_id': move_line.product_id.id, + 'picking_id': move_line.picking_id.id, + 'move_line_id': move_line.id, + 'quality_state': state, + 'point_id': quality_point.id, + 'team_id': quality_team.id, + }) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_receipt_operation_activates_preservation(self, lot_name_seed, quality_state): + """ + Property: For any quality check associated with a receipt operation (picking type code = 'incoming'), + the lot preservation behavior should be applied when lot numbers are assigned. + + This test verifies Requirement 4.1: + - When a quality check is associated with a receipt operation (incoming shipment), + the system applies the lot preservation behavior + + Test strategy: + 1. Create a receipt operation (picking_type_code = 'incoming') + 2. Create a quality check with a specific state + 3. Assign a lot number to the stock move line + 4. Verify that: + a) The quality check is correctly identified as a receipt operation + b) The lot preservation behavior is activated (_should_preserve_state returns True) + c) The quality check state is preserved (demonstrating the behavior is active) + d) The lot number is propagated to the quality check + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking (this is the key: picking_type_code = 'incoming') + picking, move = self._create_receipt_picking(product) + + # Verify this is indeed a receipt operation + self.assertEqual( + picking.picking_type_id.code, + 'incoming', + "Picking should be a receipt operation (incoming)" + ) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Verify the move line is identified as a receipt operation + self.assertTrue( + move_line._is_receipt_operation(), + "Move line should be identified as a receipt operation" + ) + + # Create quality check with the specified state + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # PROPERTY VERIFICATION PART 1: Receipt operation detection + # The quality check should be correctly identified as a receipt operation + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should be identified as a receipt operation" + ) + + # PROPERTY VERIFICATION PART 2: Preservation behavior activation + # The quality check should indicate that state should be preserved + self.assertTrue( + quality_check._should_preserve_state(), + "Quality check should indicate that state preservation is active for receipt operations" + ) + + # Record the initial quality check state + initial_state = quality_check.quality_state + + # Create and assign a lot number to the move line + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION PART 3: State preservation (demonstrating behavior is active) + # The quality check state should remain unchanged, proving the preservation behavior is active + self.assertEqual( + quality_check.quality_state, + initial_state, + f"Quality check state should remain '{initial_state}' after lot assignment, " + f"demonstrating that lot preservation behavior is active for receipt operations" + ) + + # PROPERTY VERIFICATION PART 4: Lot propagation (demonstrating behavior is active) + # The lot should be propagated to the quality check + self.assertEqual( + quality_check.lot_id.id, + lot.id, + f"Quality check should have lot {lot.name} assigned, " + f"demonstrating that lot propagation is active for receipt operations" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + ) + def test_property_receipt_operation_detection_robustness(self, lot_name_seed): + """ + Property: For any receipt operation, the system should correctly identify it + as a receipt operation through multiple detection paths. + + This test verifies that the receipt operation detection is robust and works + through different access paths (picking_id, move_id, etc.). + + Validates: Requirement 4.1 (operation type identification) + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check + quality_check = self._create_quality_check_for_move_line(move_line, state='pass') + + # PROPERTY VERIFICATION: Multiple detection paths should all identify this as a receipt operation + + # Path 1: Through picking_id.picking_type_id.code + self.assertEqual( + picking.picking_type_id.code, + 'incoming', + "Picking type code should be 'incoming'" + ) + + # Path 2: Through move_line._is_receipt_operation() + self.assertTrue( + move_line._is_receipt_operation(), + "Move line should be identified as receipt operation" + ) + + # Path 3: Through quality_check._is_receipt_operation() + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should be identified as receipt operation" + ) + + # Path 4: Through quality_check.picking_id.picking_type_id.code + if quality_check.picking_id: + self.assertEqual( + quality_check.picking_id.picking_type_id.code, + 'incoming', + "Quality check's picking should have 'incoming' code" + ) + + # Path 5: Through quality_check.move_line_id.picking_id.picking_type_id.code + if quality_check.move_line_id and quality_check.move_line_id.picking_id: + self.assertEqual( + quality_check.move_line_id.picking_id.picking_type_id.code, + 'incoming', + "Quality check's move line's picking should have 'incoming' code" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + initial_state=st.sampled_from(['pass', 'fail']), + ) + def test_property_receipt_operation_preserves_completed_checks(self, lot_name_seed, initial_state): + """ + Property: For any completed quality check (pass or fail) on a receipt operation, + the lot preservation behavior should prevent state reset when lot is assigned. + + This is a critical test that demonstrates the core value of the module: + quality checks completed before lot assignment should not be reset. + + Validates: Requirement 4.1 (receipt operation behavior application) + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line (no lot assigned yet) + move_line = picking.move_line_ids[0] + + # Create quality check with completed state (pass or fail) + quality_check = self._create_quality_check_for_move_line(move_line, state=initial_state) + + # Verify this is a receipt operation + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should be for a receipt operation" + ) + + # Verify the quality check is in the expected completed state + self.assertEqual( + quality_check.quality_state, + initial_state, + f"Quality check should be in '{initial_state}' state before lot assignment" + ) + + # Now assign a lot number (this is when standard Odoo would reset the state) + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: The completed quality check state should be preserved + # This demonstrates that the receipt operation behavior is active and working + self.assertEqual( + quality_check.quality_state, + initial_state, + f"Quality check should remain in '{initial_state}' state after lot assignment. " + f"This demonstrates that the lot preservation behavior is active for receipt operations." + ) + + # Additionally verify the lot was assigned + self.assertEqual( + quality_check.lot_id.id, + lot.id, + "Quality check should have the lot assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + ) + def test_property_receipt_operation_behavior_consistency(self, lot_name_seed): + """ + Property: For any receipt operation, the behavior should be consistent + across multiple lot assignments and updates. + + This test verifies that the receipt operation behavior remains active + throughout the lifecycle of the quality check. + + Validates: Requirement 4.1 + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check with 'pass' state + quality_check = self._create_quality_check_for_move_line(move_line, state='pass') + + # Verify receipt operation detection + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should be identified as receipt operation" + ) + + # First lot assignment + lot1 = self._create_lot(product, f'LOT-1-{lot_name_seed}') + move_line.write({'lot_id': lot1.id}) + quality_check.invalidate_recordset() + + # Verify state preserved after first assignment + self.assertEqual( + quality_check.quality_state, + 'pass', + "State should be preserved after first lot assignment" + ) + + # Verify receipt operation detection still works + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should still be identified as receipt operation after lot assignment" + ) + + # Second lot assignment (lot change) + lot2 = self._create_lot(product, f'LOT-2-{lot_name_seed}') + move_line.write({'lot_id': lot2.id}) + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: State should still be preserved after lot change + self.assertEqual( + quality_check.quality_state, + 'pass', + "State should be preserved after lot change, demonstrating consistent behavior" + ) + + # Verify the new lot is assigned + self.assertEqual( + quality_check.lot_id.id, + lot2.id, + "Quality check should have the new lot assigned" + ) + + # Verify receipt operation detection still works after multiple updates + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should still be identified as receipt operation after multiple updates" + ) diff --git a/tests/test_property_relationship_integrity.py b/tests/test_property_relationship_integrity.py new file mode 100644 index 0000000..90e585e --- /dev/null +++ b/tests/test_property_relationship_integrity.py @@ -0,0 +1,399 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import TransactionCase +from hypothesis import given, strategies as st, settings, assume + + +class TestRelationshipIntegrity(TransactionCase): + """ + Property-based tests for relationship integrity during lot number updates. + + Feature: quality-check-lot-preserve, Property 4: Relationship integrity during updates + + Property: For any quality check linked to a stock move line, when lot number updates + occur, the link between the quality check and stock move line should remain intact + (move_line_id unchanged). + + Validates: Requirements 3.3 + """ + + def setUp(self): + super(TestRelationshipIntegrity, self).setUp() + + # Get required models + self.StockMoveLine = self.env['stock.move.line'] + self.QualityCheck = self.env['quality.check'] + self.StockPicking = self.env['stock.picking'] + self.StockMove = self.env['stock.move'] + self.ProductProduct = self.env['product.product'] + self.StockLocation = self.env['stock.location'] + self.StockPickingType = self.env['stock.picking.type'] + self.StockLot = self.env['stock.lot'] + self.QualityPoint = self.env['quality.point'] + + # Get or create locations + self.supplier_location = self.env.ref('stock.stock_location_suppliers') + self.stock_location = self.env.ref('stock.stock_location_stock') + + # Get or create receipt picking type + self.receipt_picking_type = self.env['stock.picking.type'].search([ + ('code', '=', 'incoming') + ], limit=1) + + if not self.receipt_picking_type: + self.receipt_picking_type = self.env['stock.picking.type'].create({ + 'name': 'Receipts', + 'code': 'incoming', + 'sequence_code': 'IN', + 'warehouse_id': self.env['stock.warehouse'].search([], limit=1).id, + }) + + def _create_product_with_tracking(self, name): + """Helper to create a product with lot tracking enabled""" + return self.ProductProduct.create({ + 'name': name, + 'type': 'consu', + 'tracking': 'lot', + }) + + def _create_receipt_picking(self, product): + """Helper to create a receipt picking with a move line for the given product""" + picking = self.StockPicking.create({ + 'picking_type_id': self.receipt_picking_type.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + move = self.StockMove.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 10.0, + 'product_uom': product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + # Confirm the picking to create move lines + picking.action_confirm() + + return picking, move + + def _create_lot(self, product, lot_name): + """Helper to create a lot for a product""" + return self.StockLot.create({ + 'name': lot_name, + 'product_id': product.id, + 'company_id': self.env.company.id, + }) + + def _create_quality_check_for_move_line(self, move_line, state='none'): + """Helper to create a quality check linked to a move line""" + # Get or create a quality team + quality_team = self.env['quality.alert.team'].search([], limit=1) + if not quality_team: + quality_team = self.env['quality.alert.team'].create({ + 'name': 'Test Quality Team', + }) + + # Create a quality point for the product if needed + quality_point = self.QualityPoint.search([ + ('product_ids', 'in', [move_line.product_id.id]), + ('picking_type_ids', 'in', [self.receipt_picking_type.id]), + ], limit=1) + + if not quality_point: + # Get a test type - use passfail which should always exist + test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False) + if not test_type: + # If no test type exists, create a minimal one + test_type = self.env['quality.point.test_type'].create({ + 'name': 'Pass/Fail Test', + 'technical_name': 'passfail', + }) + + quality_point = self.QualityPoint.create({ + 'title': f'Quality Check for {move_line.product_id.name}', + 'product_ids': [(4, move_line.product_id.id)], + 'picking_type_ids': [(4, self.receipt_picking_type.id)], + 'test_type_id': test_type.id, + 'team_id': quality_team.id, + }) + + return self.QualityCheck.create({ + 'product_id': move_line.product_id.id, + 'picking_id': move_line.picking_id.id, + 'move_line_id': move_line.id, + 'quality_state': state, + 'point_id': quality_point.id, + 'team_id': quality_team.id, + }) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_relationship_integrity_on_initial_lot_assignment(self, lot_name_seed, quality_state): + """ + Property: For any quality check linked to a stock move line, when a lot number + is initially assigned to the move line, the link between the quality check and + stock move line should remain intact. + + This test verifies Requirement 3.3: + - When a lot number update occurs, the system maintains the link between + the quality check and the stock move line + + Test strategy: + 1. Create a receipt with a quality check linked to a move line + 2. Record the initial move_line_id relationship + 3. Assign a lot number to the move line + 4. Verify the move_line_id relationship remains unchanged + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check linked to the move line + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # Record the initial move_line_id relationship + initial_move_line_id = quality_check.move_line_id.id + + # Verify the relationship is established + self.assertEqual( + initial_move_line_id, + move_line.id, + "Quality check should be linked to the move line initially" + ) + + # Create and assign a lot number to the move line + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # The move_line_id relationship should remain unchanged after lot assignment + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + f"Quality check should still be linked to move line {initial_move_line_id} " + f"after lot assignment" + ) + + # Additionally verify the lot was actually assigned + self.assertEqual( + quality_check.lot_id.id, + lot.id, + "Quality check should have the lot assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + initial_lot_seed=st.integers(min_value=1, max_value=1000000), + updated_lot_seed=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_relationship_integrity_on_lot_update(self, initial_lot_seed, updated_lot_seed, quality_state): + """ + Property: For any quality check linked to a stock move line with an assigned lot, + when the lot number is changed on the move line, the link between the quality check + and stock move line should remain intact. + + This test verifies that relationship integrity is maintained even when lot numbers + are updated (not just initially assigned). + + Validates: Requirement 3.3 + """ + # Ensure the two lot numbers are different + assume(initial_lot_seed != updated_lot_seed) + + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {initial_lot_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check linked to the move line + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # Record the initial move_line_id relationship + initial_move_line_id = quality_check.move_line_id.id + + # Assign initial lot number + initial_lot = self._create_lot(product, f'LOT-INITIAL-{initial_lot_seed}') + move_line.write({'lot_id': initial_lot.id}) + + # Refresh and verify relationship is still intact after first assignment + quality_check.invalidate_recordset() + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + "Relationship should be intact after initial lot assignment" + ) + + # Create a new lot and update the move line + updated_lot = self._create_lot(product, f'LOT-UPDATED-{updated_lot_seed}') + move_line.write({'lot_id': updated_lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # The move_line_id relationship should remain unchanged after lot update + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + f"Quality check should still be linked to move line {initial_move_line_id} " + f"after lot update" + ) + + # Verify the lot was actually updated + self.assertEqual( + quality_check.lot_id.id, + updated_lot.id, + "Quality check should have the updated lot assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_seed_1=st.integers(min_value=1, max_value=1000000), + lot_seed_2=st.integers(min_value=1, max_value=1000000), + lot_seed_3=st.integers(min_value=1, max_value=1000000), + ) + def test_property_relationship_integrity_multiple_updates(self, lot_seed_1, lot_seed_2, lot_seed_3): + """ + Property: For any quality check linked to a stock move line, when multiple + consecutive lot number updates occur, the link between the quality check and + stock move line should remain intact throughout all updates. + + This test verifies that relationship integrity is maintained even with + multiple sequential lot updates. + + Validates: Requirement 3.3 + """ + # Ensure all lot numbers are different + assume(lot_seed_1 != lot_seed_2) + assume(lot_seed_2 != lot_seed_3) + assume(lot_seed_1 != lot_seed_3) + + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_seed_1}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check linked to the move line + quality_check = self._create_quality_check_for_move_line(move_line, state='pass') + + # Record the initial move_line_id relationship + initial_move_line_id = quality_check.move_line_id.id + + # Create three different lots + lot_1 = self._create_lot(product, f'LOT-1-{lot_seed_1}') + lot_2 = self._create_lot(product, f'LOT-2-{lot_seed_2}') + lot_3 = self._create_lot(product, f'LOT-3-{lot_seed_3}') + + # First update: assign lot_1 + move_line.write({'lot_id': lot_1.id}) + quality_check.invalidate_recordset() + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + "Relationship should be intact after first lot assignment" + ) + + # Second update: change to lot_2 + move_line.write({'lot_id': lot_2.id}) + quality_check.invalidate_recordset() + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + "Relationship should be intact after second lot update" + ) + + # Third update: change to lot_3 + move_line.write({'lot_id': lot_3.id}) + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # The move_line_id relationship should remain unchanged after all updates + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + f"Quality check should still be linked to move line {initial_move_line_id} " + f"after multiple lot updates" + ) + + # Verify the final lot was assigned + self.assertEqual( + quality_check.lot_id.id, + lot_3.id, + "Quality check should have the final lot assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + num_updates=st.integers(min_value=1, max_value=5), + ) + def test_property_relationship_integrity_variable_updates(self, lot_name_seed, num_updates): + """ + Property: For any quality check linked to a stock move line, regardless of + the number of lot updates, the move_line_id relationship should never change. + + This test uses a variable number of updates to verify relationship integrity + across different update patterns. + + Validates: Requirement 3.3 + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check linked to the move line + quality_check = self._create_quality_check_for_move_line(move_line, state='pass') + + # Record the initial move_line_id relationship + initial_move_line_id = quality_check.move_line_id.id + + # Perform multiple lot updates + for i in range(num_updates): + lot = self._create_lot(product, f'LOT-{lot_name_seed}-{i}') + move_line.write({'lot_id': lot.id}) + + # Refresh and verify relationship after each update + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: Relationship should be intact after each update + self.assertEqual( + quality_check.move_line_id.id, + initial_move_line_id, + f"Quality check should still be linked to move line {initial_move_line_id} " + f"after update {i+1} of {num_updates}" + ) + + # Verify the lot was updated + self.assertEqual( + quality_check.lot_id.id, + lot.id, + f"Quality check should have lot {lot.name} assigned after update {i+1}" + ) diff --git a/tests/test_property_state_preservation.py b/tests/test_property_state_preservation.py new file mode 100644 index 0000000..aea436c --- /dev/null +++ b/tests/test_property_state_preservation.py @@ -0,0 +1,369 @@ +# -*- coding: utf-8 -*- + +from odoo.tests.common import TransactionCase +from hypothesis import given, strategies as st, settings, assume + + +class TestStatePreservation(TransactionCase): + """ + Property-based tests for quality check state preservation during lot assignment. + + Feature: quality-check-lot-preserve, Property 1: State preservation during lot assignment + + Property: For any quality check on a receipt operation in any state (Pass, Fail, + In Progress, Todo), when a lot number is assigned to the related stock move line, + the quality check state should remain unchanged. + + Validates: Requirements 1.1, 1.2 + """ + + def setUp(self): + super(TestStatePreservation, self).setUp() + + # Get required models + self.StockMoveLine = self.env['stock.move.line'] + self.QualityCheck = self.env['quality.check'] + self.StockPicking = self.env['stock.picking'] + self.StockMove = self.env['stock.move'] + self.ProductProduct = self.env['product.product'] + self.StockLocation = self.env['stock.location'] + self.StockPickingType = self.env['stock.picking.type'] + self.StockLot = self.env['stock.lot'] + self.QualityPoint = self.env['quality.point'] + + # Get or create locations + self.supplier_location = self.env.ref('stock.stock_location_suppliers') + self.stock_location = self.env.ref('stock.stock_location_stock') + + # Get or create receipt picking type + self.receipt_picking_type = self.env['stock.picking.type'].search([ + ('code', '=', 'incoming') + ], limit=1) + + if not self.receipt_picking_type: + self.receipt_picking_type = self.env['stock.picking.type'].create({ + 'name': 'Receipts', + 'code': 'incoming', + 'sequence_code': 'IN', + 'warehouse_id': self.env['stock.warehouse'].search([], limit=1).id, + }) + + def _create_product_with_tracking(self, name): + """Helper to create a product with lot tracking enabled""" + return self.ProductProduct.create({ + 'name': name, + 'type': 'consu', # 'consu' for consumable/storable product in Odoo 18 + 'tracking': 'lot', + }) + + def _create_receipt_picking(self, product): + """Helper to create a receipt picking with a move line for the given product""" + picking = self.StockPicking.create({ + 'picking_type_id': self.receipt_picking_type.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + move = self.StockMove.create({ + 'name': product.name, + 'product_id': product.id, + 'product_uom_qty': 10.0, + 'product_uom': product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.supplier_location.id, + 'location_dest_id': self.stock_location.id, + }) + + # Confirm the picking to create move lines + picking.action_confirm() + + return picking, move + + def _create_lot(self, product, lot_name): + """Helper to create a lot for a product""" + return self.StockLot.create({ + 'name': lot_name, + 'product_id': product.id, + 'company_id': self.env.company.id, + }) + + def _create_quality_check_for_move_line(self, move_line, state='none'): + """Helper to create a quality check linked to a move line""" + # Get or create a quality team + quality_team = self.env['quality.alert.team'].search([], limit=1) + if not quality_team: + quality_team = self.env['quality.alert.team'].create({ + 'name': 'Test Quality Team', + }) + + # Create a quality point for the product if needed + quality_point = self.QualityPoint.search([ + ('product_ids', 'in', [move_line.product_id.id]), + ('picking_type_ids', 'in', [self.receipt_picking_type.id]), + ], limit=1) + + if not quality_point: + # Get a test type - use passfail which should always exist + test_type = self.env.ref('quality_control.test_type_passfail', raise_if_not_found=False) + if not test_type: + # If no test type exists, create a minimal one + test_type = self.env['quality.point.test_type'].create({ + 'name': 'Pass/Fail Test', + 'technical_name': 'passfail', + }) + + quality_point = self.QualityPoint.create({ + 'title': f'Quality Check for {move_line.product_id.name}', + 'product_ids': [(4, move_line.product_id.id)], + 'picking_type_ids': [(4, self.receipt_picking_type.id)], + 'test_type_id': test_type.id, + 'team_id': quality_team.id, + }) + + return self.QualityCheck.create({ + 'product_id': move_line.product_id.id, + 'picking_id': move_line.picking_id.id, + 'move_line_id': move_line.id, + 'quality_state': state, + 'point_id': quality_point.id, + 'team_id': quality_team.id, + }) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_state_preservation_on_lot_assignment(self, lot_name_seed, quality_state): + """ + Property: For any quality check on a receipt operation in any state, + when a lot number is assigned to the related stock move line, + the quality check state should remain unchanged. + + This test verifies Requirements 1.1 and 1.2: + - 1.1: Quality check state is preserved regardless of subsequent lot number assignment + - 1.2: Quality check maintains its state (Pass, Fail, In Progress, Todo) when lot is assigned + + Test strategy: + 1. Create a receipt operation with a quality check in a specific state + 2. Record the initial state + 3. Assign a lot number to the stock move line + 4. Verify the quality check state remains unchanged + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check with the specified state + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # Record the initial quality check state + initial_state = quality_check.quality_state + + # Verify the quality check is for a receipt operation + self.assertTrue( + quality_check._is_receipt_operation(), + "Quality check should be identified as a receipt operation" + ) + + # Create and assign a lot number to the move line + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + + # Assign the lot to the move line (this is the critical action) + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database to get latest state + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # The quality check state should remain unchanged after lot assignment + self.assertEqual( + quality_check.quality_state, + initial_state, + f"Quality check state should remain '{initial_state}' after lot assignment, " + f"but found '{quality_check.quality_state}'" + ) + + # Additionally verify the lot was actually assigned to the quality check + self.assertEqual( + quality_check.lot_id.id, + lot.id, + f"Quality check should have lot {lot.name} assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + ) + def test_property_pass_state_preservation(self, lot_name_seed): + """ + Specific property test for "Pass" state preservation. + + Property: For any quality check marked as "Pass" on a receipt operation, + when a lot number is assigned, the quality check should remain in "Pass" state. + + This test specifically validates Requirement 1.3: + - When a quality check is marked as "Pass" before lot assignment, + the system keeps the quality check as "Pass" after lot number assignment + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check with "pass" state + quality_check = self._create_quality_check_for_move_line(move_line, state='pass') + + # Verify initial state is "pass" + self.assertEqual( + quality_check.quality_state, + 'pass', + "Quality check should initially be in 'pass' state" + ) + + # Create and assign a lot number + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: Quality check should still be in "pass" state + self.assertEqual( + quality_check.quality_state, + 'pass', + "Quality check should remain in 'pass' state after lot assignment" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_name_seed=st.integers(min_value=1, max_value=1000000), + initial_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_state_preservation_without_initial_lot(self, lot_name_seed, initial_state): + """ + Property: For any quality check created without a lot number, + when a lot number is later assigned to the move line, + the quality check state should be preserved. + + This tests the common workflow where quality checks are performed + before lot numbers are generated or assigned. + """ + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_name_seed}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line (no lot assigned yet) + move_line = picking.move_line_ids[0] + + # Verify no lot is assigned initially + self.assertFalse(move_line.lot_id, "Move line should not have a lot initially") + + # Create quality check with the specified state (no lot assigned) + quality_check = self._create_quality_check_for_move_line(move_line, state=initial_state) + + # Verify no lot is assigned to quality check initially + self.assertFalse(quality_check.lot_id, "Quality check should not have a lot initially") + + # Record the initial state + recorded_state = quality_check.quality_state + + # Now assign a lot number (simulating lot generation after quality check) + lot = self._create_lot(product, f'LOT-{lot_name_seed}') + move_line.write({'lot_id': lot.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # 1. State should be preserved + self.assertEqual( + quality_check.quality_state, + recorded_state, + f"Quality check state should remain '{recorded_state}' after lot assignment" + ) + + # 2. Lot should be assigned to quality check + self.assertEqual( + quality_check.lot_id.id, + lot.id, + "Quality check should have the lot assigned" + ) + + @settings(max_examples=100, deadline=None) + @given( + lot_seed_1=st.integers(min_value=1, max_value=1000000), + lot_seed_2=st.integers(min_value=1, max_value=1000000), + quality_state=st.sampled_from(['none', 'pass', 'fail']), + ) + def test_property_state_preservation_on_lot_change(self, lot_seed_1, lot_seed_2, quality_state): + """ + Property: For any quality check with an assigned lot number, + when the lot number is changed on the move line, + the quality check state should remain unchanged. + + This verifies that state preservation works not just for initial assignment, + but also for lot number changes. + """ + # Ensure the two lot numbers are different + assume(lot_seed_1 != lot_seed_2) + + # Create a product with lot tracking + product = self._create_product_with_tracking(f'Test Product {lot_seed_1}') + + # Create receipt picking + picking, move = self._create_receipt_picking(product) + + # Get the move line + move_line = picking.move_line_ids[0] + + # Create quality check with the specified state + quality_check = self._create_quality_check_for_move_line(move_line, state=quality_state) + + # Assign initial lot number + lot_1 = self._create_lot(product, f'LOT-1-{lot_seed_1}') + move_line.write({'lot_id': lot_1.id}) + + # Refresh and record the state after first lot assignment + quality_check.invalidate_recordset() + state_after_first_assignment = quality_check.quality_state + + # Verify state is still the original state + self.assertEqual( + state_after_first_assignment, + quality_state, + "State should be preserved after first lot assignment" + ) + + # Now change the lot number + lot_2 = self._create_lot(product, f'LOT-2-{lot_seed_2}') + move_line.write({'lot_id': lot_2.id}) + + # Refresh quality check from database + quality_check.invalidate_recordset() + + # PROPERTY VERIFICATION: + # State should still be the original state after lot change + self.assertEqual( + quality_check.quality_state, + quality_state, + f"Quality check state should remain '{quality_state}' after lot change" + ) + + # Verify the lot was actually changed + self.assertEqual( + quality_check.lot_id.id, + lot_2.id, + "Quality check should have the new lot assigned" + )