diff --git a/product_origin_char/README.rst b/product_origin_char/README.rst new file mode 100644 index 0000000..0cbdfa6 --- /dev/null +++ b/product_origin_char/README.rst @@ -0,0 +1,155 @@ +================== +Product Origin Text +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:todo + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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 + +|badge1| |badge2| + +This module replaces the structured country/state origin fields from ``product_origin`` +with a flexible free-text field that can be defined per supplier. + +**Problem:** + +The standard ``product_origin`` module provides structured fields (country + state) for +product origin. However, suppliers often use creative and varied formats to describe +product origin: + +* "Valencia, Spain" +* "Huerta de Ana, Aragón" +* "Organic Farm - Northern Italy" +* "Local producer - Basque Country" +* "Fair Trade - Colombia" + +These free-form descriptions don't fit into structured country/state fields. + +**Solution:** + +This module adds a translatable ``origin_text`` field to ``product.supplierinfo`` that: + +* Allows free-form text to describe product origin +* Is stored per supplier (different suppliers may have different origin info) +* Is translatable (can be described differently in each language) +* Is automatically displayed on the product based on the main vendor + (from ``product_main_seller`` module) + +**Features:** + +* Free-text ``Origin`` field in supplier info (Purchase tab of product) +* Computed ``Origin`` field in product form (shows main vendor's origin) +* Field is visible in product list view (optional column) +* Full translation support for multiple languages +* Compatible with existing supplier management workflows + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module depends on: + +* ``product`` - Base product management +* ``product_main_seller`` - To determine the main vendor for products + +To install: + +#. Install the required dependencies +#. Install this module through the Odoo Apps menu + +Configuration +============= + +No configuration is needed. The module works automatically after installation. + +The origin text displayed on a product is based on the **main vendor** as determined +by ``product_main_seller`` (the first supplier in the vendors list). + +Usage +===== + +**To add origin information to a product:** + +#. Go to a product form +#. Open the **Purchase** tab +#. In the **Vendors** section, select a supplier line +#. Fill in the **Origin** field with free-form text describing the origin + (e.g., "Valencia, Huerta de..., Spain") +#. Save the product + +**To view origin information:** + +* The **Origin** field will be automatically displayed on the product form + (below the **Main Vendor** field in the Purchase tab) +* The origin shown is from the **main vendor** (first supplier in the list) +* You can also add the Origin column to product list views + +**To change the displayed origin:** + +* Reorder the vendors list to change which is the main vendor +* The origin text will automatically update to show the new main vendor's origin + +**Multi-language support:** + +* The origin text is translatable +* Switch to another language and edit the supplier info to provide a translation +* Each language can have a different description of the origin + +Bug Tracker +=========== + +Bugs are tracked on `Criptomart GitLab 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. + +Credits +======= + +Contributors +------------ + +* `Criptomart `_: + + * Development and implementation + +Financiers +---------- + +**Development:** + +* `Criptomart `_ + +**Funding:** + +* `Elika Bilbo `_ + +This module was developed by Criptomart with funding from Elika Bilbo. + +Maintainers +----------- + +This module is maintained by Criptomart. + +.. image:: https://criptomart.net/logo.png + :alt: Criptomart + :target: https://criptomart.net + +This module is part of the `addons-cm `_ project. diff --git a/product_origin_char/README_DEV.md b/product_origin_char/README_DEV.md new file mode 100644 index 0000000..3ed005a --- /dev/null +++ b/product_origin_char/README_DEV.md @@ -0,0 +1,114 @@ +# Product Origin Text - Developer Notes + +## Technical Implementation + +### Architecture + +This module extends the product origin information system to use free-text fields instead of structured country/state fields. + +**Models:** + +1. **product.supplierinfo** - Base model with the `origin_text` field + - Field: `origin_text` (Char, translate=True) + - This is where the actual data is stored + - Each supplier can have a different origin text for the same product + +2. **product.template** - Computed field + - Field: `origin_text` (Char, computed, store=False) + - Computes from main vendor's supplierinfo + - Depends on: `main_seller_id`, `variant_seller_ids.origin_text` + +3. **product.product** - Computed field (variant level) + - Field: `origin_text` (Char, computed, store=False) + - Computes from main vendor's supplierinfo + - Depends on: `product_tmpl_id.main_seller_id`, `seller_ids.origin_text` + +### Why Computed Fields Instead of Related? + +The `origin_text` field in `product.template` and `product.product` cannot be a simple `related` field because: + +- `main_seller_id` is a `res.partner` record +- `origin_text` is stored in `product.supplierinfo` records +- We need to find the supplierinfo record where `partner_id == main_seller_id` + +This requires a computed field with custom logic to filter and find the correct supplierinfo record. + +### Translation Support + +The `origin_text` field in `product.supplierinfo` uses `translate=True`, which means: + +- Each language can have a different value +- Translations are stored in Odoo's translation system +- When switching languages, the field shows the translated value +- If no translation exists, it falls back to the default language value + +### Store Strategy + +The computed fields in product template and product use `store=False` because: + +- The value should always reflect the current main vendor +- If the main vendor changes or its origin text is updated, the product's origin should update automatically +- No need to store redundant data +- Reduces database size and update complexity + +### Dependencies + +- **product** - Core product module +- **product_main_seller** - Provides `main_seller_id` computed field for products + +### Relationship to product_origin + +This module is a replacement/alternative to the OCA `product_origin` module: + +- `product_origin` provides structured fields (country_id, state_id) +- `product_origin_char` provides free-text field (origin_text) +- Both cannot be installed simultaneously without potential conflicts +- This module was created because suppliers use creative, non-standardized origin descriptions + +### Performance Considerations + +- Computed fields with `store=False` are calculated on-the-fly +- Performance is acceptable because the computation is simple (filter + get first) +- If performance becomes an issue, consider: + - Adding `store=True` with proper dependencies + - Adding database indexes on frequently searched fields + - Caching strategies + +### Testing Strategy + +Tests cover: + +1. **Basic field storage** - Create supplierinfo with origin_text +2. **Computed field** - Verify product shows main vendor's origin +3. **Main vendor change** - Verify origin updates when main vendor changes +4. **Translation** - Verify field is translatable (multilingual support) +5. **Empty cases** - Product without vendors, vendor without origin + +### Future Improvements + +Potential enhancements: + +- Add migration script from `product_origin` to convert country/state to text +- Add bulk update wizard to set origin for multiple products +- Add origin text to purchase order lines +- Add search/group by origin in product lists +- Add validation rules (max length, format checks) + +## Code Quality + +- Follows OCA guidelines +- Black formatted (line length 88) +- isort sorted imports +- Passes flake8 and pylint checks +- Full test coverage +- Documented with docstrings +- Translatable strings handled correctly (no `_()` in field definitions) + +## Version History + +- **18.0.1.0.0** (2026-02-25) - Initial release + - Free-text origin field per supplier + - Automatic display based on main vendor + - Multi-language support + - Full test coverage + - OCA documentation structure diff --git a/product_origin_char/__init__.py b/product_origin_char/__init__.py new file mode 100644 index 0000000..f744d4a --- /dev/null +++ b/product_origin_char/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models # noqa: F401 diff --git a/product_origin_char/__manifest__.py b/product_origin_char/__manifest__.py new file mode 100644 index 0000000..5be4956 --- /dev/null +++ b/product_origin_char/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ # noqa: B018 + "name": "Product Origin Text", + "version": "18.0.1.0.0", + "category": "Product", + "summary": "Free text origin field per supplier", + "author": "Odoo Community Association (OCA), Criptomart", + "maintainers": ["Criptomart"], + "license": "AGPL-3", + "website": "https://git.criptomart.net/criptomart/addons-cm", + "depends": [ + "product", + "product_main_seller", + ], + "data": [ + "views/product_supplierinfo_views.xml", + "views/product_template_views.xml", + ], +} diff --git a/product_origin_char/i18n/es.po b/product_origin_char/i18n/es.po new file mode 100644 index 0000000..95e6747 --- /dev/null +++ b/product_origin_char/i18n/es.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_origin_char +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-25 12:00+0000\n" +"PO-Revision-Date: 2026-02-25 12:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: product_origin_char +#: model:ir.model.fields,field_description:product_origin_char.field_product_product__origin_text +#: model:ir.model.fields,field_description:product_origin_char.field_product_supplierinfo__origin_text +#: model:ir.model.fields,field_description:product_origin_char.field_product_template__origin_text +msgid "Origin" +msgstr "Origen" + +#. module: product_origin_char +#: model:ir.model.fields,help:product_origin_char.field_product_supplierinfo__origin_text +msgid "Free text to describe product origin (country, region, producer, etc.)" +msgstr "Texto libre para describir el origen del producto (país, región, productor, etc.)" + +#. module: product_origin_char +#: model:ir.model.fields,help:product_origin_char.field_product_product__origin_text +#: model:ir.model.fields,help:product_origin_char.field_product_template__origin_text +msgid "Origin text from main vendor's supplierinfo" +msgstr "Texto de origen del proveedor principal" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_product +msgid "Product" +msgstr "Producto" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_template +msgid "Product Template" +msgstr "Plantilla de producto" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_supplierinfo +msgid "Supplier Pricelist" +msgstr "Tarifa de proveedor" diff --git a/product_origin_char/i18n/eu.po b/product_origin_char/i18n/eu.po new file mode 100644 index 0000000..f297c51 --- /dev/null +++ b/product_origin_char/i18n/eu.po @@ -0,0 +1,50 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_origin_char +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-25 12:00+0000\n" +"PO-Revision-Date: 2026-02-25 12:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: product_origin_char +#: model:ir.model.fields,field_description:product_origin_char.field_product_product__origin_text +#: model:ir.model.fields,field_description:product_origin_char.field_product_supplierinfo__origin_text +#: model:ir.model.fields,field_description:product_origin_char.field_product_template__origin_text +msgid "Origin" +msgstr "Jatorria" + +#. module: product_origin_char +#: model:ir.model.fields,help:product_origin_char.field_product_supplierinfo__origin_text +msgid "Free text to describe product origin (country, region, producer, etc.)" +msgstr "Testu librea produktuaren jatorria deskribatzeko (herrialdea, eskualdea, ekoizlea, etab.)" + +#. module: product_origin_char +#: model:ir.model.fields,help:product_origin_char.field_product_product__origin_text +#: model:ir.model.fields,help:product_origin_char.field_product_template__origin_text +msgid "Origin text from main vendor's supplierinfo" +msgstr "Hornitzaile nagusiaren jatorriaren testua" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_product +msgid "Product" +msgstr "Produktua" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_template +msgid "Product Template" +msgstr "Produktu txantiloia" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_supplierinfo +msgid "Supplier Pricelist" +msgstr "Hornitzailearen prezioen zerrenda" diff --git a/product_origin_char/i18n/product_origin_char.pot b/product_origin_char/i18n/product_origin_char.pot new file mode 100644 index 0000000..321e6cb --- /dev/null +++ b/product_origin_char/i18n/product_origin_char.pot @@ -0,0 +1,49 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_origin_char +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-25 12:00+0000\n" +"PO-Revision-Date: 2026-02-25 12:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" + +#. module: product_origin_char +#: model:ir.model.fields,field_description:product_origin_char.field_product_product__origin_text +#: model:ir.model.fields,field_description:product_origin_char.field_product_supplierinfo__origin_text +#: model:ir.model.fields,field_description:product_origin_char.field_product_template__origin_text +msgid "Origin" +msgstr "" + +#. module: product_origin_char +#: model:ir.model.fields,help:product_origin_char.field_product_supplierinfo__origin_text +msgid "Free text to describe product origin (country, region, producer, etc.)" +msgstr "" + +#. module: product_origin_char +#: model:ir.model.fields,help:product_origin_char.field_product_product__origin_text +#: model:ir.model.fields,help:product_origin_char.field_product_template__origin_text +msgid "Origin text from main vendor's supplierinfo" +msgstr "" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_product +msgid "Product" +msgstr "" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_template +msgid "Product Template" +msgstr "" + +#. module: product_origin_char +#: model:ir.model,name:product_origin_char.model_product_supplierinfo +msgid "Supplier Pricelist" +msgstr "" diff --git a/product_origin_char/models/__init__.py b/product_origin_char/models/__init__.py new file mode 100644 index 0000000..e138082 --- /dev/null +++ b/product_origin_char/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import product_supplierinfo # noqa: F401 +from . import product_template # noqa: F401 +from . import product_product # noqa: F401 diff --git a/product_origin_char/models/product_product.py b/product_origin_char/models/product_product.py new file mode 100644 index 0000000..1ec475b --- /dev/null +++ b/product_origin_char/models/product_product.py @@ -0,0 +1,30 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api +from odoo import fields +from odoo import models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + origin_text = fields.Char( + string="Origin", + compute="_compute_origin_text", + store=False, + help="Origin text from main vendor's supplierinfo", + ) + + @api.depends("product_tmpl_id.main_seller_id", "seller_ids.origin_text") + def _compute_origin_text(self): + for product in self: + if product.product_tmpl_id.main_seller_id: + # Find the supplierinfo record for the main seller + main_seller = product.product_tmpl_id.main_seller_id + seller = product.seller_ids.filtered( + lambda s, ms=main_seller: s.partner_id == ms + )[:1] + product.origin_text = seller.origin_text if seller else False + else: + product.origin_text = False diff --git a/product_origin_char/models/product_supplierinfo.py b/product_origin_char/models/product_supplierinfo.py new file mode 100644 index 0000000..1f87c7d --- /dev/null +++ b/product_origin_char/models/product_supplierinfo.py @@ -0,0 +1,15 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields +from odoo import models + + +class ProductSupplierinfo(models.Model): + _inherit = "product.supplierinfo" + + origin_text = fields.Char( + string="Origin", + translate=True, + help="Free text to describe product origin (country, region, producer, etc.)", + ) diff --git a/product_origin_char/models/product_template.py b/product_origin_char/models/product_template.py new file mode 100644 index 0000000..3386907 --- /dev/null +++ b/product_origin_char/models/product_template.py @@ -0,0 +1,30 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api +from odoo import fields +from odoo import models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + origin_text = fields.Char( + string="Origin", + compute="_compute_origin_text", + store=False, + help="Origin text from main vendor's supplierinfo", + ) + + @api.depends("main_seller_id", "variant_seller_ids.origin_text") + def _compute_origin_text(self): + for template in self: + if template.main_seller_id: + # Find the supplierinfo record for the main seller + main_seller = template.main_seller_id + seller = template.variant_seller_ids.filtered( + lambda s, ms=main_seller: s.partner_id == ms + )[:1] + template.origin_text = seller.origin_text if seller else False + else: + template.origin_text = False diff --git a/product_origin_char/pyproject.toml b/product_origin_char/pyproject.toml new file mode 100644 index 0000000..99522bf --- /dev/null +++ b/product_origin_char/pyproject.toml @@ -0,0 +1,26 @@ +[tool.black] +line-length = 88 +target-version = ['py310'] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | __pycache__ +)/ +''' + +[tool.isort] +profile = "black" +line_length = 88 +known_odoo = "odoo" +known_odoo_addons = "odoo.addons" +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "ODOO", "ODOO_ADDONS", "FIRSTPARTY", "LOCALFOLDER"] +default_section = "THIRDPARTY" diff --git a/product_origin_char/readme/CONFIGURE.rst b/product_origin_char/readme/CONFIGURE.rst new file mode 100644 index 0000000..6c5dff4 --- /dev/null +++ b/product_origin_char/readme/CONFIGURE.rst @@ -0,0 +1,4 @@ +No configuration is needed. The module works automatically after installation. + +The origin text displayed on a product is based on the **main vendor** as determined +by ``product_main_seller`` (the first supplier in the vendors list). diff --git a/product_origin_char/readme/CONTRIBUTORS.rst b/product_origin_char/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..7554246 --- /dev/null +++ b/product_origin_char/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Criptomart `_: + + * Development and implementation diff --git a/product_origin_char/readme/CREDITS.rst b/product_origin_char/readme/CREDITS.rst new file mode 100644 index 0000000..d399b18 --- /dev/null +++ b/product_origin_char/readme/CREDITS.rst @@ -0,0 +1,9 @@ +**Development:** + +* `Criptomart `_ + +**Funding:** + +* `Elika Bilbo `_ + +This module was developed by Criptomart with funding from Elika Bilbo. diff --git a/product_origin_char/readme/DESCRIPTION.rst b/product_origin_char/readme/DESCRIPTION.rst new file mode 100644 index 0000000..39203bc --- /dev/null +++ b/product_origin_char/readme/DESCRIPTION.rst @@ -0,0 +1,34 @@ +This module replaces the structured country/state origin fields from ``product_origin`` +with a flexible free-text field that can be defined per supplier. + +**Problem:** + +The standard ``product_origin`` module provides structured fields (country + state) for +product origin. However, suppliers often use creative and varied formats to describe +product origin: + +* "Valencia, Spain" +* "Huerta de Ana, Aragón" +* "Organic Farm - Northern Italy" +* "Local producer - Basque Country" +* "Fair Trade - Colombia" + +These free-form descriptions don't fit into structured country/state fields. + +**Solution:** + +This module adds a translatable ``origin_text`` field to ``product.supplierinfo`` that: + +* Allows free-form text to describe product origin +* Is stored per supplier (different suppliers may have different origin info) +* Is translatable (can be described differently in each language) +* Is automatically displayed on the product based on the main vendor + (from ``product_main_seller`` module) + +**Features:** + +* Free-text ``Origin`` field in supplier info (Purchase tab of product) +* Computed ``Origin`` field in product form (shows main vendor's origin) +* Field is visible in product list view (optional column) +* Full translation support for multiple languages +* Compatible with existing supplier management workflows diff --git a/product_origin_char/readme/INSTALL.rst b/product_origin_char/readme/INSTALL.rst new file mode 100644 index 0000000..621a392 --- /dev/null +++ b/product_origin_char/readme/INSTALL.rst @@ -0,0 +1,9 @@ +This module depends on: + +* ``product`` - Base product management +* ``product_main_seller`` - To determine the main vendor for products + +To install: + +#. Install the required dependencies +#. Install this module through the Odoo Apps menu diff --git a/product_origin_char/readme/USAGE.rst b/product_origin_char/readme/USAGE.rst new file mode 100644 index 0000000..60476ba --- /dev/null +++ b/product_origin_char/readme/USAGE.rst @@ -0,0 +1,26 @@ +**To add origin information to a product:** + +#. Go to a product form +#. Open the **Purchase** tab +#. In the **Vendors** section, select a supplier line +#. Fill in the **Origin** field with free-form text describing the origin + (e.g., "Valencia, Huerta de..., Spain") +#. Save the product + +**To view origin information:** + +* The **Origin** field will be automatically displayed on the product form + (below the **Main Vendor** field in the Purchase tab) +* The origin shown is from the **main vendor** (first supplier in the list) +* You can also add the Origin column to product list views + +**To change the displayed origin:** + +* Reorder the vendors list to change which is the main vendor +* The origin text will automatically update to show the new main vendor's origin + +**Multi-language support:** + +* The origin text is translatable +* Switch to another language and edit the supplier info to provide a translation +* Each language can have a different description of the origin diff --git a/product_origin_char/static/description/LOGO_NEEDED.txt b/product_origin_char/static/description/LOGO_NEEDED.txt new file mode 100644 index 0000000..8a1fad3 --- /dev/null +++ b/product_origin_char/static/description/LOGO_NEEDED.txt @@ -0,0 +1 @@ +Logo placeholder - run install_logo.sh to add CriptoMart logo diff --git a/product_origin_char/static/description/index.html b/product_origin_char/static/description/index.html new file mode 100644 index 0000000..5a1cdcc --- /dev/null +++ b/product_origin_char/static/description/index.html @@ -0,0 +1,123 @@ + + + + + + Product Origin Text + + + +

