diff --git a/stock_valuation_layer_vendor/README.rst b/stock_valuation_layer_vendor/README.rst new file mode 100644 index 0000000..e9c9fcd --- /dev/null +++ b/stock_valuation_layer_vendor/README.rst @@ -0,0 +1,106 @@ +============================ +Stock Valuation Layer Vendor +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7bb64c277e472c06137f6cf7274c16963ef0a6e00a2a837a9f8cf2dcd5a06433 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/18.0/stock_valuation_layer_vendor + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-18-0/stock-logistics-workflow-18-0-stock_valuation_layer_vendor + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a vendor field to stock valuation layers, populated +from the purchase order linked to the related stock move. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Companies in a corporate group may source stock from a sister entity as +well as from external vendors. Stock received from a group company +carries that entity's internal margin, which must be excluded from +consolidated valuation to avoid double-booking costs and sales between +entities. + +The source vendor is only reachable from the valuation layer through a +multi-level relation, which is impractical for filtering. Storing it +directly on each layer makes it easy to filter out intragroup-sourced +valuation, and provides a readily available dimension for supplier-based +valuation analysis in general. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Quartile + +Contributors +------------ + +- `Quartile `__: + + - Toshikimi Shigenobu + - Aung Ko Ko Lin + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-nobuQuartile| image:: https://github.com/nobuQuartile.png?size=40px + :target: https://github.com/nobuQuartile + :alt: nobuQuartile +.. |maintainer-Aungkokolin1997| image:: https://github.com/Aungkokolin1997.png?size=40px + :target: https://github.com/Aungkokolin1997 + :alt: Aungkokolin1997 + +Current `maintainers `__: + +|maintainer-nobuQuartile| |maintainer-Aungkokolin1997| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_valuation_layer_vendor/__init__.py b/stock_valuation_layer_vendor/__init__.py new file mode 100644 index 0000000..6d58305 --- /dev/null +++ b/stock_valuation_layer_vendor/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import pre_init_hook diff --git a/stock_valuation_layer_vendor/__manifest__.py b/stock_valuation_layer_vendor/__manifest__.py new file mode 100644 index 0000000..86a351e --- /dev/null +++ b/stock_valuation_layer_vendor/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Stock Valuation Layer Vendor", + "summary": "Store the vendor on stock valuation layers created from purchases.", + "version": "18.0.1.0.0", + "author": "Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-workflow", + "category": "Inventory", + "license": "AGPL-3", + "depends": ["purchase_stock"], + "data": ["views/stock_valuation_layer_views.xml"], + "pre_init_hook": "pre_init_hook", + "maintainers": ["nobuQuartile", "Aungkokolin1997"], + "installable": True, +} diff --git a/stock_valuation_layer_vendor/hooks.py b/stock_valuation_layer_vendor/hooks.py new file mode 100644 index 0000000..139f914 --- /dev/null +++ b/stock_valuation_layer_vendor/hooks.py @@ -0,0 +1,19 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tools.sql import column_exists, create_column + + +def pre_init_hook(env): + if column_exists(env.cr, "stock_valuation_layer", "vendor_id"): + return + create_column(env.cr, "stock_valuation_layer", "vendor_id", "int4") + env.cr.execute( + """ + UPDATE stock_valuation_layer svl + SET vendor_id = pol.partner_id + FROM stock_move sm + JOIN purchase_order_line pol ON pol.id = sm.purchase_line_id + WHERE svl.stock_move_id = sm.id + """ + ) diff --git a/stock_valuation_layer_vendor/i18n/ja.po b/stock_valuation_layer_vendor/i18n/ja.po new file mode 100644 index 0000000..194c873 --- /dev/null +++ b/stock_valuation_layer_vendor/i18n/ja.po @@ -0,0 +1,32 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_valuation_layer_vendor +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-23 09:43+0000\n" +"PO-Revision-Date: 2026-07-23 09:43+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_valuation_layer_vendor +#: model:ir.model,name:stock_valuation_layer_vendor.model_stock_valuation_layer +msgid "Stock Valuation Layer" +msgstr "在庫評価レイヤ" + +#. module: stock_valuation_layer_vendor +#: model:ir.model.fields,field_description:stock_valuation_layer_vendor.field_stock_valuation_layer__vendor_id +#: model_terms:ir.ui.view,arch_db:stock_valuation_layer_vendor.stock_valuation_layer_view_search +msgid "Vendor" +msgstr "仕入先" + +#. module: stock_valuation_layer_vendor +#: model:ir.model.fields,help:stock_valuation_layer_vendor.field_stock_valuation_layer__vendor_id +msgid "Vendor of the purchase order linked to the stock move, if any." +msgstr "在庫移動に紐づく購買オーダの仕入先(存在する場合)。" diff --git a/stock_valuation_layer_vendor/models/__init__.py b/stock_valuation_layer_vendor/models/__init__.py new file mode 100644 index 0000000..f75b2df --- /dev/null +++ b/stock_valuation_layer_vendor/models/__init__.py @@ -0,0 +1 @@ +from . import stock_valuation_layer diff --git a/stock_valuation_layer_vendor/models/stock_valuation_layer.py b/stock_valuation_layer_vendor/models/stock_valuation_layer.py new file mode 100644 index 0000000..5ed3685 --- /dev/null +++ b/stock_valuation_layer_vendor/models/stock_valuation_layer.py @@ -0,0 +1,34 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockValuationLayer(models.Model): + _inherit = "stock.valuation.layer" + + vendor_id = fields.Many2one( + "res.partner", + readonly=True, + index=True, + help="Vendor of the purchase order linked to the stock move, if any.", + ) + + @api.model_create_multi + def create(self, vals_list): + move_ids = [ + vals["stock_move_id"] for vals in vals_list if vals.get("stock_move_id") + ] + # Resolve the vendor per move once, letting the ORM prefetch the + # related purchase order and partner in batch. + moves = self.env["stock.move"].browse(move_ids) + vendor_by_move = { + move.id: move.purchase_line_id.partner_id.id + for move in moves + if move.purchase_line_id + } + for vals in vals_list: + vendor_id = vendor_by_move.get(vals.get("stock_move_id")) + if vendor_id: + vals["vendor_id"] = vendor_id + return super().create(vals_list) diff --git a/stock_valuation_layer_vendor/pyproject.toml b/stock_valuation_layer_vendor/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/stock_valuation_layer_vendor/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/stock_valuation_layer_vendor/readme/CONTEXT.md b/stock_valuation_layer_vendor/readme/CONTEXT.md new file mode 100644 index 0000000..d5ad3ac --- /dev/null +++ b/stock_valuation_layer_vendor/readme/CONTEXT.md @@ -0,0 +1,9 @@ +Companies in a corporate group may source stock from a sister entity as well as +from external vendors. Stock received from a group company carries that entity's +internal margin, which must be excluded from consolidated valuation to avoid +double-booking costs and sales between entities. + +The source vendor is only reachable from the valuation layer through a multi-level +relation, which is impractical for filtering. Storing it directly on each layer +makes it easy to filter out intragroup-sourced valuation, and provides a readily +available dimension for supplier-based valuation analysis in general. diff --git a/stock_valuation_layer_vendor/readme/CONTRIBUTORS.md b/stock_valuation_layer_vendor/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..3318a65 --- /dev/null +++ b/stock_valuation_layer_vendor/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Quartile](https://www.quartile.co): + - Toshikimi Shigenobu + - Aung Ko Ko Lin diff --git a/stock_valuation_layer_vendor/readme/DESCRIPTION.md b/stock_valuation_layer_vendor/readme/DESCRIPTION.md new file mode 100644 index 0000000..96cc072 --- /dev/null +++ b/stock_valuation_layer_vendor/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module adds a vendor field to stock valuation layers, populated from the +purchase order linked to the related stock move. diff --git a/stock_valuation_layer_vendor/static/description/index.html b/stock_valuation_layer_vendor/static/description/index.html new file mode 100644 index 0000000..8a28b00 --- /dev/null +++ b/stock_valuation_layer_vendor/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +Stock Valuation Layer Vendor + + + +
+

Stock Valuation Layer Vendor

+ + +

Beta License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

This module adds a vendor field to stock valuation layers, populated +from the purchase order linked to the related stock move.

+

Table of contents

+ +
+

Use Cases / Context

+

Companies in a corporate group may source stock from a sister entity as +well as from external vendors. Stock received from a group company +carries that entity’s internal margin, which must be excluded from +consolidated valuation to avoid double-booking costs and sales between +entities.

+

The source vendor is only reachable from the valuation layer through a +multi-level relation, which is impractical for filtering. Storing it +directly on each layer makes it easy to filter out intragroup-sourced +valuation, and provides a readily available dimension for supplier-based +valuation analysis in general.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

Contributors

+
    +
  • Quartile:
      +
    • Toshikimi Shigenobu
    • +
    • Aung Ko Ko Lin
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

nobuQuartile Aungkokolin1997

+

This module is part of the OCA/stock-logistics-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/stock_valuation_layer_vendor/tests/__init__.py b/stock_valuation_layer_vendor/tests/__init__.py new file mode 100644 index 0000000..f63c2fc --- /dev/null +++ b/stock_valuation_layer_vendor/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_valuation_layer_vendor diff --git a/stock_valuation_layer_vendor/tests/test_stock_valuation_layer_vendor.py b/stock_valuation_layer_vendor/tests/test_stock_valuation_layer_vendor.py new file mode 100644 index 0000000..ca301ef --- /dev/null +++ b/stock_valuation_layer_vendor/tests/test_stock_valuation_layer_vendor.py @@ -0,0 +1,95 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command + +from odoo.addons.base.tests.common import BaseCommon + + +class TestStockValuationLayerVendor(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.vendor = cls.env["res.partner"].create({"name": "Test Vendor"}) + cls.product = cls.env["product.product"].create( + {"name": "Test Product", "is_storable": True, "standard_price": 100.0} + ) + cls.stock_location = cls.env.ref("stock.stock_location_stock") + + def _create_purchase_order(self, partner): + return self.env["purchase.order"].create( + { + "partner_id": partner.id, + "order_line": [ + Command.create( + { + "product_id": self.product.id, + "name": self.product.name, + "product_qty": 10.0, + "product_uom": self.product.uom_id.id, + "price_unit": 100.0, + } + ) + ], + } + ) + + def _receive_purchase_order(self, purchase_order): + """Confirm the order and validate its receipt, as a user would.""" + purchase_order.button_confirm() + picking = purchase_order.picking_ids + picking.action_assign() + picking.move_ids.write({"quantity": 10.0, "picked": True}) + picking._action_done() + return picking + + def test_vendor_id_set_from_purchase_move(self): + picking = self._receive_purchase_order(self._create_purchase_order(self.vendor)) + svl = picking.move_ids.stock_valuation_layer_ids + self.assertTrue(svl) + self.assertEqual(svl.vendor_id, self.vendor) + + def test_vendor_id_set_from_purchase_move_batch(self): + vendor_2 = self.env["res.partner"].create({"name": "Test Vendor 2"}) + picking_1 = self._receive_purchase_order( + self._create_purchase_order(self.vendor) + ) + picking_2 = self._receive_purchase_order(self._create_purchase_order(vendor_2)) + self.assertEqual( + picking_1.move_ids.stock_valuation_layer_ids.vendor_id, self.vendor + ) + self.assertEqual( + picking_2.move_ids.stock_valuation_layer_ids.vendor_id, vendor_2 + ) + + def test_vendor_id_empty_without_purchase(self): + # Receiving stock without a purchase order (here via an inventory + # adjustment) produces a layer whose move has no purchase line. + quant = self.env["stock.quant"].create( + { + "product_id": self.product.id, + "location_id": self.stock_location.id, + "inventory_quantity": 10.0, + } + ) + quant.action_apply_inventory() + svl = self.env["stock.valuation.layer"].search( + [("product_id", "=", self.product.id)] + ) + self.assertTrue(svl) + self.assertFalse(svl.vendor_id) + + def test_vendor_id_empty_without_move(self): + # A layer with no stock move (e.g. a manual revaluation) must not raise + # and must have no vendor. This path has no realistic document flow, so + # the layer is created directly. + svl = self.env["stock.valuation.layer"].create( + { + "product_id": self.product.id, + "company_id": self.env.company.id, + "quantity": 10.0, + "value": 1000.0, + "unit_cost": 100.0, + } + ) + self.assertFalse(svl.vendor_id) diff --git a/stock_valuation_layer_vendor/views/stock_valuation_layer_views.xml b/stock_valuation_layer_vendor/views/stock_valuation_layer_views.xml new file mode 100644 index 0000000..d38ef7c --- /dev/null +++ b/stock_valuation_layer_vendor/views/stock_valuation_layer_views.xml @@ -0,0 +1,40 @@ + + + + stock.valuation.layer.search + stock.valuation.layer + + + + + + + + + + + + stock.valuation.layer.form + stock.valuation.layer + + + + + + + + + stock.valuation.layer.list + stock.valuation.layer + + + + + + + +