Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

153
sale_packaging_default/README.rst Executable file
View File

@@ -0,0 +1,153 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
===========================
Default packaging for sales
===========================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:b4285d78917da55a4c2dbf7f5b262b55d75267b6383e7ae483a4dd47a10e50f1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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%2Fsale--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/sale-workflow/tree/18.0/sale_packaging_default
:alt: OCA/sale-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_packaging_default
: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/sale-workflow&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module simplifies and emphasizes the usage of packaging in sales
orders.
- Packaging fields in sale order lines appear before product quantity
fields.
- When selecting a product to sell, its default packaging is added
automatically.
.. 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:
Use Cases / Context
===================
Some companies, like food distributors, need to emphasize the usage of
product packaging in the sale process.
Product packaging in Odoo is designed to be computed automatically when
you choose the final amount of products to sell. For example, if you
sell 12 kgs of apples and those can be packaged by bags of 4kg, you'll
get automatically suggested to sell 3 bags.
Although you can still do that, with this module the natural usage is
inverted: it's easier for you to select the packaging and quantity
first, and then the final amount of products is computed automatically.
Following the same example, you'll choose bags, then 3, and then the
12kg will be computed automatically.
Configuration
=============
To configure this module, you need to:
1. Go to *Sales > Configuration > Settings*.
2. Under *Product Catalog*, enable *Product Packagings*.
Usage
=====
To use this module, you need to configure a product with packaging:
1. Go to *Sales > Products > Products* and select or create a product.
2. In the *Inventory* tab, add some packaging(s).
3. Enable *Sales* and in one packaging.
4. Sort the packaging options at will. The first one enabled for *Sales*
will be considered the default one.
Then you have to sell it:
1. Go to *Sales > Orders > Quotations* and create a new quotation.
2. Select any customer.
3. Select that product.
You will notice that:
- The product is added with the default sale packaging.
- The packaging quantity is set to 1 packaging unit.
- The product UoM quantity is set to the amount of units contained in 1
packaging.
- When changing to another product, instead of keeping the amount of UoM
units, we now keep the packaging qty, and the UoM qty is recomputed
accordingly.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-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/sale-workflow/issues/new?body=module:%20sale_packaging_default%0Aversion:%2018.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
-------
* Moduon
Contributors
------------
- Jairo Llopis (`Moduon <https://www.moduon.team/>`__)
- Emilio Pascual (`Moduon <https://www.moduon.team/>`__)
- Sabrina Rodriguez (`Moduon <https://www.moduon.team/>`__)
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-yajo| image:: https://github.com/yajo.png?size=40px
:target: https://github.com/yajo
:alt: yajo
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-yajo|
This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/18.0/sale_packaging_default>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,20 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
{
"name": "Default packaging for sales",
"version": "18.0.1.0.0",
"summary": "Simplify using products default packaging for sales",
"development_status": "Alpha",
"category": "Sales",
"website": "https://github.com/OCA/sale-workflow",
"author": "Moduon, Odoo Community Association (OCA)",
"maintainers": ["yajo"],
"license": "LGPL-3",
"application": False,
"installable": True,
"depends": ["sale"],
"data": [
"views/sale_order_view.xml",
],
}

View File

@@ -0,0 +1,37 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_packaging_default
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-03-07 12:35+0000\n"
"Last-Translator: Jairo Llopis <yajo.sk8@gmail.com>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_product_packaging
msgid "Product Packaging"
msgstr "Envase del producto"
#. module: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de Pedido de Venta"
#~ msgid "Default for sales"
#~ msgstr "Por defecto para ventas"
#~ msgid ""
#~ "The first packaging with this option checked will be used by default in "
#~ "sales orders."
#~ msgstr ""
#~ "El primer empaquetado de la lista con esta opción marcada es el que se "
#~ "utilizará por defecto en los pedidos de venta."

View File

