Merge PR #254 into 12.0

Signed-off-by rousseldenis
This commit is contained in:
OCA-git-bot
2023-01-11 13:02:21 +00:00
18 changed files with 505 additions and 0 deletions

View File

@@ -0,0 +1 @@
../../../../stock_production_lot_traceability_shipment_report

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@@ -0,0 +1 @@
TO BE GENERATED

View File

@@ -0,0 +1,2 @@
from . import reports
from . import wizards

View File

@@ -0,0 +1,21 @@
# Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Stock Lot Shipment Traceability",
"summary": "Show the Shipments directly or indirectly involving a Lot/SN",
"version": "12.0.1.0.0",
"author": "Camptocamp, Odoo Community Association (OCA)",
"maintainers": ["ivantodorovich"],
"website": "https://github.com/OCA/stock-logistics-reporting",
"license": "AGPL-3",
"category": "Stock",
"depends": ["stock_production_lot_traceability"],
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"reports/stock_shipment_traceability_report.xml",
"wizards/stock_shipment_traceability_report_wizard.xml",
],
}

View File

@@ -0,0 +1,4 @@
* `Camptocamp <https://www.camptocamp.com>`_
* Damien Crier <damien.crier@camptocamp.com>
* Iván Todorovich <ivan.todorovich@camptocamp.com>

View File

@@ -0,0 +1,6 @@
Show all the shipments that directly or indirectly involved a Lot/Serial Number.
This report is useful to track the involved parties in a specific Lot/Serial Number.
It can be used to track customers that received a malfuctioning/contaminated product,
either directly (product sent), or indirectly (used in the components)

View File

@@ -0,0 +1,9 @@
#. Go to *Inventory > Analysis > Shipment Traceability Report*.
#. Select the *Lot/Serial Number* to be traced.
#. Confirm
You'll see a list of shipment lines involving the selected Lot/Serial Number, either
directly or indirectly.
This means, if this lot was used as a component for another product and it was delivered
instead, you'll find all the affected deliveries.

View File

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

View File

