Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions product_uom_packaging/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

=====================
Product UoM Packaging
=====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:78154503ae387e6d2fb01e10858678bea98a026add6ae6b94ccc2d4dd69c8737
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github
:target: https://github.com/OCA/product-attribute/tree/19.0/product_uom_packaging
:alt: OCA/product-attribute
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/product-attribute-19-0/product-attribute-19-0-product_uom_packaging
: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/product-attribute&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module provides a bridge between products, units of measure, and
package types.

In Odoo 19, the ``product.packaging`` model was removed and packaging
functionality was merged into the UoM system. However, UoMs are shared
across products, which means you cannot specify product-specific package
dimensions.

This module introduces a ``product.uom.packaging`` model that allows you
to:

- Define which package type (with physical dimensions) applies to a
specific product when sold/purchased in a specific UoM
- For example: "Product A in a 12-pack uses a 12x12x12 box" while
"Product B in a 12-pack uses a 6x12x8 box"

The package type (from ``stock.package.type``) provides:

- Physical dimensions (length, width, height)
- Weight limits (base weight, max weight)
- Barcode for the package type

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Usage
=====

1. Go to Inventory > Configuration > Products > Product UoM Packaging
2. Create a new record linking a product to a UoM and optionally a
package type
3. The package type defines the physical dimensions used when the
product is packaged in that UoM

Alternatively, this information can be managed directly from the product
form (requires extending the product views, which may be done in a
separate module).

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/product-attribute/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 <https://github.com/OCA/product-attribute/issues/new?body=module:%20product_uom_packaging%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* Bemade Inc.

Contributors
------------

- Marc Durepos marc@bemade.org, Bemade Inc.

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-mdurepos| image:: https://github.com/mdurepos.png?size=40px
:target: https://github.com/mdurepos
:alt: mdurepos

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-mdurepos|

This module is part of the `OCA/product-attribute <https://github.com/OCA/product-attribute/tree/19.0/product_uom_packaging>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions product_uom_packaging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
35 changes: 35 additions & 0 deletions product_uom_packaging/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2025 Bemade Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

{
"name": "Product UoM Packaging",
"summary": """
Link products to UoMs with package type for dimensions""",
"version": "19.0.1.0.0",
"development_status": "Alpha",
"license": "LGPL-3",
"author": "Bemade Inc., Odoo Community Association (OCA)",
"website": "https://github.com/OCA/product-attribute",
"maintainers": ["mdurepos"],
"depends": [
"product",
"stock",
],
"data": [
"security/ir.model.access.csv",
"views/product_uom_packaging_views.xml",
"views/product_product_views.xml",
"views/stock_move_views.xml",
"views/stock_move_line_views.xml",
"views/stock_picking_views.xml",
],
"assets": {
"web.assets_unit_tests": [
"product_uom_packaging/static/tests/**/*",
("remove", "product_uom_packaging/static/tests/tours/**/*"),
],
"web.assets_tests": [
"product_uom_packaging/static/tests/tours/**/*",
],
},
}
8 changes: 8 additions & 0 deletions product_uom_packaging/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from . import (
product_uom_packaging,
product_template,
product_product,
stock_move,
stock_move_line,
stock_picking,
)
110 changes: 110 additions & 0 deletions product_uom_packaging/models/product_product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from odoo import api, fields, models


class ProductProduct(models.Model):
_inherit = "product.product"

# Stored Many2many relationship (inverse of product_variant_ids)
variant_packaging_ids = fields.Many2many(
"product.uom.packaging",
relation="product_uom_packaging_variant_rel",
column1="product_id",
column2="packaging_id",
string="Variant-Specific Packaging",
help="Packaging configurations specifically for this variant.",
)

# Computed field for template-level packaging (read-only)
template_packaging_ids = fields.Many2many(
"product.uom.packaging",
compute="_compute_template_packaging_ids",
string="Template Packaging",
help="Packaging configurations inherited from product template.",
)

# Combined field (computed, read-only for display)
packaging_ids = fields.Many2many(
"product.uom.packaging",
compute="_compute_packaging_ids",
inverse="_inverse_packaging_ids",
string="All Packaging",
help="All packaging for this variant (template + variant).",
)

def action_open_template(self):
"""Open the product template form view."""
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": "product.template",
"res_id": self.product_tmpl_id.id,
"view_mode": "form",
"target": "current",
}

@api.depends("product_tmpl_id.packaging_ids")
def _compute_template_packaging_ids(self):
"""Compute template-level packaging configurations inherited by this variant."""
for product in self:
template_configs = self.env["product.uom.packaging"].search(
[
("product_tmpl_id", "=", product.product_tmpl_id.id),
("company_id", "=", self.env.company.id),
("product_variant_ids", "=", False),
]
)
product.template_packaging_ids = template_configs

@api.depends("template_packaging_ids", "variant_packaging_ids")
def _compute_packaging_ids(self):
"""Compute all packaging configurations for this variant."""
for product in self:
# Combine template-level and variant-level configs
product.packaging_ids = (
product.template_packaging_ids + product.variant_packaging_ids
)

def _inverse_packaging_ids(self):
"""
Inverse function to handle changes to packaging configurations
on the product template but not related to any specific variants.
"""
for product in self:
# Get current template packaging (excluding variant-specific ones)
current_template_packaging = product.product_tmpl_id.packaging_ids.filtered(
lambda p: not p.product_variant_ids
)

# Get desired packaging from computed field (excl. variant-specific)
variant_packaging = product.variant_packaging_ids
desired_packaging = product.packaging_ids.filtered(
lambda p, vp=variant_packaging: p not in vp
)

# Find packaging to remove (in current but not in desired)
to_remove = current_template_packaging - desired_packaging
# Find packaging to add (in desired but not in current)
to_add = desired_packaging - current_template_packaging

# Remove unwanted packaging
if to_remove:
to_remove.unlink()

# Add new packaging by setting the correct template. Only clear
# product_variant_ids if it was already empty (template-level)
if to_add:
for packaging in to_add:
if not packaging.product_variant_ids:
packaging.write(
{
"product_tmpl_id": product.product_tmpl_id.id,
"product_variant_ids": False,
}
)
else:
# This is variant-specific packaging, just set the template
packaging.write(
{
"product_tmpl_id": product.product_tmpl_id.id,
}
)
13 changes: 13 additions & 0 deletions product_uom_packaging/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import fields, models


class ProductTemplate(models.Model):
_inherit = "product.template"

# Stored One2many for direct template-level packaging management
packaging_ids = fields.One2many(
"product.uom.packaging",
"product_tmpl_id",
string="Packaging Configurations",
help="Packaging configurations for this product template.",
)
Loading