Initial commit: OCA Workflow Process packages (456 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:00 +02:00
commit d366e42934
18799 changed files with 1284507 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# Default packaging for sales
Odoo addon: sale_packaging_default
## Installation
```bash
pip install odoo-bringout-oca-sale-workflow-sale_packaging_default
```
## Dependencies
This addon depends on:
- sale
## Manifest Information
- **Name**: Default packaging for sales
- **Version**: 16.0.2.2.1
- **Category**: Sales
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/sale-workflow](https://github.com/OCA/sale-workflow) branch 16.0, addon `sale_packaging_default`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Sale_packaging_default Module - sale_packaging_default
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for sale_packaging_default. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [sale](../../odoo-bringout-oca-ocb-sale)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon sale_packaging_default or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-sale-workflow-sale_packaging_default"
# or
uv pip install odoo-bringout-oca-sale-workflow-sale_packaging_default"
```

View file

@ -0,0 +1,13 @@
# Models
Detected core models and extensions in sale_packaging_default.
```mermaid
classDiagram
class product_packaging
class sale_order_line
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: sale_packaging_default. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon sale_packaging_default
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon sale_packaging_default
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-sale-workflow-sale_packaging_default"
version = "16.0.0"
description = "Default packaging for sales - Simplify using products default packaging for sales"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-sale>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["sale_packaging_default"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,152 @@
.. 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:b12ce0af56606edb1ae20928e7d47ffca4ded69f5eb06c128bca968a1470ca30
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/16.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-16-0/sale-workflow-16-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=16.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:%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
-------
* Moduon
Contributors
------------
- Jairo Llopis (`Moduon <https://www.moduon.team/>`__)
- Emilio Pascual (`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/16.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": "16.0.2.2.1",
"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,24 @@
# 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"
"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 "Pakiranje proizvoda"
#. module: sale_packaging_default
#: model:ir.model,name:sale_packaging_default.model_sale_order_line
msgid "Sales Order Line"
msgstr "Stavka prodajne narudžbe"

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 16.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,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,2 @@
- Jairo Llopis ([Moduon](https://www.moduon.team/))
- Emilio Pascual ([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,497 @@
<!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:b12ce0af56606edb1ae20928e7d47ffca4ded69f5eb06c128bca968a1470ca30
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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/16.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-16-0/sale-workflow-16-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=16.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:%2016.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>
</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/16.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.common 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']/tree/field[@name='product_uom_qty']"
position="before"
>
<xpath
expr="//field[@name='order_line']/tree/field[@name='product_packaging_id']"
position="move"
/>
<xpath
expr="//field[@name='order_line']/tree/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>