@@ -0,0 +1,37 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_packaging_default
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-12-04 09:34+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_product_packaging
msgid "Product Packaging"
msgstr "Imballaggio prodotto"
#. module: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_sale_order_line
msgid "Sales Order Line"
msgstr "Riga ordine di vendita"
#~ msgid "Default for sales"
#~ msgstr "Predefinito per le vendite"
#~ msgid ""
#~ "The first packaging with this option checked will be used by default in "
#~ "sales orders."
#~ msgstr ""
#~ "Il primo imballaggio con questa opzione selezionata verrà utilizzato in "
#~ "modo predefinito negli ordini di vendita."

View File

@@ -0,0 +1,24 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_packaging_default
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \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: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_product_packaging
msgid "Product Packaging"
msgstr ""
#. module: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_sale_order_line
msgid "Sales Order Line"
msgstr ""

View File

@@ -0,0 +1,2 @@
from . import product_packaging
from . import sale_order_line

View File

@@ -0,0 +1,13 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import models
class ProductPackaging(models.Model):
_inherit = "product.packaging"
def _find_suitable_product_packaging(self, product_qty, uom_id):
"""Find nothing if you want to keep what was there."""
if self.env.context.get("keep_product_packaging"):
return self.browse()
return super()._find_suitable_product_packaging(product_qty, uom_id)

View File

@@ -0,0 +1,89 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import api, fields, models
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
def onchange(self, values, field_name, field_onchange):
"""Record which field was being changed."""
if isinstance(field_name, list):
names = set(field_name)
elif field_name:
names = {field_name}
else:
names = set()
_self = self.with_context(changing_fields=names)
return super(SaleOrderLine, _self).onchange(values, field_name, field_onchange)
@api.depends("product_id", "product_uom_qty", "product_uom")
def _compute_product_packaging_id(self):
"""Set a default packaging for sales if possible."""
for line in self:
if line.product_id != line.product_packaging_id.product_id:
line.product_packaging_id = line._get_default_packaging(line.product_id)
result = super()._compute_product_packaging_id()
# If there's no way to package the desired qty, remove the packaging.
# It is only done when the user is currently manually setting
# `product_uom_qty` to zero. In other cases, we are maybe getting
# default values and this difference will get fixed by other compute
# methods later.
if (
self.env.context.get("changing_fields")
and "product_uom_qty" not in self.env.context["changing_fields"]
):
return result
for line in self:
if (
line.product_uom_qty
and line.product_packaging_id
and line.product_uom
and line.product_uom_qty
!= line.product_packaging_id._check_qty(
line.product_uom_qty, line.product_uom
)
):
line.product_packaging_id = False
return result
@api.model
def _get_default_packaging(self, product):
return fields.first(
product.packaging_ids.filtered_domain([("sales", "=", True)])
)
@api.depends("product_packaging_id", "product_uom", "product_uom_qty")
def _compute_product_packaging_qty(self):
"""Set a valid packaging quantity."""
changing_fields = self.env.context.get("changing_fields", set())
# Keep the packaging qty when changing the product
if "product_id" in changing_fields and all(
line.product_id and line.product_packaging_qty for line in self
):
return
result = super()._compute_product_packaging_qty()
for line in self:
if not line.product_packaging_id:
continue
# Reset to 1 packaging if it's empty or not a whole number
if not line.product_packaging_qty or line.product_packaging_qty % 1:
line.product_packaging_qty = int(
"product_uom_qty" not in changing_fields
)
return result
@api.depends(
"display_type",
"product_id",
"product_packaging_id",
"product_packaging_qty",
)
def _compute_product_uom_qty(self):
# Avoid a circular dependency. Upstream `product_uom_qty` has an
# undeclared dependency over `product_packaging_qty`, which depends
# again on `product_uom_qty`.
_self = self.with_context(keep_product_packaging=True)
result = super(SaleOrderLine, _self)._compute_product_uom_qty()
return result

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,4 @@
To configure this module, you need to:
1. Go to *Sales \> Configuration \> Settings*.
2. Under *Product Catalog*, enable *Product Packagings*.

View File

@@ -0,0 +1,12 @@
Some companies, like food distributors, need to emphasize the usage of product
packaging in the sale process.
Product packaging in Odoo is designed to be computed automatically when you
choose the final amount of products to sell. For example, if you sell 12 kgs of
apples and those can be packaged by bags of 4kg, you'll get automatically
suggested to sell 3 bags.
Although you can still do that, with this module the natural usage is inverted:
it's easier for you to select the packaging and quantity first, and then the
final amount of products is computed automatically. Following the same example,
you'll choose bags, then 3, and then the 12kg will be computed automatically.

