[IMP] stock_move_packaging_qty: report done packaging qtys

The title sounds like a simple task, but it requires many changes:

- Store done pkg qtys in `stock.move.line` records.
- The qtys are not computed. Instead, the user must write them by hand. This happens because many times the packaging just indicates an approximate content, but is not always exact until reaching this point of the process, where both the done qtys (both UoM and packagings) are measured and get real values.
- Added compute and inverse on `stock.move` for simple scenarios where one move = one move line.
- Add all this on all reports and views.

This change also opens the door to be able to get a real inventory measured in product packagings. That's not implemented in this MVP, but the idea is recorded.

@moduon MT-5531
This commit is contained in:
Jairo Llopis
2024-04-09 13:44:37 +01:00
parent 6a79163834
commit ebb9ab7450
14 changed files with 403 additions and 49 deletions

View File

@@ -7,7 +7,7 @@ Stock Packaging Qty
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ac3969cedf86da72ca20a93812053d6d9054a9f5d8e0c896eab57fe2b835eefc
!! source digest: sha256:5b4005788a852d7f71c44042e6c8839be5963fd5d6ac95822b93c48c660a2730
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -28,13 +28,22 @@ Stock Packaging Qty
|badge1| |badge2| |badge3| |badge4| |badge5|
Add packaging fields in the stock moves and their reports.
Add packaging fields in the stock moves, their lines and their reports.
**Table of contents**
.. contents::
:local:
Known issues / Roadmap
======================
* Maybe we should track also reserved packaging quantities?
* Since we store done product packaging quantities in the stock move lines, we
should be able to use this information in quants to provide real
packaging-based stock data.
Bug Tracker
===========

View File

@@ -10,7 +10,8 @@
"depends": ["stock"],
"data": [
"views/report_stock_picking.xml",
"views/stock_move_tree_view.xml",
"views/stock_move_line_view.xml",
"views/stock_move_view.xml",
"views/stock_picking_form_view.xml",
],
"license": "LGPL-3",

View File

@@ -1 +1,2 @@
from . import stock_move
from . import stock_move_line

View File

@@ -1,17 +1,24 @@
# Copyright 2020 Camptocamp SA
# Copyright 2021 ForgeFlow, S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, fields, models
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo import _, api, exceptions, fields, models
from odoo.tools import float_compare
class StockPicking(models.Model):
class StockMove(models.Model):
_inherit = "stock.move"
product_packaging_qty = fields.Float(
string="Pkg. Qty.",
string="Pkgs. Demand",
compute="_compute_product_packaging_qty",
inverse="_inverse_product_packaging_qty",
help="Amount of packages demanded.",
help="Amount of product packagings demanded.",
)
product_packaging_qty_done = fields.Float(
string="Pkgs. Done",
compute="_compute_product_packaging_qty_done",
inverse="_inverse_product_packaging_qty_done",
help="Amount of product packagings done.",
)
@api.depends(
@@ -31,6 +38,18 @@ class StockPicking(models.Model):
move.product_uom_qty / move._get_single_package_uom_qty()
)
@api.depends(
"move_line_ids.product_packaging_qty_done",
"move_line_nosuggest_ids.product_packaging_qty_done",
)
def _compute_product_packaging_qty_done(self):
"""Get the sum of done packaging qtys from move lines."""
for move in self:
lines = move._get_move_lines()
move.product_packaging_qty_done = sum(
lines.mapped("product_packaging_qty_done")
)
@api.onchange("product_packaging_qty")
def _inverse_product_packaging_qty(self):
"""Store the quantity in the product's UoM.
@@ -43,6 +62,23 @@ class StockPicking(models.Model):
uom_factor = move._get_single_package_uom_qty()
move.product_uom_qty = move.product_packaging_qty * uom_factor
def _inverse_product_packaging_qty_done(self):
"""Store the done packaging dqty in the move line if there's just one."""
for move in self:
lines = move._get_move_lines()
# Setting 0 done pkgs with no lines? Nothing to do
if not lines and not move.product_packaging_qty_done:
continue
if len(lines) != 1:
raise exceptions.UserError(
_(
"There are %d move lines involved. "
"Please set their product packaging done qty directly.",
len(lines),
)
)
lines.product_packaging_qty_done = move.product_packaging_qty_done
@api.onchange("product_packaging_id")
def _onchange_product_packaging(self):
"""Add a default qty if the packaging has an invalid value."""
@@ -62,3 +98,32 @@ class StockPicking(models.Model):
return self.product_packaging_id.product_uom_id._compute_quantity(
self.product_packaging_id.qty, self.product_uom
)
def _set_quantities_to_reservation(self):
"""Add packaging qtys when clicking on "Set Quantities"."""
result = super()._set_quantities_to_reservation()
digits = self.env["stock.move.line"].fields_get(["qty_done"], ["digits"])[
"qty_done"
]["digits"][1]
for line in self.move_line_ids:
if float_compare(line.qty_done, line.reserved_uom_qty, digits):
continue
if not line.product_packaging_id:
line.product_packaging_qty_done = 0
continue
line.product_packaging_qty_done = (
line.product_packaging_id._check_qty(
line.qty_done, line.product_uom_id, "DOWN"
)
/ line.product_packaging_id.qty
)
return result
def _clear_quantities_to_zero(self):
"""Clear packaging qtys when clicking on "Clear Quantities"."""
result = super()._clear_quantities_to_zero()
for line in self.move_line_ids:
if line.qty_done:
continue
line.product_packaging_qty_done = 0
return result

