diff --git a/.circleci/config.yml b/.circleci/config.yml index e59d8566..79ee144f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,43 +8,60 @@ jobs: tests: machine: true steps: - - checkout + - checkout - - run: - <<: *quay_io_login + - run: + <<: *quay_io_login - - run: + - run: name: Build -- Init Database command: docker-compose run --rm odoo odoo --stop-after-init -i main - - run: + - run: name: Setup Log Folder For Reports command: sudo mkdir -p .log && sudo chmod 777 .log - - run: - name: Run Test - command: docker-compose run --rm odoo run_pytest.sh + - run: + name: Run Test and Generate Coverage + # Add the --junitxml parameter to generate test results in a specific subfolder. + # This prevents CircleCI from confusing coverage.xml with JUnit test results. + command: | + docker-compose run --rm odoo bash -c "mkdir -p /var/log/odoo/test-results && cd /var/log/odoo && pytest /mnt/extra-addons -v --disable-warnings --cov=/mnt/extra-addons --cov-branch --cov-report=xml --junitxml=/var/log/odoo/test-results/junit.xml" - - run: + - run: name: Codacy Coverage - command: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l python -r .log/coverage.xml + # Always run this step to upload coverage even if tests fail + when: always + command: | + # Verify the file is successfully passed to the host machine + if [ -f ".log/coverage.xml" ]; then + bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l python -r .log/coverage.xml + else + echo "Error: coverage.xml not found in .log directory." + exit 1 + fi - - store_test_results: - path: .log + - store_test_results: + # Point strictly to the subfolder containing the JUnit XML + # so CircleCI ignores the coverage.xml file. + path: .log/test-results - # job that find the next tag for the current branch/repo and push the tag to github. - # it will trigger the publish of a new docker image. + # Job that finds the next tag for the current branch/repo and pushes the tag to github. + # It will trigger the publish of a new docker image. auto-tag: machine: true steps: - checkout + - run: <<: *quay_io_login + - run: name: Get nws command: | curl -L $NWS_BIN_LOCATION > ./nws chmod +x ./nws + - run: name: Set tag command: | @@ -63,4 +80,4 @@ workflows: - tests filters: branches: - only: /^1\d\.0/ + only: /^1\d\.0/ \ No newline at end of file diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 00000000..d1e1e566 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,11 @@ +# .codacy.yml +# Exclude folders and heavy files from Codacy static analysis to prevent Diff/Deltas timeout +exclude_paths: + - '**/data/**' + - '**/static/lib/**' + - '**/tests/**' + - '**/doc/**' + - '**/i18n/**' + - '**/*.csv' + - '**/*.po' + - '**/*.pot' \ No newline at end of file diff --git a/gitoo.yml b/gitoo.yml index 3e9fdeda..c100e442 100644 --- a/gitoo.yml +++ b/gitoo.yml @@ -11,6 +11,12 @@ includes: - account_asset_management +- url: https://github.com/OCA/dms + branch: "14.0" + includes: + - dms + + - url: https://github.com/oca/queue branch: "14.0" includes: ["queue_job"] @@ -40,6 +46,12 @@ branch: "14.0" includes: - onchange_helper + - auditlog + +- url: https://github.com/OCA/server-ux + branch: "14.0" + includes: + - mass_editing - url: https://github.com/OCA/stock-logistics-warehouse branch: "14.0" diff --git a/stock_serial_single_quant/README.rst b/stock_serial_single_quant/README.rst index 65214dae..01b6f12a 100644 --- a/stock_serial_single_quant/README.rst +++ b/stock_serial_single_quant/README.rst @@ -53,6 +53,16 @@ If the serial number has a quant, the owner of the stock move line must match th .. image:: static/description/wrong_owner_message.png +Configuration +------------- +To bypass the constraints for a specific warehouse: + +1. Go to **Inventory > Configuration > Warehouses**. +2. Select the warehouse you want to configure. +3. Check the box **Bypass Serial Constraints**. + +When this option is enabled, stock moves destined to this warehouse will not be subject to the single quant constraints. This is particularly useful for specific logistical flows (like quarantine or transit zones). + Contributors ------------ diff --git a/stock_serial_single_quant/__init__.py b/stock_serial_single_quant/__init__.py index 36ffad66..bd6efb6a 100644 --- a/stock_serial_single_quant/__init__.py +++ b/stock_serial_single_quant/__init__.py @@ -1,4 +1,6 @@ # © Numigi (tm) and all its contributors (https://numigi.com/r/home) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +"""Init module.""" + from . import models diff --git a/stock_serial_single_quant/__manifest__.py b/stock_serial_single_quant/__manifest__.py index 17a6d519..9c0f40b6 100644 --- a/stock_serial_single_quant/__manifest__.py +++ b/stock_serial_single_quant/__manifest__.py @@ -1,14 +1,20 @@ # © Numigi (tm) and all its contributors (https://numigi.com/r/home) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +# pylint: disable=pointless-statement +# noqa: B018 + { "name": "Stock Serial Single Quant", "summary": "Add constraints on serial numbers", - "version": "1.0.0", + "version": "14.0.1.1.0", "category": "Inventory", "author": "Numigi", "license": "LGPL-3", "depends": ["stock"], - "data": ["views/stock_production_lot.xml"], + "data": [ + "views/stock_production_lot.xml", + "views/stock_warehouse.xml", + ], "installable": True, } diff --git a/stock_serial_single_quant/models/__init__.py b/stock_serial_single_quant/models/__init__.py index dbee5082..f97802de 100644 --- a/stock_serial_single_quant/models/__init__.py +++ b/stock_serial_single_quant/models/__init__.py @@ -1,4 +1,7 @@ # © Numigi (tm) and all its contributors (https://numigi.com/r/home) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from . import stock_move, stock_move_line, stock_production_lot +from . import stock_move # noqa: F401 +from . import stock_move_line # noqa: F401 +from . import stock_production_lot # noqa: F401 +from . import stock_warehouse # noqa: F401 diff --git a/stock_serial_single_quant/models/stock_move.py b/stock_serial_single_quant/models/stock_move.py index 5e384b51..68c42764 100644 --- a/stock_serial_single_quant/models/stock_move.py +++ b/stock_serial_single_quant/models/stock_move.py @@ -17,8 +17,9 @@ def _has_serialized_product(self): return self.product_id.tracking == "serial" def _check_serial_number_constraints(self): - lines_with_existing_quants = self.move_line_ids.filtered( - lambda l: bool(l.lot_id.sudo().get_positive_quants()) - ) - for line in lines_with_existing_quants: + lines_to_check = self._get_lines_to_check() + for line in lines_to_check: line.check_serial_number_constraints() + + def _get_lines_to_check(self): + return self.move_line_ids.filtered(lambda line: line._requires_serial_check()) diff --git a/stock_serial_single_quant/models/stock_move_line.py b/stock_serial_single_quant/models/stock_move_line.py index c11c5d36..a94e825c 100644 --- a/stock_serial_single_quant/models/stock_move_line.py +++ b/stock_serial_single_quant/models/stock_move_line.py @@ -58,17 +58,27 @@ def check_serial_number_constraints(self): self._check_serial_number_source_package() self._check_serial_number_source_owner() + def _requires_serial_check(self): + dest_warehouse = self.location_dest_id.get_warehouse() + is_bypassed = dest_warehouse.bypass_serial_single_quant + has_quants = bool(self.lot_id.sudo().get_positive_quants()) + return not is_bypassed and has_quants + def _check_serial_number_source_location(self): - serial_location = self.lot_id.get_current_location() - if serial_location != self.location_id: - raise ValidationError( - _(WRONG_LOCATION_MESSAGE).format( - serial=self.lot_id.name, - product=self.product_id.display_name, - location=self.location_id.display_name, - serial_location=serial_location.display_name, - ) + serial_locations = self.lot_id.get_current_location() + if self.location_id not in serial_locations: + self._raise_wrong_location_error(serial_locations) + + def _raise_wrong_location_error(self, serial_locations): + location_names = ", ".join(serial_locations.mapped("display_name")) + raise ValidationError( + _(WRONG_LOCATION_MESSAGE).format( + serial=self.lot_id.name, + product=self.product_id.display_name, + location=self.location_id.display_name, + serial_location=location_names, ) + ) def _check_serial_number_source_package(self): serial_package = self.lot_id.get_current_package() diff --git a/stock_serial_single_quant/models/stock_warehouse.py b/stock_serial_single_quant/models/stock_warehouse.py new file mode 100644 index 00000000..09d50de1 --- /dev/null +++ b/stock_serial_single_quant/models/stock_warehouse.py @@ -0,0 +1,13 @@ +# © Numigi (tm) and all its contributors (https://numigi.com/r/home) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" + + bypass_serial_single_quant = fields.Boolean( + string="Bypass Serial Constraints", + default=False, + ) diff --git a/stock_serial_single_quant/tests/test_stock_move.py b/stock_serial_single_quant/tests/test_stock_move.py index 02d3423e..e77bac7f 100644 --- a/stock_serial_single_quant/tests/test_stock_move.py +++ b/stock_serial_single_quant/tests/test_stock_move.py @@ -109,3 +109,37 @@ def test_expected_source_owner(self): self.quant_1.owner_id = self.owner_1 with pytest.raises(ValidationError): self.move_serial_number(self.serial_1, self.location_1, self.location_2) + + def test_bypass_serial_single_quant_enabled(self): + # Enable the bypass on the destination warehouse + self.warehouse_3.bypass_serial_single_quant = True + # Move the serial number from an invalid location (location_2) + # to the bypassed warehouse (location_3) + # It should NOT raise a ValidationError because the check is bypassed + move = self.move_serial_number(self.serial_1, self.location_2, self.location_3) + + assert move.state == "done" + expected_locations = self.location_1 | self.location_3 + assert self.serial_1.get_current_location() == expected_locations + + def test_multi_quant_raises_validation_error_not_singleton(self): + # Create a second quant for the same serial in a different location (incoherence) + self.make_quant(self.location_2, self.serial_1) + + # Now serial_1 is physically in both location_1 and location_2. + # Moving it from a completely different location (location_1a) should raise a + # ValidationError and NOT a ValueError (Expected Singleton) + with pytest.raises(ValidationError) as excinfo: + self.move_serial_number(self.serial_1, self.location_1a, self.location_3) + + # Verify that both locations are correctly joined and displayed in the error message + error_message = str(excinfo.value) + assert self.location_1.display_name in error_message + assert self.location_2.display_name in error_message + + def test_bypass_disabled_still_raises_error(self): + # Ensure that by default (bypass = False), the error is still raised + self.warehouse_3.bypass_serial_single_quant = False + + with pytest.raises(ValidationError): + self.move_serial_number(self.serial_1, self.location_2, self.location_3) diff --git a/stock_serial_single_quant/views/stock_warehouse.xml b/stock_serial_single_quant/views/stock_warehouse.xml new file mode 100644 index 00000000..3ada93ea --- /dev/null +++ b/stock_serial_single_quant/views/stock_warehouse.xml @@ -0,0 +1,13 @@ + + + + stock.warehouse.form.inherit.serial.single.quant + stock.warehouse + + + + + + + + \ No newline at end of file