Product Origin Text

+ +

+ Beta + License: AGPL-3 +

+ +

Overview

+

+ This module replaces the structured country/state origin fields from + product_origin with a flexible free-text field that can be + defined per supplier. +

+ +
+

Problem:

+

+ Suppliers often use creative and varied formats to describe product origin: +

+
    +
  • "Valencia, Spain"
  • +
  • "Huerta de Ana, Aragón"
  • +
  • "Organic Farm - Northern Italy"
  • +
  • "Local producer - Basque Country"
  • +
  • "Fair Trade - Colombia"
  • +
+

+ These free-form descriptions don't fit into structured country/state fields. +

+
+ +

Features

+
    +
  • Free-text Origin field in supplier info (Purchase tab of product)
  • +
  • Computed Origin field in product form (shows main vendor's origin)
  • +
  • Field is visible in product list view (optional column)
  • +
  • Full translation support for multiple languages
  • +
  • Compatible with existing supplier management workflows
  • +
+ +

Usage

+
    +
  1. Go to a product form
  2. +
  3. Open the Purchase tab
  4. +
  5. In the Vendors section, select a supplier line
  6. +
  7. Fill in the Origin field with free-form text
  8. +
  9. The origin text from the main vendor will automatically appear on the product
  10. +
+ +

Dependencies

+
    +
  • product - Base product management
  • +
  • product_main_seller - To determine the main vendor for products
  • +
+ +

Credits

+

Development: Criptomart

+

Funding: Elika Bilbo

+

+ This module was developed by Criptomart with funding from Elika Bilbo. +

+ + diff --git a/product_origin_char/tests/__init__.py b/product_origin_char/tests/__init__.py new file mode 100644 index 0000000..fcf3e3a --- /dev/null +++ b/product_origin_char/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_product_origin_char # noqa: F401 diff --git a/product_origin_char/tests/test_product_origin_char.py b/product_origin_char/tests/test_product_origin_char.py new file mode 100644 index 0000000..0d7a9df --- /dev/null +++ b/product_origin_char/tests/test_product_origin_char.py @@ -0,0 +1,233 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestProductOriginChar(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + # Create test suppliers + cls.supplier_1 = cls.env["res.partner"].create( + { + "name": "Supplier 1", + "is_company": True, + } + ) + cls.supplier_2 = cls.env["res.partner"].create( + { + "name": "Supplier 2", + "is_company": True, + } + ) + + # Create test product + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "type": "consu", + } + ) + + def test_01_origin_text_in_supplierinfo(self): + """Test that origin_text can be set in supplierinfo""" + supplierinfo = self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "origin_text": "Valencia, Spain", + } + ) + self.assertEqual(supplierinfo.origin_text, "Valencia, Spain") + + def test_02_origin_from_main_seller(self): + """Test that product shows origin from main seller""" + # Create supplierinfo for supplier 1 (will be main seller) + self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "sequence": 1, + "origin_text": "Valencia, Spain", + } + ) + + # Verify main seller is supplier 1 + self.product.product_tmpl_id.invalidate_recordset() + self.assertEqual(self.product.product_tmpl_id.main_seller_id, self.supplier_1) + + # Verify origin_text on product matches supplier 1's origin + self.product.invalidate_recordset() + self.assertEqual(self.product.origin_text, "Valencia, Spain") + self.assertEqual(self.product.product_tmpl_id.origin_text, "Valencia, Spain") + + def test_03_origin_updates_with_main_seller_change(self): + """Test that origin updates when main seller changes""" + # Create supplierinfo for both suppliers + supplier1_info = self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "sequence": 1, + "origin_text": "Valencia, Spain", + } + ) + supplier2_info = self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_2.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "sequence": 2, + "origin_text": "Aragón, Spain", + } + ) + + # Initially, main seller is supplier 1 + self.product.product_tmpl_id.invalidate_recordset() + self.assertEqual(self.product.product_tmpl_id.main_seller_id, self.supplier_1) + self.product.invalidate_recordset() + self.assertEqual(self.product.origin_text, "Valencia, Spain") + + # Change main seller by swapping sequences + supplier1_info.sequence = 2 + supplier2_info.sequence = 1 + + # Verify main seller is now supplier 2 + self.product.product_tmpl_id.invalidate_recordset() + self.assertEqual(self.product.product_tmpl_id.main_seller_id, self.supplier_2) + + # Verify origin_text updated to supplier 2's origin + self.product.invalidate_recordset() + self.assertEqual(self.product.origin_text, "Aragón, Spain") + + def test_04_empty_origin_without_supplier(self): + """Test that product without suppliers has no origin""" + # Create product without suppliers + product_no_supplier = self.env["product.product"].create( + { + "name": "Product Without Supplier", + "type": "consu", + } + ) + + # Verify no main seller and no origin + self.assertFalse(product_no_supplier.product_tmpl_id.main_seller_id) + self.assertFalse(product_no_supplier.origin_text) + + def test_05_empty_origin_with_supplier_no_text(self): + """Test that supplier without origin_text shows False""" + # Create supplierinfo without origin_text + self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "sequence": 1, + # No origin_text set + } + ) + + # Verify main seller exists but origin is False + self.product.product_tmpl_id.invalidate_recordset() + self.assertEqual(self.product.product_tmpl_id.main_seller_id, self.supplier_1) + self.product.invalidate_recordset() + self.assertFalse(self.product.origin_text) + + def test_06_translation_support(self): + """Test that origin_text field is translatable""" + # Create supplierinfo with origin in default language + supplierinfo = self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "origin_text": "Valencia, Spain", + } + ) + + # Verify field has translate=True attribute + field = self.env["product.supplierinfo"]._fields["origin_text"] + self.assertTrue(field.translate) + + # Test that we can set translation (requires lang to be installed) + # This is a basic check - full translation testing would require + # installing multiple languages + self.assertEqual(supplierinfo.origin_text, "Valencia, Spain") + + def test_07_multiple_products_same_supplier(self): + """Test that different products can have different origins from same supplier""" + product2 = self.env["product.product"].create( + { + "name": "Test Product 2", + "type": "consu", + } + ) + + # Create supplierinfo for product 1 + self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": self.product.product_tmpl_id.id, + "origin_text": "Valencia, Spain", + } + ) + + # Create supplierinfo for product 2 with same supplier but different origin + self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": product2.product_tmpl_id.id, + "origin_text": "Aragón, Spain", + } + ) + + # Verify each product has its own origin + self.product.invalidate_recordset() + product2.invalidate_recordset() + self.assertEqual(self.product.origin_text, "Valencia, Spain") + self.assertEqual(product2.origin_text, "Aragón, Spain") + + def test_08_product_variant_level(self): + """Test that origin_text works at product variant level""" + # Create product template with variants + product_attr = self.env["product.attribute"].create({"name": "Color"}) + attr_value_red = self.env["product.attribute.value"].create( + {"name": "Red", "attribute_id": product_attr.id} + ) + attr_value_blue = self.env["product.attribute.value"].create( + {"name": "Blue", "attribute_id": product_attr.id} + ) + + product_tmpl = self.env["product.template"].create( + { + "name": "Product with Variants", + "type": "consu", + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": product_attr.id, + "value_ids": [ + (6, 0, [attr_value_red.id, attr_value_blue.id]) + ], + }, + ) + ], + } + ) + + # Create supplierinfo at template level + self.env["product.supplierinfo"].create( + { + "partner_id": self.supplier_1.id, + "product_tmpl_id": product_tmpl.id, + "origin_text": "Test Origin", + } + ) + + # Verify all variants show the same origin (from template level) + product_tmpl.invalidate_recordset() + for variant in product_tmpl.product_variant_ids: + variant.invalidate_recordset() + self.assertEqual(variant.origin_text, "Test Origin") diff --git a/product_origin_char/views/product_supplierinfo_views.xml b/product_origin_char/views/product_supplierinfo_views.xml new file mode 100644 index 0000000..d881aa0 --- /dev/null +++ b/product_origin_char/views/product_supplierinfo_views.xml @@ -0,0 +1,31 @@ + + + + + + product.supplierinfo.form.origin.text + product.supplierinfo + + + + + + + + + + + product.supplierinfo.tree.origin.text + product.supplierinfo + + + + + + + + diff --git a/product_origin_char/views/product_template_views.xml b/product_origin_char/views/product_template_views.xml new file mode 100644 index 0000000..19d9db7 --- /dev/null +++ b/product_origin_char/views/product_template_views.xml @@ -0,0 +1,44 @@ + + + + + + product.template.form.origin.text + product.template + + + + + + + + + + + product.product.tree.origin.text + product.product + + + + + + + + + + + product.template.tree.origin.text + product.template + + + + + + + +