Skip to content

Commit

Permalink
[ADD] purchase_by_packaging: Added module.
Browse files Browse the repository at this point in the history
  • Loading branch information
mibab.heli committed Mar 13, 2024
1 parent 3fc2e6e commit e69fa60
Show file tree
Hide file tree
Showing 22 changed files with 1,153 additions and 0 deletions.
118 changes: 118 additions & 0 deletions purchase_by_packaging/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
=====================
Purchase By Packaging
=====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:53eb37fdcb59a67839d55db5839c08ddaa0eef12c42d2a09a26ef4d724404118
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/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%2Fpurchase--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/purchase-workflow/tree/16.0/purchase_by_packaging
:alt: OCA/purchase-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/purchase-workflow-16-0/purchase-workflow-16-0-purchase_by_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/purchase-workflow&target_branch=16.0
:alt: Try me on Runboat

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

This module provides different configuration option to manage packagings on
purchase orders.

The creation/update of purchase order line will be blocked (by constraints) if the data
on the purchase.order.line does not fit with the configuration of the product's
packagings.

It's also possible to force the quantity to purchase during creation/modification of
the purchase order line if the "Force purchase quantity" is ticked on the packaging.

For example, if your packaging is set to purchase by 5 units and the employee fill
the quantity with 3, the quantity will be automatically replaced by 5 (it always
rounds up).

.. 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:

Configuration
=============

Following options are available to define which packaging type can be purchased and
which product can only be purchased by packaging.

* Can be purchased: On product packaging type model, this checkbox defines if product
packagings from this particular type are available to be selected on purchase
order line.

* Purchase only by packaging: On product template model, this checkbox restricts
purchases of these products if no packaging is selected on the purchase order line.
If no packaging is selected, it will either be auto-assigned if the quantity
on the purchase order line matches a packaging quantity or an error will be raised.

* Force purchase quantity (on the packaging): force rounds up the quantity during
creation/modification of the purchase order line with the factor set on the packaging.

Known issues / Roadmap
======================

* Odoo allows to define product.packaging records with a qty = 0. This does not
make much sense with the can_be_purchased checkbox, and we probably need to add a
constraint here.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/purchase-workflow/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/purchase-workflow/issues/new?body=module:%20purchase_by_packaging%0Aversion:%2016.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
~~~~~~~

* Ametras

Contributors
~~~~~~~~~~~~

* Bastian Guenther <[email protected]>

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.