View File

@@ -0,0 +1,49 @@
# Copyright 2024 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from odoo import fields, models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
product_packaging_id = fields.Many2one(
related="move_id.product_packaging_id", readonly=True
)
product_packaging_qty_done = fields.Float(
string="Done Pkg. Qty.",
help="Product packaging quantity done.",
)
def _get_aggregated_properties(self, move_line=False, move=False):
"""Aggregate by product packaging too."""
result = super()._get_aggregated_properties(move_line, move)
pkg = result["move"].product_packaging_id
result["product_packaging"] = pkg
result["line_key"] += f"_{pkg.id}"
return result
def _get_aggregated_product_quantities(self, **kwargs):
"""Aggregate by product packaging too."""
result = super()._get_aggregated_product_quantities(**kwargs)
# Know all involved move lines, following upstream criteria
all_lines = self.browse()
processed_moves = all_lines.move_id
if kwargs.get("except_package"):
all_lines |= self - self.filtered("result_package_id")
if not kwargs.get("strict"):
moves = (self.picking_id | self.picking_id.backorder_ids).move_ids
all_lines |= moves.move_line_ids | moves.move_line_nosuggest_ids
# Aggregate product packaging quantities
for move_line in all_lines:
props = self._get_aggregated_properties(move_line)
try:
agg = result[props["line_key"]]
except KeyError:
continue # Missing aggregation; nothing to do
agg.setdefault("product_packaging_qty", 0.0)
agg.setdefault("product_packaging_qty_done", 0.0)
agg["product_packaging_qty_done"] += move_line.product_packaging_qty_done
if move_line.move_id not in processed_moves:
agg["product_packaging_qty"] += move_line.move_id.product_packaging_qty
processed_moves |= move_line.move_id
return result

View File

@@ -1 +1 @@
Add packaging fields in the stock moves and their reports.
Add packaging fields in the stock moves, their lines and their reports.

View File

@@ -0,0 +1,5 @@
* Maybe we should track also reserved packaging quantities?
* Since we store done product packaging quantities in the stock move lines, we
should be able to use this information in quants to provide real
packaging-based stock data.

View File

@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!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>
@@ -366,24 +367,34 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:ac3969cedf86da72ca20a93812053d6d9054a9f5d8e0c896eab57fe2b835eefc
!! source digest: sha256:5b4005788a852d7f71c44042e6c8839be5963fd5d6ac95822b93c48c660a2730
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.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/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/stock-logistics-warehouse/tree/16.0/stock_move_packaging_qty"><img alt="OCA/stock-logistics-warehouse" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/stock-logistics-warehouse-16-0/stock-logistics-warehouse-16-0-stock_move_packaging_qty"><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/stock-logistics-warehouse&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>Add packaging fields in the stock moves and their reports.</p>
<p>Add packaging fields in the stock moves, their lines and their reports.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-1">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-1">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Maybe we should track also reserved packaging quantities?</li>
<li>Since we store done product packaging quantities in the stock move lines, we
should be able to use this information in quants to provide real
packaging-based stock data.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h1>
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-warehouse/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
@@ -391,15 +402,15 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-2">Credits</a></h1>
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
<ul class="simple">
<li>ForgeFlow</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
<ul class="simple">
<li>Mateu Griful &lt;<a class="reference external" href="mailto:mateu.griful&#64;forgeflow.com">mateu.griful&#64;forgeflow.com</a>&gt;</li>
<li>Lois Rilo &lt;<a class="reference external" href="mailto:lois.rilo&#64;forgeflow.com">lois.rilo&#64;forgeflow.com</a>&gt;</li>
@@ -407,7 +418,7 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
<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