View File

@@ -0,0 +1,3 @@
- Jairo Llopis ([Moduon](https://www.moduon.team/))
- Emilio Pascual ([Moduon](https://www.moduon.team/))
- Sabrina Rodriguez ([Moduon](https://www.moduon.team/))

View File

@@ -0,0 +1,7 @@
This module simplifies and emphasizes the usage of packaging in sales
orders.
- Packaging fields in sale order lines appear before product quantity
fields.
- When selecting a product to sell, its default packaging is added
automatically.

View File

@@ -0,0 +1,21 @@
To use this module, you need to configure a product with packaging:
1. Go to *Sales \> Products \> Products* and select or create a
product.
2. In the *Inventory* tab, add some packaging(s).
3. Enable *Sales* and in one packaging.
4. Sort the packaging options at will. The first one enabled for *Sales* will
be considered the default one.
Then you have to sell it:
1. Go to *Sales \> Orders \> Quotations* and create a new quotation.
2. Select any customer.
3. Select that product.
You will notice that:
- The product is added with the default sale packaging.
- The packaging quantity is set to 1 packaging unit.
- The product UoM quantity is set to the amount of units contained in 1 packaging.
- When changing to another product, instead of keeping the amount of UoM units,
we now keep the packaging qty, and the UoM qty is recomputed accordingly.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,498 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="default-packaging-for-sales">
<h1>Default packaging for sales</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:b4285d78917da55a4c2dbf7f5b262b55d75267b6383e7ae483a4dd47a10e50f1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/18.0/sale_packaging_default"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_packaging_default"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module simplifies and emphasizes the usage of packaging in sales
orders.</p>
<ul class="simple">
<li>Packaging fields in sale order lines appear before product quantity
fields.</li>
<li>When selecting a product to sell, its default packaging is added
automatically.</li>
</ul>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">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.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#use-cases-context" id="toc-entry-1">Use Cases / Context</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="use-cases-context">
<h2><a class="toc-backref" href="#toc-entry-1">Use Cases / Context</a></h2>
<p>Some companies, like food distributors, need to emphasize the usage of
product packaging in the sale process.</p>
<p>Product packaging in Odoo is designed to be computed automatically when
you choose the final amount of products to sell. For example, if you
sell 12 kgs of apples and those can be packaged by bags of 4kg, youll
get automatically suggested to sell 3 bags.</p>
<p>Although you can still do that, with this module the natural usage is
inverted: its easier for you to select the packaging and quantity
first, and then the final amount of products is computed automatically.
Following the same example, youll choose bags, then 3, and then the
12kg will be computed automatically.</p>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-2">Configuration</a></h2>
<p>To configure this module, you need to:</p>
<ol class="arabic simple">
<li>Go to <em>Sales &gt; Configuration &gt; Settings</em>.</li>
<li>Under <em>Product Catalog</em>, enable <em>Product Packagings</em>.</li>
</ol>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-3">Usage</a></h2>
<p>To use this module, you need to configure a product with packaging:</p>
<ol class="arabic simple">
<li>Go to <em>Sales &gt; Products &gt; Products</em> and select or create a product.</li>
<li>In the <em>Inventory</em> tab, add some packaging(s).</li>
<li>Enable <em>Sales</em> and in one packaging.</li>
<li>Sort the packaging options at will. The first one enabled for <em>Sales</em>
will be considered the default one.</li>
</ol>
<p>Then you have to sell it:</p>
<ol class="arabic simple">
<li>Go to <em>Sales &gt; Orders &gt; Quotations</em> and create a new quotation.</li>
<li>Select any customer.</li>
<li>Select that product.</li>
</ol>
<p>You will notice that:</p>
<ul class="simple">
<li>The product is added with the default sale packaging.</li>
<li>The packaging quantity is set to 1 packaging unit.</li>
<li>The product UoM quantity is set to the amount of units contained in 1
packaging.</li>
<li>When changing to another product, instead of keeping the amount of UoM
units, we now keep the packaging qty, and the UoM qty is recomputed
accordingly.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-4">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_packaging_default%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-5">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-6">Authors</a></h3>
<ul class="simple">
<li>Moduon</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-7">Contributors</a></h3>
<ul class="simple">
<li>Jairo Llopis (<a class="reference external" href="https://www.moduon.team/">Moduon</a>)</li>
<li>Emilio Pascual (<a class="reference external" href="https://www.moduon.team/">Moduon</a>)</li>
<li>Sabrina Rodriguez (<a class="reference external" href="https://www.moduon.team/">Moduon</a>)</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-8">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/yajo"><img alt="yajo" src="https://github.com/yajo.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/18.0/sale_packaging_default">OCA/sale-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_sale_packaging_default

View File

@@ -0,0 +1,165 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import fields
from odoo.tests import Form
from odoo.addons.product.tests.common import ProductCommon
class SalePackagingDefaultCase(ProductCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.groups_id |= cls.env.ref("product.group_stock_packaging")
with Form(cls.product) as product_f:
with product_f.packaging_ids.new() as packaging_f:
packaging_f.name = "Dozen"
packaging_f.qty = 12
packaging_f.sales = True
packaging_f.sequence = 20
with product_f.packaging_ids.new() as packaging_f:
packaging_f.name = "Big box"
packaging_f.qty = 100
packaging_f.sales = True
packaging_f.sequence = 10 # This is the default one
cls.big_box, cls.dozen = cls.product.packaging_ids
assert cls.dozen.name == "Dozen"
assert cls.big_box.name == "Big box"
cls.product2 = cls.env["product.product"].create(
{
"name": "Product 2",
"type": "consu",
"packaging_ids": [
fields.Command.create(
{
"name": "3-pack",
"qty": 3,
"sales": True,
"sequence": 10,
}
),
],
}
)
cls.p2_three_pack = cls.product2.packaging_ids[0]
assert cls.p2_three_pack.name == "3-pack"
cls.product_packaging_qty_no_integer = cls.env["product.product"].create(
{
"name": "Product packaging with qty not integer",
"type": "consu",
"packaging_ids": [
fields.Command.create(
{
"name": "1 Piece",
"qty": 1.6,
"sales": True,
"sequence": 10,
}
),
],
}
)
cls.packaging = cls.product_packaging_qty_no_integer.packaging_ids
def test_default_packaging_sale_order(self):
"""Check is packaging usage in sale order."""
# Create a sale order with the product
so_f = Form(self.env["sale.order"])
so_f.partner_id = self.partner
with so_f.order_line.new() as line_f:
line_f.product_id = self.product
# Automatically set the default packaging and the quantity
self.assertEqual(line_f.product_packaging_id, self.big_box)
self.assertEqual(line_f.product_packaging_qty, 1)
self.assertEqual(line_f.product_uom_qty, 100)
# Change the packaging, and qtys are recalculated
line_f.product_packaging_id = self.dozen
self.assertEqual(line_f.product_packaging_qty, 1)
self.assertEqual(line_f.product_uom_qty, 12)
# Change product qty, and packaging is recalculated
line_f.product_uom_qty = 1200
self.assertEqual(line_f.product_packaging_qty, 12)
self.assertEqual(line_f.product_packaging_id, self.big_box)
self.assertEqual(line_f.product_uom_qty, 1200)
# I want it in dozens, so I change the packaging
line_f.product_packaging_id = self.dozen
self.assertEqual(line_f.product_packaging_id, self.dozen)
self.assertEqual(line_f.product_uom_qty, 1200)
self.assertEqual(line_f.product_packaging_qty, 100)
# I want less dozens, so I change the packaging qty
line_f.product_packaging_qty = 90
self.assertEqual(line_f.product_packaging_id, self.dozen)
self.assertEqual(line_f.product_uom_qty, 1080)
# Change the packaging again, and qtys are recalculated
line_f.product_packaging_id = self.big_box
self.assertEqual(line_f.product_packaging_qty, 1)
self.assertEqual(line_f.product_uom_qty, 100)
# I want more units, so I change the uom qty
line_f.product_uom_qty = 120
self.assertEqual(line_f.product_packaging_qty, 10)
self.assertEqual(line_f.product_packaging_id, self.dozen)
# If I set a uom qty without packaging, it is emptied
line_f.product_uom_qty = 7
self.assertFalse(line_f.product_packaging_id)
self.assertEqual(line_f.product_packaging_qty, 0)
self.assertEqual(line_f.product_uom_qty, 7)
# Setting zero uom qty resets to the default packaging
line_f.product_uom_qty = 0
self.assertEqual(line_f.product_packaging_id, self.big_box)
self.assertEqual(line_f.product_packaging_qty, 0)
self.assertEqual(line_f.product_uom_qty, 0)
def test_sale_order_product_picker_compatibility(self):
"""Emulate a call done by the product picker module and see it works.
This test asserts support for cross-compatibility with
`sale_order_product_picker`.
"""
so_f = Form(
self.env["sale.order"].with_context(
default_product_id=self.product.id, default_price_unit=20
)
)
so_f.partner_id = self.partner
# User clicks on +1 button
with so_f.order_line.new() as line_f:
self.assertEqual(line_f.product_uom_qty, 1)
self.assertFalse(line_f.product_packaging_id)
def test_product_change(self):
"""Set one product, alter qtys, change product, qtys are reset."""
so_f = Form(self.env["sale.order"])
so_f.partner_id = self.partner
with so_f.order_line.new() as line_f:
line_f.product_id = self.product
self.assertEqual(line_f.product_packaging_id, self.big_box)
self.assertEqual(line_f.product_packaging_qty, 1)
self.assertEqual(line_f.product_uom_qty, 100)
line_f.product_uom_qty = 120
self.assertEqual(line_f.product_packaging_id, self.dozen)
self.assertEqual(line_f.product_packaging_qty, 10)
self.assertEqual(line_f.product_uom_qty, 120)
line_f.product_id = self.product2
self.assertEqual(line_f.product_packaging_id, self.p2_three_pack)
self.assertEqual(line_f.product_packaging_qty, 10)
self.assertEqual(line_f.product_uom_qty, 30)
def test_product_packaging_qty_no_integer(self):
"""Check behavior with float qty in packaging without using modulo operator.
If product_packaging_qty is multiple of qty pacakging, with modulo operator the
quantity per package might be a float. Example # 8 % 1.6 = 1.5999999999999996
"""
so_f = Form(self.env["sale.order"])
so_f.partner_id = self.partner
with so_f.order_line.new() as line_f:
line_f.product_id = self.product_packaging_qty_no_integer
# Automatically set the default packaging and the quantity
self.assertEqual(line_f.product_packaging_id, self.packaging)
self.assertEqual(line_f.product_packaging_qty, 1)
self.assertEqual(line_f.product_uom_qty, 1.6)
# Change qty to 8 to force calculate with modulo operator
# (8 % 1.6 = 1.5999999999999996)
line_f.product_packaging_qty = 5
self.assertEqual(line_f.product_packaging_id, self.packaging)
self.assertEqual(line_f.product_uom_qty, 8)

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Moduon Team S.L. <info@moduon.team>
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->
<data>
<record id="view_order_form_inherit_sale" model="ir.ui.view">
<field name="name">Simplify packaging fields</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<!-- Move packaging and packaging qty before product qty in tree view -->
<xpath
expr="//field[@name='order_line']/list/field[@name='product_uom_qty']"
position="before"
>
<xpath
expr="//field[@name='order_line']/list/field[@name='product_packaging_id']"
position="move"
/>
<xpath
expr="//field[@name='order_line']/list/field[@name='product_packaging_qty']"
position="move"
/>
</xpath>
<!-- Move packaging and packaging qty before product qty in form view -->
<xpath
expr="//field[@name='order_line']/form//label[@for='product_uom_qty']"
position="before"
>
<xpath
expr="//field[@name='order_line']/form//field[@name='product_packaging_id']"
position="move"
/>
<xpath
expr="//field[@name='order_line']/form//field[@name='product_packaging_qty']"
position="move"
/>
</xpath>
</field>
</record>
</data>