This module is part of the `OCA/purchase-workflow <https://github.com/OCA/purchase-workflow/tree/16.0/purchase_by_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 purchase_by_packaging/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
21 changes: 21 additions & 0 deletions purchase_by_packaging/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Purchase By Packaging",
"summary": "Manage purchase of packaging",
"version": "16.0.1.0.0",
"development_status": "Alpha",
"category": "Warehouse Management",
"website": "https://github.com/OCA/purchase-workflow",
"author": "Ametras, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"purchase_only_by_packaging",
"product_packaging_level",
"purchase_packaging_uom",
],
"data": [
"views/product_packaging.xml",
],
}
4 changes: 4 additions & 0 deletions purchase_by_packaging/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import product_packaging
from . import product_product
from . import product_template
from . import purchase_order_line
23 changes: 23 additions & 0 deletions purchase_by_packaging/models/product_packaging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2021 Ametras
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, fields, models


class ProductPackaging(models.Model):
_inherit = "product.packaging"

purchase_rounding = fields.Float(
string="Purchase Rounding Precision",
digits="Product Unit of Measure",
required=True,
default=0.1,
help="The allowed package quantity will be a multiple of this value. "
"Use 1.0 for a package that cannot be further split.",
)

actual_purchase_qty = fields.Float(compute="_compute_actual_purchase_qty")

@api.depends("purchase_rounding", "qty")
def _compute_actual_purchase_qty(self):
for record in self:
record.actual_purchase_qty = record.purchase_rounding * record.qty
54 changes: 54 additions & 0 deletions purchase_by_packaging/models/product_product.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import fields, models
from odoo.tools import float_compare, float_is_zero, float_round


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

def _convert_purchase_packaging_qty(self, qty, uom, packaging):
"""
Convert the given qty with given UoM to the packaging uom.
To do that, first transform the qty to the reference UoM and then
transform using the packaging UoM.
The given qty is not updated if the product has purchase_only_by_packaging
set to False or if the packaging is not set.
Inspired from purchase_order_line_packaging_qty/
models.purchase_order_line.py _check_package(...)
:param qty: float
:return: float
"""
if not self or not packaging:
return qty
self.ensure_one()
if packaging.force_purchase_qty:
q = self.uom_po_id._compute_quantity(packaging.qty, uom)
if (
qty
and q
and float_compare(
qty / q,
float_round(qty / q, precision_rounding=1.0),
precision_rounding=0.001,
)
!= 0
):
qty = qty - (qty % q) + q
return qty

def get_first_purchase_packaging_with_multiple_qty(self, qty):
"""Return multiple of product packaging for one quantity if exist."""
self.ensure_one()
packagings = self._get_purchase_packagings_with_multiple_qty(qty)
return fields.first(packagings.sorted("qty"))

def _get_purchase_packagings_with_multiple_qty(self, qty):
self.ensure_one()
return self.packaging_ids.filtered(
lambda pack: pack.can_be_purchased
and not float_is_zero(
pack.qty, precision_rounding=pack.product_uom_po_id.rounding
)
)
12 changes: 12 additions & 0 deletions purchase_by_packaging/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2021 Ametras
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, models


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

@api.onchange("purchase_ok")
def _change_purchase_ok(self):
if not self.purchase_ok and self.purchase_only_by_packaging:
self.purchase_only_by_packaging = False
125 changes: 125 additions & 0 deletions purchase_by_packaging/models/purchase_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright 2021 Ametras
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, models


class PurchaseOrderLine(models.Model):

_inherit = "purchase.order.line"

def get_packaging_qty(self, vals=None):
if not vals:
vals = []
product = (
self.env["product.product"].browse(vals["product_id"])
if "product_id" in vals
else self.product_id
)

quantity = vals["product_qty"] if "product_qty" in vals else self.product_qty
uom = (
self.env["uom.uom"].browse(vals["product_uom"])
if "product_uom" in vals
else self.product_uom
)
packaging = (
self.env["product.packaging"].browse(vals["product_packaging_id"])
if "product_packaging_id" in vals
else self.product_packaging_id
)

return product._convert_purchase_packaging_qty(
quantity,
uom or product.uom_po_id,
packaging=packaging,
)

@api.depends("product_id", "product_uom_qty", "product_uom")
def _compute_product_packaging_id(self):
for rec in self:
rec.product_packaging_id = False
rec._force_packaging()
rec._force_qty_with_package()
return super()._compute_product_packaging_id()

def _get_product_packaging_having_multiple_qty(self, product, qty, uom):
if uom != product.uom_po_id:
qty = uom._compute_quantity(qty, product.uom_po_id)
return product.get_first_purchase_packaging_with_multiple_qty(qty)

def _inverse_product_packaging_qty(self):
# Force skipping of auto assign
# if we are writing the product_qty directly via inverse
return super(
PurchaseOrderLine, self.with_context(_skip_auto_assign=True)
)._inverse_product_packaging_qty()

def write(self, vals):
"""Auto assign packaging if needed"""
if vals.get("product_packaging_id") or self.env.context.get(
"_skip_auto_assign"
):
# setting the packaging directly, skip auto assign
return super().write(vals)
for line in self:
line_vals = vals.copy()
if line_vals.get("product_id", False):
packaging = line._get_autoassigned_packaging(line_vals)
if packaging:
line_vals.update({"product_packaging_id": packaging})
if (
line_vals.get("product_qty")
or line_vals.get("product_id")
or line_vals.get("product_packaging_id")
or line_vals.get("product_uom")
):
product_qty = line.get_packaging_qty(line_vals)
line_vals.update({"product_qty": product_qty})
super(PurchaseOrderLine, line).write(line_vals)
return True

@api.model_create_multi
def create(self, vals_list):
"""Auto assign packaging if needed"""
# Fill the packaging if they are empty and the quantity is a multiple
for vals in vals_list:
if not vals.get("product_packaging_id"):
if "product_qty" not in vals:
vals["product_qty"] = 1.0
packaging = self._get_autoassigned_packaging(vals)
if packaging:
vals.update({"product_packaging_id": packaging})
if vals.get("product_id") and vals.get("product_uom"):
product_qty = self.get_packaging_qty(vals)
vals.update({"product_qty": product_qty})
return super().create(vals_list)

def _get_autoassigned_packaging(self, vals=None):
if not vals:
vals = []
product = (
self.env["product.product"].browse(vals["product_id"])
if "product_id" in vals
else self.product_id
)
if product and product.purchase_only_by_packaging:
quantity = (
vals["product_qty"] if "product_qty" in vals else self.product_qty
)
uom = (
self.env["uom.uom"].browse(vals["product_uom"])
if "product_uom" in vals
else self.product_uom
)
packaging = self._get_product_packaging_having_multiple_qty(
product, quantity, uom
)
if packaging:
return packaging.id
return None

def _force_packaging(self):
if not self.product_packaging_id and self.product_id.purchase_only_by_packaging:
packaging_id = self._get_autoassigned_packaging()
if packaging_id:
self.update({"product_packaging_id": packaging_id})
14 changes: 14 additions & 0 deletions purchase_by_packaging/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Following options are available to define which packaging type can be purchased and
which product can only be purchased by packaging.

* Can be purchased: On product packaging type model, this checkbox defines if product
packagings from this particular type are available to be selected on purchase
order line.

* Purchase only by packaging: On product template model, this checkbox restricts
purchases of these products if no packaging is selected on the purchase order line.
If no packaging is selected, it will either be auto-assigned if the quantity
on the purchase order line matches a packaging quantity or an error will be raised.

* Force purchase quantity (on the packaging): force rounds up the quantity during
creation/modification of the purchase order line with the factor set on the packaging.
1 change: 1 addition & 0 deletions purchase_by_packaging/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Bastian Guenther <[email protected]>
13 changes: 13 additions & 0 deletions purchase_by_packaging/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This module provides different configuration option to manage packagings on
purchase orders.

The creation/update of purchase order line will be blocked (by constraints) if the data
on the purchase.order.line does not fit with the configuration of the product's
packagings.

It's also possible to force the quantity to purchase during creation/modification of
the purchase order line if the "Force purchase quantity" is ticked on the packaging.

For example, if your packaging is set to purchase by 5 units and the employee fill
the quantity with 3, the quantity will be automatically replaced by 5 (it always
rounds up).
Loading

0 comments on commit e69fa60

Please sign in to comment.