View File

@@ -12,7 +12,14 @@ class TestStockMovePackagingQty(TransactionCase):
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.env.user.groups_id |= cls.env.ref("product.group_stock_packaging")
cls.partner = cls.env.ref("base.res_partner_12")
cls.product = cls.env.ref("product.product_product_9")
cls.product = cls.env["product.product"].create(
{
"name": "Test product",
"type": "consu",
"uom_id": cls.env.ref("uom.product_uom_unit").id,
"uom_po_id": cls.env.ref("uom.product_uom_unit").id,
}
)
cls.packaging = cls.env["product.packaging"].create(
{"name": "Test packaging", "product_id": cls.product.id, "qty": 5.0}
)
@@ -59,3 +66,20 @@ class TestStockMovePackagingQty(TransactionCase):
move_f.product_packaging_id = self.packaging
self.assertEqual(move_f.product_uom_qty, 5)
self.assertEqual(move_f.product_packaging_qty, 1)
picking = picking_f.save()
self.assertEqual(picking.state, "draft")
picking.action_assign()
picking.action_set_quantities_to_reservation()
self.assertRecordValues(
picking.move_ids_without_package,
[
{
"product_id": self.product.id,
"product_packaging_id": self.packaging.id,
"product_packaging_qty_done": 1,
"product_packaging_qty": 1,
"product_uom_qty": 5,
}
],
)
picking.button_validate()

View File