@@ -0,0 +1,202 @@
# Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from textwrap import indent, dedent
from odoo import models, fields, tools, api
class StockMoveLineDeliveryReport(models.Model):
_name = "stock.shipment.traceability.report"
_description = "Analysis on received/delivered stock lines"
_auto = False
_order = "date asc"
name = fields.Char(readonly=True)
date = fields.Datetime(readonly=True)
usage = fields.Selection(
selection=[("customer", "Customer"), ("supplier", "Supplier")],
readonly=True,
)
direction = fields.Selection([("in", "In"), ("out", "Out")], readonly=True)
kind = fields.Selection(
[
("reception", "Reception"),
("in_return", "Supplier Return"),
("delivery", "Delivery"),
("out_return", "Customer Return"),
],
readonly=True,
)
move_id = fields.Many2one("stock.move", "Move", readonly=True)
group_id = fields.Many2one("procurement.group", "Procurement Group", readonly=True)
product_id = fields.Many2one("product.product", "Product", readonly=True)
product_uom_id = fields.Many2one("uom.uom", "Unit of Measure", readonly=True)
product_uom_qty = fields.Float("Quantity", readonly=True)
product_uom_qty_directed = fields.Float(
"Quantity (directed)",
help="Quantity directed to the customer/supplier.",
readonly=True,
)
picking_id = fields.Many2one("stock.picking", "Picking", readonly=True)
owner_id = fields.Many2one("res.partner", "Owner", readonly=True)
location_id = fields.Many2one("stock.location", "From", readonly=True)
location_dest_id = fields.Many2one("stock.location", "To", readonly=True)
origin = fields.Char(readonly=True)
reference = fields.Char(readonly=True)
lot_id = fields.Many2one("stock.production.lot", "Lot/Serial Number", readonly=True)
company_id = fields.Many2one("res.company", "Company", readonly=True)
partner_id = fields.Many2one("res.partner", "Partner", readonly=True)
def _with_expressions(self):
return []
def _with(self):
expressions = self._with_expressions()
return ("WITH %s" % ", ".join(expressions)) if expressions else ""
def _select_fields(self):
return {
"id": "sml.id",
"name": "sm.name",
"date": "sml.date",
"usage": """
COALESCE(
NULLIF(sl_from.usage, 'internal'),
NULLIF(sl_dest.usage, 'internal')
)
""",
"direction": """
(
CASE
WHEN sl_from.usage = 'internal' THEN 'out'
ELSE 'in'
END
)
""",
"kind": """
(
CASE
WHEN sl_dest.usage = 'customer' THEN 'delivery'
WHEN sl_from.usage = 'supplier' THEN 'reception'
WHEN sl_from.usage = 'customer' THEN 'out_return'
WHEN sl_dest.usage = 'supplier' THEN 'in_return'
END
)
""",
"move_id": "sml.move_id",
"group_id": "sm.group_id",
"product_id": "sml.product_id",
"product_uom_id": "pt.uom_id",
"product_uom_qty": """
(
CASE
WHEN sl_from.usage = 'internal' THEN -sml.qty_done
ELSE sml.qty_done
END
)
""",
"product_uom_qty_directed": """
CASE
WHEN sl_from.usage = 'internal'
THEN -sml.qty_done
ELSE sml.qty_done
END
*
CASE
WHEN (
COALESCE(
NULLIF(sl_from.usage, 'internal'),
NULLIF(sl_dest.usage, 'internal')
)
= 'customer'
)
THEN -1
ELSE 1
END
""",
"picking_id": "sm.picking_id",
"owner_id": "sml.owner_id",
"location_id": "sml.location_id",
"location_dest_id": "sml.location_dest_id",
"origin": "sm.origin",
"reference": "sm.reference",
"lot_id": "sml.lot_id",
"company_id": "sm.company_id",
"partner_id": "sp.partner_id",
}
def _select_expressions(self):
return [
"%s AS %s" % (dedent(expr).strip(), fname)
for fname, expr in self._select_fields().items()
]
def _select(self):
indented_expressions = (
indent(expr, " ")
for expr in self._select_expressions()
)
return "SELECT\n%s" % ",\n".join(indented_expressions)
def _from(self):
return "FROM stock_move_line sml"
def _join_expressions(self):
return [
"INNER JOIN stock_move sm ON sm.id = sml.move_id",
"INNER JOIN stock_picking sp ON sp.id = sm.picking_id",
"INNER JOIN product_product pp ON pp.id = sml.product_id",
"INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id",
"INNER JOIN stock_location sl_from ON sl_from.id = sml.location_id",
"INNER JOIN stock_location sl_dest ON sl_dest.id = sml.location_dest_id",
]
def _join(self):
return "\n".join(self._join_expressions())
def _where_expressions(self):
return [
"sml.state = 'done'",
"""
(
(
sl_from.usage = 'internal'
AND sl_dest.usage IN ('customer', 'supplier')
) OR (
sl_from.usage IN ('customer', 'supplier')
AND sl_dest.usage = 'internal'
)
)
""",
]
def _where(self):
expressions = self._where_expressions()
return "WHERE %s" % "\nAND ".join(expressions) if expressions else ""
def _groupby_expressions(self):
return []
def _groupby(self):
expressions = self._groupby_expressions()
return "GROUP BY %s" % ", ".join(expressions) if expressions else ""
def _query(self):
return "\n".join(
(
self._with(),
self._select(),
self._from(),
self._join(),
self._where(),
self._groupby(),
)
)
@api.model_cr
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute(
"CREATE or REPLACE VIEW %s AS (%s)" % (self._table, self._query())
)

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
@author Iván Todorovich <ivan.todorovich@camptocamp.com>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_stock_shipment_traceability_report_pivot" model="ir.ui.view">
<field name="model">stock.shipment.traceability.report</field>
<field name="arch" type="xml">
<pivot>
<field name="partner_id" type="row" />
<field name="product_uom_qty" type="measure" />
</pivot>
</field>
</record>
<record id="view_stock_shipment_traceability_report_tree" model="ir.ui.view">
<field name="model">stock.shipment.traceability.report</field>
<field name="arch" type="xml">
<tree decoration-danger="kind in ('in_return', 'out_return')" decoration-success="kind == 'reception'">
<field name="kind"/>
<field name="date"/>
<field name="picking_id"/>
<field name="product_id"/>
<field name="lot_id"/>
<field name="product_uom_qty_directed" string="Quantity"/>
<field name="product_uom_id"/>
<field name="partner_id"/>
<field name="origin"/>
</tree>
</field>
</record>
<record id="view_stock_shipment_traceability_report_search" model="ir.ui.view">
<field name="model">stock.shipment.traceability.report</field>
<field name="arch" type="xml">
<search>
<field name="product_id"/>
<field name="partner_id"/>
<field name="date"/>
<field name="direction"/>
<group expand="1" string="Group By">
<filter string="Product" name="Product" context="{'group_by':'product_id'}"/>
<filter string="Partner" name="partner" context="{'group_by':'partner_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_stock_shipment_traceability_report" model="ir.actions.act_window">
<field name="name">Shipment Traceability Report</field>
<field name="res_model">stock.shipment.traceability.report</field>
<field name="view_type">form</field>
<field name="view_mode">tree,pivot</field>
<field name="target">main</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
access_stock_shipment_traceability_report,access stock.shipment.traceability.report,model_stock_shipment_traceability_report,base.group_user,1,0,0,0
access_stock_shipment_traceability_report_wizard,access stock.shipment.traceability.report.wizard,model_stock_shipment_traceability_report_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_shipment_traceability_report access stock.shipment.traceability.report model_stock_shipment_traceability_report base.group_user 1 0 0 0
3 access_stock_shipment_traceability_report_wizard access stock.shipment.traceability.report.wizard model_stock_shipment_traceability_report_wizard base.group_user 1 1 1 1

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
@author Iván Todorovich <ivan.todorovich@camptocamp.com>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="access_stock_shipment_traceability_report_rule_multi_company" model="ir.rule">
<field name="name">Analysis on received/delivered stock lines: Multi-company</field>
<field name="model_id" ref="model_stock_shipment_traceability_report"/>
<field name="domain_force">[('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</odoo>

View File

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

View File

@@ -0,0 +1,72 @@
# Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.tests import Form
from odoo.addons.stock_production_lot_traceability.tests.common import (
CommonStockLotTraceabilityCase,
)
class TestStockLotTraceability(CommonStockLotTraceabilityCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner1 = cls.env.ref("base.res_partner_1")
cls.partner2 = cls.env.ref("base.res_partner_2")
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
cls.location_customers = cls.env.ref("stock.stock_location_customers")
cls._do_picking_out(cls.partner1, cls.product3, cls.product3_lot1, 2)
cls._do_picking_out(cls.partner2, cls.product3, cls.product3_lot2, 2)
@classmethod
def _do_picking_out(cls, partner, product, lot, quantity, validate=True):
picking = cls.env["stock.picking"].create(
{
"partner_id": partner.id,
"picking_type_id": cls.picking_type_out.id,
"location_id": cls.location_stock.id,
"location_dest_id": cls.location_customers.id,
"move_type": "direct",
}
)
moves = cls._do_stock_move(
product,
lot,
quantity,
picking.location_id,
picking.location_dest_id,
validate=False,
)
moves.write({"picking_id": picking.id})
moves.move_line_ids.write({"picking_id": picking.id})
if validate:
picking.action_confirm()
picking.button_validate()
return picking
def _get_shipment_report_lines(self, lot):
Wizard = self.env["stock.shipment.traceability.report.wizard"]
wizard = Wizard.create({"lot_id": lot.id})
res = wizard.confirm()
self.assertIn("res_model", res)
self.assertIn("domain", res)
return self.env[res["res_model"]].search(res["domain"])
def test_report_wizard_form_onchange(self):
Wizard = self.env["stock.shipment.traceability.report.wizard"]
with Form(Wizard) as form:
form.lot_id = self.product1_lot1
self.assertEqual(form.product_id, self.product1)
def test_shipment_report(self):
# Case 1: Deliveries involving product1_lot1
lines = self._get_shipment_report_lines(self.product1_lot1)
self.assertEqual(lines.partner_id, self.partner1)
self.assertEqual(lines.product_id, self.product3)
self.assertEqual(lines.lot_id, self.product3_lot1)
# Case 1: Deliveries involving product1_lot1
lines = self._get_shipment_report_lines(self.product1_lot2)
self.assertEqual(lines.partner_id, self.partner2)
self.assertEqual(lines.product_id, self.product3)
self.assertEqual(lines.lot_id, self.product3_lot2)

View File

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

View File

@@ -0,0 +1,57 @@
# Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class StockTraceabilityDeliveryReportWizard(models.TransientModel):
_name = "stock.shipment.traceability.report.wizard"
_description = "Stock Traceability Delivery Report Wizard"
product_id = fields.Many2one(
"product.product",
string="Product",
domain="[('type', '=', 'product')]",
)
lot_id = fields.Many2one(
"stock.production.lot",
string="Lot/Serial Number",
domain="[('product_id', '=?', product_id)]",
required=True,
)
@api.onchange("lot_id")
def _onchange_lot_id(self):
if self.lot_id and self.lot_id.product_id != self.product_id:
self.product_id = self.lot_id.product_id
def _get_affected_lots(self):
"""Get all the lots affected by ``self.lot_id``"""
return self.lot_id | self.lot_id.produce_lot_ids
def _get_affected_move_lines(self):
lots = self._get_affected_lots()
return self.env["stock.move.line"].search(
[
("state", "=", "done"),
("lot_id", "in", lots.ids),
"|",
("location_id.usage", "=", "customer"),
("location_dest_id.usage", "=", "customer"),
]
)
def confirm(self):
self.ensure_one()
lines = self._get_affected_move_lines()
if not lines: # pragma: no cover
raise UserError(_("There isn't any shipment involving this lot."))
action = self.env["ir.actions.act_window"].for_xml_id(
"stock_production_lot_traceability_shipment_report",
"action_stock_shipment_traceability_report"
)
action["display_name"] = "%s (%s)" % (action["name"], self.lot_id.display_name)
action["domain"] = [("id", "in", lines.ids)]
return action

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
@author Iván Todorovich <ivan.todorovich@camptocamp.com>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_stock_shipment_traceability_report_wizard_form" model="ir.ui.view">
<field name="model">stock.shipment.traceability.report.wizard</field>
<field name="arch" type="xml">
<form>
<group name="main">
<group name="left">
<field name="product_id" options="{'no_create': True}" />
<field name="lot_id" options="{'no_create': True}" />
</group>
<group name="right">
</group>
</group>
<footer>
<button string="Confirm" name="confirm" type="object" class="btn-primary"/>
<button string="Cancel" special="cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<record id="action_stock_shipment_traceability_report_wizard" model="ir.actions.act_window">
<field name="name">Shipment Traceability Report</field>
<field name="res_model">stock.shipment.traceability.report.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem
id="menu_stock_shipment_traceability_report_wizard"
action="action_stock_shipment_traceability_report_wizard"
parent="stock.menu_warehouse_report"
/>
</odoo>