@@ -40,5 +40,72 @@
<span t-field="move.product_packaging_qty" />
</div>
</xpath>
<xpath
expr="//table[@name='stock_move_table']/tbody//td[
1 + count(
//table[@name='stock_move_table']
//th[@name='th_sm_quantity']
/preceding-sibling::*
)
]"
position="inside"
>
<div
t-if="move.product_packaging_id"
class="text-secondary"
groups="product.group_stock_packaging"
>
<span t-field="move.product_packaging_id" />:
<span t-field="move.product_packaging_qty_done" />
</div>
</xpath>
</template>
<template
id="stock_report_delivery_aggregated_move_lines"
inherit_id="stock.stock_report_delivery_aggregated_move_lines"
>
<xpath expr="//td[@name='move_line_aggregated_qty_ordered']">
<div
t-if="aggregated_lines[line]['product_packaging']"
class="text-secondary"
groups="product.group_stock_packaging"
>
<span
t-esc="aggregated_lines[line]['product_packaging'].display_name"
/>:
<span t-esc="aggregated_lines[line]['product_packaging_qty']" />
</div>
</xpath>
<xpath expr="//td[@name='move_line_aggregated_qty_done']">
<div
t-if="aggregated_lines[line]['product_packaging']"
class="text-secondary"
groups="product.group_stock_packaging"
>
<span
t-esc="aggregated_lines[line]['product_packaging'].display_name"
/>:
<span t-esc="aggregated_lines[line]['product_packaging_qty_done']" />
</div>
</xpath>
</template>
<template
id="stock_report_delivery_has_serial_move_line"
inherit_id="stock.stock_report_delivery_has_serial_move_line"
>
<xpath expr="//td[@name='move_line_lot_qty_done']">
<div
t-if="ml.product_packaging_id"
class="text-secondary"
groups="product.group_stock_packaging"
>
<span t-field="ml.product_packaging_id" />:
<span t-field="ml.product_packaging_qty_done" />
</div>
</xpath>
</template>
</data>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Moduon Team S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<data>
<record id="view_stock_move_line_operation_tree" model="ir.ui.view">
<field name="name">Add product packaging qty done info</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_stock_move_line_operation_tree" />
<field name="arch" type="xml">
<field name="qty_done" position="before">
<field name="product_packaging_id" invisible="1" />
<field
name="product_packaging_qty_done"
attrs="{'invisible': [('product_packaging_id', '=', False)]}"
groups="product.group_stock_packaging"
/>
</field>
</field>
</record>
<record id="view_stock_move_line_mobile_form_extension" model="ir.ui.view">
<field name="name">Add product packaging qty done info</field>
<field name="model">stock.move.line</field>
<field name="inherit_id" ref="stock.view_move_line_mobile_form" />
<field name="arch" type="xml">
<xpath expr="//label[@for='qty_done']" position="before">
<label for="product_packaging_qty_done" />
<div class="o_row">
<field
name="product_packaging_qty_done"
attrs="{'invisible': [('product_packaging_id', '=', False)]}"
groups="product.group_stock_packaging"
/>
<field
name="product_packaging_id"
groups="product.group_stock_packaging"
/>
</div>
</xpath>
</field>
</record>
<record id="view_stock_move_line_detailed_operation_tree" model="ir.ui.view">
<field name="name">Add product packaging qty done info</field>
<field name="model">stock.move.line</field>
<field
name="inherit_id"
ref="stock.view_stock_move_line_detailed_operation_tree"
/>
<field name="arch" type="xml">
<field name="qty_done" position="before">
<field
name="product_packaging_qty_done"
attrs="{'invisible': [('product_packaging_id', '=', False)]}"
groups="product.group_stock_packaging"
/>
<field
name="product_packaging_id"
groups="product.group_stock_packaging"
/>
</field>
</field>
</record>
</data>

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<data>
<record id="view_move_extra_tree" model="ir.ui.view">
<field name="name">view.move.extra.tree</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_tree" />
<field name="arch" type="xml">
<field name="product_packaging_id" position="after">
<field name="is_initial_demand_editable" invisible="1" />
<field
name="product_packaging_qty"
groups="product.group_stock_packaging"
attrs="{
'readonly': [
'|',
('product_packaging_id', '=', False),
('is_initial_demand_editable', '=', False),
],
}"
/>
</field>
</field>
</record>
</data>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Moduon Team S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<data>
<record id="view_move_extra_tree" model="ir.ui.view">
<field name="name">view.move.extra.tree</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_tree" />
<field name="arch" type="xml">
<field name="product_packaging_id" position="after">
<field name="is_initial_demand_editable" invisible="1" />
<field
name="product_packaging_qty"
groups="product.group_stock_packaging"
attrs="{
'readonly': [
'|',
('product_packaging_id', '=', False),
('is_initial_demand_editable', '=', False),
],
}"
/>
</field>
</field>
</record>
<record id="view_stock_move_operations" model="ir.ui.view">
<field name="name">Add product packaging qty info</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_stock_move_operations" />
<field name="arch" type="xml">
<xpath expr="//group" position="inside">
<group
groups="product.group_stock_packaging"
name="packaging"
attrs="{'invisible': [('product_packaging_id', '=', False)]}"
>
<label for="product_packaging_qty" string="Packagings demanded" />
<div class="o_row">
<span><field
name="product_packaging_qty"
readonly="1"
nolabel="1"
/></span>
<span><field
name="product_packaging_id"
readonly="1"
nolabel="1"
/></span>
</div>
<label for="product_packaging_qty_done" string="Packagings done" />
<div class="o_row">
<span><field
name="product_packaging_qty_done"
readonly="1"
nolabel="1"
/></span>
<span> / </span>
<span><field
name="product_packaging_qty"
readonly="1"
nolabel="1"
/></span>
<span><field
name="product_packaging_id"
readonly="1"
nolabel="1"
/></span>
</div>
</group>
</xpath>
</field>
</record>
</data>

View File

@@ -13,9 +13,6 @@
name="product_packaging_qty"
groups="product.group_stock_packaging"
attrs="{
'column_invisible': [
('parent.immediate_transfer', '=', True),
],
'readonly': [
'|',
('product_packaging_id', '=', False),
@@ -24,6 +21,17 @@
}"
/>
</xpath>
<xpath
expr="//field[@name='move_ids_without_package']/tree/field[@name='quantity_done']"
position="before"
>
<field
name="product_packaging_qty_done"
groups="product.group_stock_packaging"
attrs="{'invisible': [('product_packaging_id', '=', False)]}"
/>
</xpath>
</field>
</record>
</data>