summaryrefslogtreecommitdiff
path: root/addons/stock/report
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/report
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/stock/report')
-rw-r--r--addons/stock/report/__init__.py7
-rw-r--r--addons/stock/report/package_templates.xml18
-rw-r--r--addons/stock/report/picking_templates.xml115
-rw-r--r--addons/stock/report/product_packaging.xml24
-rw-r--r--addons/stock/report/product_templates.xml98
-rw-r--r--addons/stock/report/report_deliveryslip.xml234
-rw-r--r--addons/stock/report/report_location_barcode.xml41
-rw-r--r--addons/stock/report/report_lot_barcode.xml38
-rw-r--r--addons/stock/report/report_package_barcode.xml82
-rw-r--r--addons/stock/report/report_stock_forecasted.py247
-rw-r--r--addons/stock/report/report_stock_forecasted.xml179
-rw-r--r--addons/stock/report/report_stock_quantity.py138
-rw-r--r--addons/stock/report/report_stock_quantity.xml58
-rw-r--r--addons/stock/report/report_stock_rule.py139
-rw-r--r--addons/stock/report/report_stock_rule.xml189
-rw-r--r--addons/stock/report/report_stockinventory.xml56
-rw-r--r--addons/stock/report/report_stockpicking_operations.xml173
-rw-r--r--addons/stock/report/stock_report_views.xml180
-rw-r--r--addons/stock/report/stock_traceability.py243
19 files changed, 2259 insertions, 0 deletions
diff --git a/addons/stock/report/__init__.py b/addons/stock/report/__init__.py
new file mode 100644
index 00000000..9d26ba48
--- /dev/null
+++ b/addons/stock/report/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import report_stock_forecasted
+from . import report_stock_quantity
+from . import report_stock_rule
+from . import stock_traceability
diff --git a/addons/stock/report/package_templates.xml b/addons/stock/report/package_templates.xml
new file mode 100644
index 00000000..b7f924a5
--- /dev/null
+++ b/addons/stock/report/package_templates.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <template id="label_package_template_view">
+ <t t-foreach="docs" t-as="package">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="package.name"/>^FS
+^FO100,100^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="package.name"/>^FS
+^XZ
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/stock/report/picking_templates.xml b/addons/stock/report/picking_templates.xml
new file mode 100644
index 00000000..7b41cd08
--- /dev/null
+++ b/addons/stock/report/picking_templates.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <template id="label_transfer_template_view_zpl">
+ <t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
+ <t t-foreach="docs" t-as="picking">
+
+ <t t-foreach="picking.move_lines" t-as="move">
+ <t t-foreach="move.move_line_ids" t-as="move_line">
+ <t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
+ <t t-set="qty" t-value="int(move_line.qty_done)"/>
+ </t>
+ <t t-else="">
+ <t t-set="qty" t-value="1"/>
+ </t>
+ <t t-foreach="range(qty)" t-as="item">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="move_line.product_id.display_name"/>^FS
+^FO100,100
+<t t-if="move_line.product_id.tracking != 'none' and (move_line.lot_id or move_line.lot_name)">
+^A0N,44,33^FDLN/SN: <t t-esc="move_line.lot_id.name or move_line.lot_name"/>^FS
+^FO100,150^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="move_line.lot_id.name or move_line.lot_name"/>^FS
+</t>
+<t t-if="move_line.product_id.tracking == 'none' and move_line.product_id.barcode">
+^BCN,100,Y,N,N
+^FD<t t-esc="move_line.product_id.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </t>
+ </t>
+ </t>
+ </template>
+
+ <template id="label_transfer_template_view_pdf">
+ <t t-call="web.basic_layout">
+ <div class="page">
+ <t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
+ <t t-foreach="docs" t-as="picking">
+ <t t-foreach="picking.move_lines" t-as="move">
+ <t t-foreach="move.move_line_ids" t-as="move_line">
+ <t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
+ <t t-set="qty" t-value="int(move_line.qty_done)"/>
+ </t>
+ <t t-else="">
+ <t t-set="qty" t-value="1"/>
+ </t>
+ <t t-foreach="range(qty)" t-as="item">
+ <t t-translation="off">
+ <div style="display: inline-table; height: 10rem; width: 32%;">
+ <table class="table table-bordered" style="border: 2px solid black;" t-if="picking.move_lines">
+ <tr>
+ <th class="table-active text-left" style="height:4rem;">
+ <span t-esc="move.product_id.display_name"/>
+ </th>
+ </tr>
+ <t t-if="move_line.product_id.tracking != 'none'">
+ <tr>
+ <td class="text-center align-middle">
+ <t t-if="move_line.lot_name or move_line.lot_id">
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', move_line.lot_name, 600, 150)" style="width:100%;height:4rem" alt="Barcode"/>
+ <span t-esc="move_line.lot_name or move_line.lot_id.name"/>
+ </t>
+ <t t-else="">
+ <span class="text-muted">No barcode available</span>
+ </t>
+ </td>
+ </tr>
+ </t>
+ <t t-if="move_line.product_id.tracking == 'none'">
+ <tr>
+ <td class="text-center align-middle" style="height: 6rem;">
+ <t t-if="move_line.product_id.barcode">
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', move_line.product_id.barcode, 600, 150)" style="width:100%;height:4rem" alt="Barcode"/>
+ <span t-esc="move_line.product_id.barcode"/>
+ </t>
+ <t t-else="">
+ <span class="text-muted">No barcode available</span>
+ </t>
+ </td>
+ </tr>
+ </t>
+ </table>
+ </div>
+ </t>
+ </t>
+ </t>
+ </t>
+ </t>
+ </div>
+ </t>
+ </template>
+
+ <template id="label_picking_type_view">
+ <t t-foreach="docs" t-as="operation">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="operation.name"/>^FS
+<t t-if="operation.barcode">
+^FO100,100
+^BCN,100,Y,N,N
+^FD<t t-esc="operation.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/stock/report/product_packaging.xml b/addons/stock/report/product_packaging.xml
new file mode 100644
index 00000000..984dbd04
--- /dev/null
+++ b/addons/stock/report/product_packaging.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <template id="label_product_packaging_view">
+ <t t-foreach="docs" t-as="packaging">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="packaging.name"/>^FS
+^FO100,100
+^A0N,44,33^FD<t t-esc="packaging.product_id.display_name"/>^FS
+^FO100,150
+^A0N,44,33^FDQty: <t t-esc="packaging.qty"/> <t t-esc="packaging.product_uom_id.name" groups="uom.group_uom"/>^FS
+<t t-if="packaging.barcode">
+^FO100,200^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="packaging.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/stock/report/product_templates.xml b/addons/stock/report/product_templates.xml
new file mode 100644
index 00000000..20b0faba
--- /dev/null
+++ b/addons/stock/report/product_templates.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <template id="label_product_template_view">
+ <t t-foreach="docs" t-as="product">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="product.display_name"/>^FS
+^FO100,100
+<t t-if="product.currency_id.position == 'after'">
+^CI28
+^A0N,44,33^FH^FDPrice: <t t-esc="product.list_price" t-options='{"widget": "float", "precision": 2}'/><t t-esc="product.currency_id.symbol"/>^FS
+</t>
+<t t-if="product.currency_id.position == 'before'">
+^CI28
+^A0N,44,33^FH^FDPrice: <t t-esc="product.currency_id.symbol"/><t t-esc="product.list_price" t-options='{"widget": "float", "precision": 2}'/>^FS
+</t>
+<t t-if="product.barcode">
+^FO100,150^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="product.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </template>
+ <template id="label_product_product_view">
+ <t t-foreach="docs" t-as="product">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="product.display_name"/>^FS
+^FO100,100
+<t t-if="product.currency_id.position == 'after'">
+^CI28
+^A0N,44,33^FH^FDPrice: <t t-esc="product.lst_price" t-options='{"widget": "float", "precision": 2}'/><t t-esc="product.currency_id.symbol"/>^FS
+</t>
+<t t-if="product.currency_id.position == 'before'">
+^CI28
+^A0N,44,33^FH^FDPrice: <t t-esc="product.currency_id.symbol"/><t t-esc="product.lst_price" t-options='{"widget": "float", "precision": 2}'/>^FS
+</t>
+<t t-if="product.barcode">
+^FO100,150^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="product.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </template>
+ <template id="label_barcode_product_template_view">
+ <t t-foreach="docs" t-as="product">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="product.display_name"/>^FS
+<t t-if="product.barcode ">
+^FO100,100^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="product.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </template>
+ <template id="label_barcode_product_product_view">
+ <t t-foreach="docs" t-as="product">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="product.display_name"/>^FS
+<t t-if="product.barcode">
+^FO100,100^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="product.barcode"/>^FS
+</t>
+^XZ
+ </t>
+ </t>
+ </template>
+ <template id="label_lot_template_view">
+ <t t-foreach="docs" t-as="lot">
+ <t t-translation="off">
+^XA
+^FO100,50
+^A0N,44,33^FD<t t-esc="lot.product_id.display_name"/>^FS
+^FO100,100
+^A0N,44,33^FDLN/SN: <t t-esc="lot.name"/>^FS
+^FO100,150^BY3
+^BCN,100,Y,N,N
+^FD<t t-esc="lot.name"/>^FS
+^XZ
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml
new file mode 100644
index 00000000..24bd5e23
--- /dev/null
+++ b/addons/stock/report/report_deliveryslip.xml
@@ -0,0 +1,234 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <template id="report_delivery_document">
+ <t t-call="web.html_container">
+ <t t-call="web.external_layout">
+ <t t-set="o" t-value="o.with_context(lang=o.partner_id.lang)" />
+ <t t-set="partner" t-value="o.partner_id or (o.move_lines and o.move_lines[0].partner_id) or False"/>
+ <t t-if="partner" name="partner_header">
+ <t t-set="address">
+ <div t-esc="partner"
+ t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/>
+ </t>
+ </t>
+
+ <div class="page">
+ <h2>
+ <span t-field="o.name"/>
+ </h2>
+ <div class="row mt32 mb32">
+ <div t-if="o.origin" class="col-auto" name="div_origin">
+ <strong>Order:</strong>
+ <p t-field="o.origin"/>
+ </div>
+ <div t-if="o.state" class="col-auto" name="div_sched_date">
+ <strong>Shipping Date:</strong>
+ <t t-if="o.state == 'done'">
+ <p t-field="o.date_done"/>
+ </t>
+ <t t-if="o.state != 'done'">
+ <p t-field="o.scheduled_date"/>
+ </t>
+ </div>
+ </div>
+ <table class="table table-sm" t-if="o.state!='done'" name="stock_move_table">
+ <thead>
+ <tr>
+ <th name="th_sm_product"><strong>Product</strong></th>
+ <th name="th_sm_quantity"><strong>Quantity</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-set="lines" t-value="o.move_lines.filtered(lambda x: x.product_uom_qty)"/>
+ <tr t-foreach="lines" t-as="move">
+ <td>
+ <span t-field="move.product_id"/>
+ <p t-if="move.description_picking != move.product_id.name">
+ <span t-field="move.description_picking"/>
+ </p>
+ </td>
+ <td>
+ <span t-field="move.product_uom_qty"/>
+ <span t-field="move.product_uom"/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <table class="table table-sm mt48" t-if="o.move_line_ids and o.state=='done'" name="stock_move_line_table">
+ <t t-set="has_serial_number" t-value="False"/>
+ <t t-set="has_serial_number" t-value="o.move_line_ids.mapped('lot_id')" groups="stock.group_lot_on_delivery_slip"/>
+ <thead>
+ <tr>
+ <th name="th_sml_product"><strong>Product</strong></th>
+ <t name="lot_serial" t-if="has_serial_number">
+ <th>
+ Lot/Serial Number
+ </th>
+ </t>
+ <th name="th_sml_quantity" class="text-center"><strong>Quantity</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ <!-- This part gets complicated with different use cases (additional use cases in extensions of this report):
+ 1. If serial numbers are used and set to print on delivery slip => print lines as is, otherwise group them by overlapping
+ product + description + uom combinations
+ 2. If any packages are assigned => split products up by package (or non-package) and then apply use case 1 -->
+ <!-- If has destination packages => create sections of corresponding products -->
+ <t t-if="o.has_packages" name="has_packages">
+ <t t-set="packages" t-value="o.move_line_ids.mapped('result_package_id')"/>
+ <t t-foreach="packages" t-as="package">
+ <t t-call="stock.stock_report_delivery_package_section_line"/>
+ <t t-set="package_move_lines" t-value="o.move_line_ids.filtered(lambda l: l.result_package_id == package)"/>
+ <!-- If printing lots/serial numbers => keep products in original lines -->
+ <t t-if="has_serial_number">
+ <tr t-foreach="package_move_lines" t-as="move_line">
+ <t t-call="stock.stock_report_delivery_has_serial_move_line"/>
+ </tr>
+ </t>
+ <!-- If not printing lots/serial numbers => merge lines with same product+description+uom -->
+ <t t-else="">
+ <t t-set="aggregated_lines" t-value="package_move_lines._get_aggregated_product_quantities()"/>
+ <t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
+ </t>
+ </t>
+ <!-- Make sure we do another section for package-less products if they exist -->
+ <t t-set="move_lines" t-value="o.move_line_ids.filtered(lambda l: not l.result_package_id)"/>
+ <t t-if="move_lines" name="no_package_move_lines">
+ <t t-call="stock.stock_report_delivery_no_package_section_line" name="no_package_section"/>
+ <t t-if="has_serial_number">
+ <tr t-foreach="move_lines" t-as="move_line">
+ <t t-call="stock.stock_report_delivery_has_serial_move_line"/>
+ </tr>
+ </t>
+ <t t-else="">
+ <t t-set="aggregated_lines" t-value="move_lines._get_aggregated_product_quantities()"/>
+ <t t-if="aggregated_lines">
+ <t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
+ </t>
+ </t>
+ </t>
+ </t>
+ <!-- No destination packages -->
+ <t t-else="">
+ <!-- If printing lots/serial numbers => keep products in original lines -->
+ <t t-if="has_serial_number">
+ <tr t-foreach="o.move_line_ids" t-as="move_line">
+ <t t-call="stock.stock_report_delivery_has_serial_move_line"/>
+ </tr>
+ </t>
+ <!-- If not printing lots/serial numbers => merge lines with same product -->
+ <t t-else="" name="aggregated_move_lines">
+ <t t-set="aggregated_lines" t-value="o.move_line_ids._get_aggregated_product_quantities()"/>
+ <t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
+ </t>
+ </t>
+ </tbody>
+ </table>
+ <t t-set="backorders" t-value="o.backorder_ids.filtered(lambda x: x.state not in ('done', 'cancel'))"/>
+ <t t-if="o.backorder_ids and backorders">
+ <p>
+ <span>All items couldn't be shipped, the following items will be shipped as soon as they become available.</span>
+ </p>
+ <table class="table table-sm" name="stock_backorder_table">
+ <thead>
+ <tr>
+ <th name="th_sb_product"><strong>Product</strong></th>
+ <th name="th_sb_quantity" class="text-center"><strong>Quantity</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-foreach="backorders" t-as="backorder">
+ <t t-set="bo_lines" t-value="backorder.move_lines.filtered(lambda x: x.product_uom_qty)"/>
+ <tr t-foreach="bo_lines" t-as="bo_line">
+ <td>
+ <span t-field="bo_line.product_id"/>
+ <p t-if="bo_line.description_picking != bo_line.product_id.name">
+ <span t-field="bo_line.description_picking"/>
+ </p>
+ </td>
+ <td class="text-center">
+ <span t-field="bo_line.product_uom_qty"/>
+ <span t-field="bo_line.product_uom"/>
+ </td>
+ </tr>
+ </t>
+ </tbody>
+ </table>
+ </t>
+
+ <div t-if="o.signature" class="mt32 ml64 mr4" name="signature">
+ <div class="offset-8">
+ <strong>Signature</strong>
+ </div>
+ <div class="offset-8">
+ <img t-att-src="image_data_uri(o.signature)" style="max-height: 4cm; max-width: 8cm;"/>
+ </div>
+ <div class="offset-8 text-center">
+ <p t-field="o.partner_id.name"/>
+ </div>
+ </div>
+ </div>
+ </t>
+ </t>
+ </template>
+
+ <!-- templates for easier extension + cut back on repeat code due to multiple conditionals -->
+ <!-- move line(s) printing for tables -->
+ <template id="stock_report_delivery_has_serial_move_line">
+ <td>
+ <span t-field="move_line.product_id"/>
+ <!-- this is an annoying workaround for the multiple types of descriptions (often auto-filled) that we do not want to print -->
+ <!-- this makes it so we can pre-filter the descriptions in inherited templates since we cannot extend the standard "if" condition -->
+ <!-- let's agree that pre-filtered descriptions will be set to "" -->
+ <t t-if="not description and description != ''">
+ <t t-set="description" t-value="move_line.move_id.description_picking"/>
+ </t>
+ <p t-if="description !='' and description != move_line.product_id.name">
+ <span t-esc="description"/>
+ </p>
+ </td>
+ <t t-if="has_serial_number" name="move_line_lot">
+ <td><span t-field="move_line.lot_id.name"/></td>
+ </t>
+ <td class="text-center" name="move_line_lot_qty_done">
+ <span t-field="move_line.qty_done"/>
+ <span t-field="move_line.product_uom_id"/>
+ </td>
+ </template>
+ <template id="stock_report_delivery_aggregated_move_lines">
+ <tr t-foreach="aggregated_lines" t-as="line">
+ <td>
+ <span t-esc="aggregated_lines[line]['name']"/>
+ <p t-if="aggregated_lines[line]['description']">
+ <span t-esc="aggregated_lines[line]['description']"/>
+ </p>
+ </td>
+ <td class="text-center" name="move_line_aggregated_qty_done">
+ <span t-esc="aggregated_lines[line]['qty_done']"/>
+ <span t-esc="aggregated_lines[line]['product_uom']"/>
+ </td>
+ </tr>
+ </template>
+
+ <!-- package related "section lines" -->
+ <template id="stock_report_delivery_package_section_line">
+ <tr t-att-class="'bg-200 font-weight-bold o_line_section'">
+ <td colspan="99" name="package_info">
+ <span t-field="package.name"/>
+ </td>
+ </tr>
+ </template>
+ <template id="stock_report_delivery_no_package_section_line">
+ <tr t-att-class="'bg-200 font-weight-bold o_line_section'">
+ <td colspan="99" name="no_package_info">
+ <span>Products with no package assigned</span>
+ </td>
+ </tr>
+ </template>
+
+ <template id="report_deliveryslip">
+ <t t-foreach="docs" t-as="o">
+ <t t-call="stock.report_delivery_document" t-lang="o.partner_id.lang"/>
+ </t>
+ </template>
+</odoo>
diff --git a/addons/stock/report/report_location_barcode.xml b/addons/stock/report/report_location_barcode.xml
new file mode 100644
index 00000000..36493b05
--- /dev/null
+++ b/addons/stock/report/report_location_barcode.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+
+<template id="report_generic_barcode">
+ <t t-call="web.html_container">
+ <t t-set='nRows' t-value='8'/>
+ <t t-set='nCols' t-value='3'/>
+ <div t-foreach="[docs[x:x + nRows * nCols] for x in range(0, len(docs), nRows * nCols)]" t-as="page_docs" class="page article">
+ <t t-if="title">
+ <h2 style="text-align: center; font-size: 3em"><t t-esc="title"/></h2>
+ </t>
+ <table>
+ <t t-foreach="range(nRows)" t-as="row">
+ <tr>
+ <t t-foreach="range(nCols)" t-as="col">
+ <t t-set="barcode_index" t-value="(row * nCols + col)"/>
+ <t t-if="barcode_index &lt; len(page_docs)">
+ <t t-set="o" t-value="page_docs[barcode_index]"/>
+ </t>
+ <t t-else="">
+ <t t-set="o" t-value="page_docs[0]"/>
+ </t>
+ <td t-att-style="barcode_index &gt;= len(page_docs) and 'visibility:hidden'">
+ <div style="text-align: center; font-size: 2em"><span t-esc="o.name"/></div>
+ <img t-if="o.barcode" class="barcode" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;humanreadable=1' % ('Code128', o.barcode, 400, 100)" alt="Barcode"/>
+ </td>
+ </t>
+ </tr>
+ </t>
+ </table>
+ </div>
+ </t>
+</template>
+
+<template id="report_location_barcode">
+ <t t-set="title">Locations</t>
+ <t t-call="stock.report_generic_barcode"/>
+</template>
+</data>
+</odoo>
diff --git a/addons/stock/report/report_lot_barcode.xml b/addons/stock/report/report_lot_barcode.xml
new file mode 100644
index 00000000..722c11ab
--- /dev/null
+++ b/addons/stock/report/report_lot_barcode.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+<template id="report_lot_label">
+ <t t-call="web.basic_layout">
+ <t t-foreach="docs" t-as="o">
+ <t>
+ <div class="page">
+ <div class="oe_structure"/>
+ <div class="row">
+ <div class="col-8">
+ <table class="table table-condensed" style="border-bottom: 0px solid white !important;width: 3in;">
+ <tr>
+ <th style="text-align: left;">
+ <span t-field="o.product_id.display_name"/>
+ </th>
+ </tr>
+ <tr name="lot_name">
+ <td>
+ <span>LN/SN:</span>
+ <span t-field="o.name"/>
+ </td>
+ </tr>
+ <tr>
+ <td style="text-align: center; vertical-align: middle;" class="col-5">
+ <img t-if="o.name" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', o.name, 600, 150)" style="width:100%;height:20%;"/>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </div>
+ </t>
+ </t>
+ </t>
+</template>
+</data>
+</odoo>
diff --git a/addons/stock/report/report_package_barcode.xml b/addons/stock/report/report_package_barcode.xml
new file mode 100644
index 00000000..6e0f9f55
--- /dev/null
+++ b/addons/stock/report/report_package_barcode.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data>
+<template id="report_package_barcode">
+ <t t-call="web.basic_layout">
+ <t t-foreach="docs" t-as="o">
+ <t>
+ <div class="page">
+ <div class="oe_structure"/>
+ <table class="table table-condensed" style="border-bottom: 0px solid white !important;">
+ <tr>
+ <th>
+ <h1 t-field="o.name" class="mt0 float-left"/>
+ </th>
+ <th name="td_pk_barcode" style="text-align: center">
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', o.name, 600, 100)" alt="Barcode"
+ style="width:300px;height:50px"/>
+ <p t-field="o.name"/>
+ </th>
+ </tr>
+ </table>
+ <div class="row mt32 mb32">
+ <div t-if="o.packaging_id" class="o_packaging_type col-auto">
+ <strong>Package Type:</strong>
+ <p t-field="o.packaging_id.name"/>
+ </div>
+ </div>
+ <table class="table table-sm" style="border-bottom: 0px solid white !important;">
+ <t t-set="has_serial_number" t-value="o.quant_ids.mapped('lot_id')" />
+ <thead>
+ <tr>
+ <th>Product</th>
+ <th name="th_quantity" class="text-right">Quantity</th>
+ <th name="th_uom" groups="uom.group_uom"/>
+ <th name="th_serial" class="text-right" t-if="has_serial_number">Lot/Serial Number</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="o.quant_ids" t-as="l">
+ <td>
+ <span t-field="l.product_id.name"/>
+ </td>
+ <td class="text-right">
+ <span t-field="l.quantity"/>
+ </td>
+ <td groups="uom.group_uom">
+ <span t-field="l.product_id.uom_id.name"/>
+ </td>
+ <td class="text-right" t-if="has_serial_number">
+ <t t-if="l.lot_id"><span t-field="l.lot_id.name"/></t>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </t>
+ </t>
+ </t>
+</template>
+
+<template id="report_package_barcode_small">
+ <t t-call="web.basic_layout">
+ <t t-foreach="docs" t-as="o">
+ <t>
+ <div class="page">
+ <div class="oe_structure"/>
+ <div class="row">
+ <div class="col-12 text-center">
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', o.name, 600, 100)" style="width:600px;height:100px" alt="Barcode"/>
+ <p t-field="o.name" style="font-size:20px;"></p>
+ </div>
+ </div>
+ <div class="row o_packaging_type" t-if="o.packaging_id">
+ <div class="col-12 text-center" style="font-size:24px; font-weight:bold;"><span>Package Type: </span><span t-field="o.packaging_id.name"/></div>
+ </div>
+ </div>
+ </t>
+ </t>
+ </t>
+</template>
+</data>
+</odoo>
diff --git a/addons/stock/report/report_stock_forecasted.py b/addons/stock/report/report_stock_forecasted.py
new file mode 100644
index 00000000..80844230
--- /dev/null
+++ b/addons/stock/report/report_stock_forecasted.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from collections import defaultdict
+
+from odoo import api, models
+from odoo.tools import float_is_zero, format_datetime, format_date, float_round
+
+
+class ReplenishmentReport(models.AbstractModel):
+ _name = 'report.stock.report_product_product_replenishment'
+ _description = "Stock Replenishment Report"
+
+ def _product_domain(self, product_template_ids, product_variant_ids):
+ if product_template_ids:
+ return [('product_tmpl_id', 'in', product_template_ids)]
+ return [('product_id', 'in', product_variant_ids)]
+
+ def _move_domain(self, product_template_ids, product_variant_ids, wh_location_ids):
+ move_domain = self._product_domain(product_template_ids, product_variant_ids)
+ move_domain += [('product_uom_qty', '!=', 0)]
+ out_domain = move_domain + [
+ '&',
+ ('location_id', 'in', wh_location_ids),
+ ('location_dest_id', 'not in', wh_location_ids),
+ ]
+ in_domain = move_domain + [
+ '&',
+ ('location_id', 'not in', wh_location_ids),
+ ('location_dest_id', 'in', wh_location_ids),
+ ]
+ return in_domain, out_domain
+
+ def _move_draft_domain(self, product_template_ids, product_variant_ids, wh_location_ids):
+ in_domain, out_domain = self._move_domain(product_template_ids, product_variant_ids, wh_location_ids)
+ in_domain += [('state', '=', 'draft')]
+ out_domain += [('state', '=', 'draft')]
+ return in_domain, out_domain
+
+ def _move_confirmed_domain(self, product_template_ids, product_variant_ids, wh_location_ids):
+ in_domain, out_domain = self._move_domain(product_template_ids, product_variant_ids, wh_location_ids)
+ out_domain += [('state', 'not in', ['draft', 'cancel', 'done'])]
+ in_domain += [('state', 'not in', ['draft', 'cancel', 'done'])]
+ return in_domain, out_domain
+
+ def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids):
+ in_domain, out_domain = self._move_draft_domain(product_template_ids, product_variant_ids, wh_location_ids)
+ incoming_moves = self.env['stock.move'].read_group(in_domain, ['product_qty:sum'], 'product_id')
+ outgoing_moves = self.env['stock.move'].read_group(out_domain, ['product_qty:sum'], 'product_id')
+ in_sum = sum(move['product_qty'] for move in incoming_moves)
+ out_sum = sum(move['product_qty'] for move in outgoing_moves)
+ return {
+ 'draft_picking_qty': {
+ 'in': in_sum,
+ 'out': out_sum
+ },
+ 'qty': {
+ 'in': in_sum,
+ 'out': out_sum
+ }
+ }
+
+ @api.model
+ def _get_report_values(self, docids, data=None):
+ return {
+ 'data': data,
+ 'doc_ids': docids,
+ 'doc_model': 'product.product',
+ 'docs': self._get_report_data(product_variant_ids=docids),
+ }
+
+ def _get_report_data(self, product_template_ids=False, product_variant_ids=False):
+ assert product_template_ids or product_variant_ids
+ res = {}
+
+ # Get the warehouse we're working on as well as its locations.
+ if self.env.context.get('warehouse'):
+ warehouse = self.env['stock.warehouse'].browse(self.env.context['warehouse'])
+ else:
+ warehouse = self.env['stock.warehouse'].search([
+ ('company_id', '=', self.env.company.id)
+ ], limit=1)
+ self.env.context = dict(self.env.context, warehouse=warehouse.id)
+ wh_location_ids = [loc['id'] for loc in self.env['stock.location'].search_read(
+ [('id', 'child_of', warehouse.view_location_id.id)],
+ ['id'],
+ )]
+ res['active_warehouse'] = warehouse.display_name
+
+ # Get the products we're working, fill the rendering context with some of their attributes.
+ if product_template_ids:
+ product_templates = self.env['product.template'].browse(product_template_ids)
+ res['product_templates'] = product_templates
+ res['product_variants'] = product_templates.product_variant_ids
+ res['multiple_product'] = len(product_templates.product_variant_ids) > 1
+ res['uom'] = product_templates[:1].uom_id.display_name
+ res['quantity_on_hand'] = sum(product_templates.mapped('qty_available'))
+ res['virtual_available'] = sum(product_templates.mapped('virtual_available'))
+ elif product_variant_ids:
+ product_variants = self.env['product.product'].browse(product_variant_ids)
+ res['product_templates'] = False
+ res['product_variants'] = product_variants
+ res['multiple_product'] = len(product_variants) > 1
+ res['uom'] = product_variants[:1].uom_id.display_name
+ res['quantity_on_hand'] = sum(product_variants.mapped('qty_available'))
+ res['virtual_available'] = sum(product_variants.mapped('virtual_available'))
+ res.update(self._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids))
+
+ res['lines'] = self._get_report_lines(product_template_ids, product_variant_ids, wh_location_ids)
+ return res
+
+ def _prepare_report_line(self, quantity, move_out=None, move_in=None, replenishment_filled=True, product=False, reservation=False):
+ timezone = self._context.get('tz')
+ product = product or (move_out.product_id if move_out else move_in.product_id)
+ is_late = move_out.date < move_in.date if (move_out and move_in) else False
+ return {
+ 'document_in': move_in._get_source_document() if move_in else False,
+ 'document_out': move_out._get_source_document() if move_out else False,
+ 'product': {
+ 'id': product.id,
+ 'display_name': product.display_name
+ },
+ 'replenishment_filled': replenishment_filled,
+ 'uom_id': product.uom_id,
+ 'receipt_date': format_datetime(self.env, move_in.date, timezone, dt_format=False) if move_in else False,
+ 'delivery_date': format_datetime(self.env, move_out.date, timezone, dt_format=False) if move_out else False,
+ 'is_late': is_late,
+ 'quantity': float_round(quantity, precision_rounding=product.uom_id.rounding),
+ 'move_out': move_out,
+ 'move_in': move_in,
+ 'reservation': reservation,
+ }
+
+ def _get_report_lines(self, product_template_ids, product_variant_ids, wh_location_ids):
+ def _rollup_move_dests(move, seen):
+ for dst in move.move_dest_ids:
+ if dst.id not in seen:
+ seen.add(dst.id)
+ _rollup_move_dests(dst, seen)
+ return seen
+
+ def _reconcile_out_with_ins(lines, out, ins, demand, only_matching_move_dest=True):
+ index_to_remove = []
+ for index, in_ in enumerate(ins):
+ if float_is_zero(in_['qty'], precision_rounding=out.product_id.uom_id.rounding):
+ continue
+ if only_matching_move_dest and in_['move_dests'] and out.id not in in_['move_dests']:
+ continue
+ taken_from_in = min(demand, in_['qty'])
+ demand -= taken_from_in
+ lines.append(self._prepare_report_line(taken_from_in, move_in=in_['move'], move_out=out))
+ in_['qty'] -= taken_from_in
+ if in_['qty'] <= 0:
+ index_to_remove.append(index)
+ if float_is_zero(demand, precision_rounding=out.product_id.uom_id.rounding):
+ break
+ for index in index_to_remove[::-1]:
+ ins.pop(index)
+ return demand
+
+ in_domain, out_domain = self._move_confirmed_domain(
+ product_template_ids, product_variant_ids, wh_location_ids
+ )
+ outs = self.env['stock.move'].search(out_domain, order='priority desc, date, id')
+ outs_per_product = defaultdict(lambda: [])
+ for out in outs:
+ outs_per_product[out.product_id.id].append(out)
+ ins = self.env['stock.move'].search(in_domain, order='priority desc, date, id')
+ ins_per_product = defaultdict(lambda: [])
+ for in_ in ins:
+ ins_per_product[in_.product_id.id].append({
+ 'qty': in_.product_qty,
+ 'move': in_,
+ 'move_dests': _rollup_move_dests(in_, set())
+ })
+ currents = {c['id']: c['qty_available'] for c in outs.product_id.read(['qty_available'])}
+
+ lines = []
+ for product in (ins | outs).product_id:
+ for out in outs_per_product[product.id]:
+ if out.state not in ('partially_available', 'assigned'):
+ continue
+ current = currents[out.product_id.id]
+ reserved = out.product_uom._compute_quantity(out.reserved_availability, product.uom_id)
+ currents[product.id] -= reserved
+ lines.append(self._prepare_report_line(reserved, move_out=out, reservation=True))
+
+ unreconciled_outs = []
+ for out in outs_per_product[product.id]:
+ # Reconcile with the current stock.
+ current = currents[out.product_id.id]
+ reserved = 0.0
+ if out.state in ('partially_available', 'assigned'):
+ reserved = out.product_uom._compute_quantity(out.reserved_availability, product.uom_id)
+ demand = out.product_qty - reserved
+ taken_from_stock = min(demand, current)
+ if not float_is_zero(taken_from_stock, precision_rounding=product.uom_id.rounding):
+ currents[product.id] -= taken_from_stock
+ demand -= taken_from_stock
+ lines.append(self._prepare_report_line(taken_from_stock, move_out=out))
+ # Reconcile with the ins.
+ if not float_is_zero(demand, precision_rounding=product.uom_id.rounding):
+ demand = _reconcile_out_with_ins(lines, out, ins_per_product[out.product_id.id], demand, only_matching_move_dest=True)
+ if not float_is_zero(demand, precision_rounding=product.uom_id.rounding):
+ unreconciled_outs.append((demand, out))
+ if unreconciled_outs:
+ for (demand, out) in unreconciled_outs:
+ # Another pass, in case there are some ins linked to a dest move but that still have some quantity available
+ demand = _reconcile_out_with_ins(lines, out, ins_per_product[product.id], demand, only_matching_move_dest=False)
+ if not float_is_zero(demand, precision_rounding=product.uom_id.rounding):
+ # Not reconciled
+ lines.append(self._prepare_report_line(demand, move_out=out, replenishment_filled=False))
+ # Unused remaining stock.
+ free_stock = currents.get(product.id, 0)
+ if not float_is_zero(free_stock, precision_rounding=product.uom_id.rounding):
+ lines.append(self._prepare_report_line(free_stock, product=product))
+ # In moves not used.
+ for in_ in ins_per_product[product.id]:
+ if float_is_zero(in_['qty'], precision_rounding=product.uom_id.rounding):
+ continue
+ lines.append(self._prepare_report_line(in_['qty'], move_in=in_['move']))
+ return lines
+
+ @api.model
+ def get_filter_state(self):
+ res = {}
+ res['warehouses'] = self.env['stock.warehouse'].search_read(fields=['id', 'name', 'code'])
+ res['active_warehouse'] = self.env.context.get('warehouse', False)
+ if not res['active_warehouse']:
+ company_id = self.env.context.get('allowed_company_ids')[0]
+ res['active_warehouse'] = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).id
+ return res
+
+
+class ReplenishmentTemplateReport(models.AbstractModel):
+ _name = 'report.stock.report_product_template_replenishment'
+ _description = "Stock Replenishment Report"
+ _inherit = 'report.stock.report_product_product_replenishment'
+
+ @api.model
+ def _get_report_values(self, docids, data=None):
+ return {
+ 'data': data,
+ 'doc_ids': docids,
+ 'doc_model': 'product.product',
+ 'docs': self._get_report_data(product_template_ids=docids),
+ }
diff --git a/addons/stock/report/report_stock_forecasted.xml b/addons/stock/report/report_stock_forecasted.xml
new file mode 100644
index 00000000..61748f6c
--- /dev/null
+++ b/addons/stock/report/report_stock_forecasted.xml
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<!-- Reports -->
+ <record id="stock_replenishment_report_product_product_action" model="ir.actions.report">
+ <field name="name">Forecasted Report</field>
+ <field name="model">product.product</field>
+ <field name="report_type">qweb-html</field>
+ <field name="report_name">stock.report_product_product_replenishment</field>
+ </record>
+
+ <record id="stock_replenishment_report_product_template_action" model="ir.actions.report">
+ <field name="name">Forecasted Report</field>
+ <field name="model">product.template</field>
+ <field name="report_type">qweb-html</field>
+ <field name="report_name">stock.report_product_template_replenishment</field>
+ </record>
+
+ <record id="stock_replenishment_product_product_action" model="ir.actions.client">
+ <field name="name">Forecasted Report</field>
+ <field name="tag">replenish_report</field>
+ </record>
+
+<!-- Templates -->
+ <template id="assets_common_replenishment_report" name="Forecasted Inventory CSS" inherit_id="web.report_assets_common">
+ <xpath expr="." position="inside">
+ <link rel="stylesheet" type="text/scss" href="/web/static/src/scss/graph_view.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/stock/static/src/scss/report_stock_forecasted.scss"/>
+ </xpath>
+ </template>
+
+ <template id="report_replenishment_header">
+ <div class="d-flex justify-content-between">
+ <div class="o_product_name">
+ <h3>
+ <t t-if="docs['product_templates']">
+ <t t-foreach="docs['product_templates']" t-as="product_template">
+ <a href="#" res-model="product.template" view-type="form" t-att-res-id="product_template.id">
+ <t t-esc="product_template.display_name"/>
+ </a>
+ </t>
+ </t>
+ <t t-elif="docs['product_variants']">
+ <t t-foreach="docs['product_variants']" t-as="product_variant">
+ <a href="#" res-model="product.product" view-type="form" t-att-res-id="product_variant.id">
+ <t t-esc="product_variant.display_name"/>
+ </a>
+ </t>
+ </t>
+ </h3>
+ <h6 t-if="docs['product_templates'] and docs['product_variants'] and len(docs['product_templates']) != len(docs['product_variants'])">
+ <t t-foreach="docs['product_variants']" t-as="product_variant">
+ <a href="#" res-model="product.product" view-type="form" t-att-res-id="product_variant.id">
+ <t t-esc="'[%s]' % product_variant.product_template_attribute_value_ids._get_combination_name()"/>
+ </a>
+ </t>
+ </h6>
+ </div>
+ <div class="row">
+ <div class="mx-3 text-center">
+ <div class="h3">
+ <t t-esc="docs['quantity_on_hand']"/>
+ <t t-esc="docs['uom']" groups="uom.group_uom"/>
+ </div>
+ <div>On Hand</div>
+ </div>
+ <div t-attf-class="mx-3 text-center #{docs['virtual_available'] &lt; 0 and 'text-danger'}">
+ <div class="h3">
+ <t t-esc="docs['virtual_available']"/>
+ <t t-esc="docs['uom']" groups="uom.group_uom"/>
+ </div>
+ <div>Forecasted</div>
+ </div>
+ <div name="pending_forecasted" t-attf-class="mx-3 text-center #{future_virtual_available &lt; 0 and 'text-danger'}">
+ <div class="h3">
+ <t t-esc="future_virtual_available"/>
+ <t t-esc="docs['uom']" groups="uom.group_uom"/>
+ </div>
+ <div>Forecasted<br/>+ Pending</div>
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <template id="report_product_product_replenishment">
+ <t t-call="web.html_container">
+ <div class="page pt-3 o_report_replenishment_page">
+ <t t-set="future_virtual_available" t-value="docs['virtual_available'] + docs['qty']['in'] - docs['qty']['out']"/>
+ <t t-call="stock.report_replenishment_header"/>
+ <div class="o_report_graph"/>
+ <table class="o_report_replenishment table table-bordered">
+ <thead>
+ <tr class="bg-light">
+ <td>Replenishment</td>
+ <td>Expected Receipt</td>
+ <td t-if="docs['multiple_product']">Product</td>
+ <td class="text-right">Quantity</td>
+ <td groups="uom.group_uom">UoM</td>
+ <td>Used by</td>
+ <td>Expected Delivery</td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-if="docs['lines'] and not any(line['document_in'] or line['replenishment_filled'] for line in docs['lines'])">
+ <td>Inventory On Hand</td>
+ <td/>
+ <td t-if="docs['multiple_product']"/>
+ <td class="text-right">0</td>
+ <td/>
+ <td/>
+ <td/>
+ <td/>
+ </tr>
+ <tr t-foreach="docs['lines']" t-as="line">
+ <td t-attf-class="#{line['is_late'] and 'o_grid_warning'}">
+ <a t-if="line['document_in']"
+ t-attf-href="#" t-esc="line['document_in'].name"
+ class="font-weight-bold" view-type="form"
+ t-att-res-model="line['document_in']._name"
+ t-att-res-id="line['document_in'].id"/>
+ <t t-elif="line['reservation']">
+ Reserved from stock
+ </t>
+ <t t-elif="line['replenishment_filled']">
+ <t t-if="line['document_out']">Inventory On Hand</t>
+ <t t-else="">Free Stock</t>
+ </t>
+ <span t-else="" class="text-muted">Not Available</span>
+ </td>
+ <td t-esc="line['receipt_date'] or ''"
+ t-attf-class="#{line['is_late'] and 'o_grid_warning'}"/>
+ <td t-if="docs['multiple_product']" t-esc="line['product']['display_name']"/>
+ <td class="text-right"><t t-if="not line['replenishment_filled']">- </t><t t-esc="line['quantity']"/></td>
+ <td t-esc="line['uom_id'].name" groups="uom.group_uom"/>
+ <td t-attf-class="#{not line['replenishment_filled'] and 'o_grid_warning'}">
+ <a t-if="line['document_out']"
+ t-attf-href="#" t-esc="line['document_out'].name"
+ class="font-weight-bold" view-type="form"
+ t-att-res-model="line['document_out']._name"
+ t-att-res-id="line['document_out'].id"/>
+ </td>
+ <td t-esc="line['delivery_date'] or ''"
+ t-attf-class="#{not line['replenishment_filled'] and 'o_grid_warning'}"/>
+ </tr>
+ </tbody>
+ <thead>
+ <tr class="o_forecasted_row">
+ <td colspan="2">Forecasted Inventory</td>
+ <td t-esc="docs['virtual_available']" class="text-right"/>
+ <td t-esc="docs['uom']" groups="uom.group_uom"/>
+ </tr>
+ </thead>
+ <tbody t-if="docs['qty']['in']">
+ <tr t-if="docs['draft_picking_qty']['in']" name="draft_picking_in">
+ <td colspan="2">Incoming Draft Transfer</td>
+ <td t-esc="docs['draft_picking_qty']['in']" class="text-right"/>
+ <td t-esc="docs['uom']" groups="uom.group_uom"/>
+ </tr>
+ <tr t-if="docs['draft_picking_qty']['out']" name="draft_picking_out">
+ <td colspan="2">Outgoing Draft Transfer</td>
+ <td t-esc="-docs['draft_picking_qty']['out']" class="text-right"/>
+ <td t-esc="docs['uom']" groups="uom.group_uom"/>
+ </tr>
+ </tbody>
+ <thead>
+ <tr class="o_forecasted_row">
+ <td colspan="2">Forecasted with Pending</td>
+ <td t-esc="future_virtual_available" class="text-right"/>
+ <td t-esc="docs['uom']" groups="uom.group_uom"/>
+ </tr>
+ </thead>
+ </table>
+ </div>
+ </t>
+ </template>
+
+ <template id="report_product_template_replenishment">
+ <t t-call="stock.report_product_product_replenishment"/>
+ </template>
+</odoo>
diff --git a/addons/stock/report/report_stock_quantity.py b/addons/stock/report/report_stock_quantity.py
new file mode 100644
index 00000000..c54f1b1a
--- /dev/null
+++ b/addons/stock/report/report_stock_quantity.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, tools, api
+
+
+class ReportStockQuantity(models.Model):
+ _name = 'report.stock.quantity'
+ _auto = False
+ _description = 'Stock Quantity Report'
+
+ date = fields.Date(string='Date', readonly=True)
+ product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id')
+ product_id = fields.Many2one('product.product', string='Product', readonly=True)
+ state = fields.Selection([
+ ('forecast', 'Forecasted Stock'),
+ ('in', 'Forecasted Receipts'),
+ ('out', 'Forecasted Deliveries'),
+ ], string='State', readonly=True)
+ product_qty = fields.Float(string='Quantity', readonly=True)
+ move_ids = fields.One2many('stock.move', readonly=True)
+ company_id = fields.Many2one('res.company', readonly=True)
+ warehouse_id = fields.Many2one('stock.warehouse', readonly=True)
+
+ def init(self):
+ tools.drop_view_if_exists(self._cr, 'report_stock_quantity')
+ query = """
+CREATE or REPLACE VIEW report_stock_quantity AS (
+SELECT
+ MIN(id) as id,
+ product_id,
+ state,
+ date,
+ sum(product_qty) as product_qty,
+ company_id,
+ warehouse_id
+FROM (SELECT
+ m.id,
+ m.product_id,
+ CASE
+ WHEN (whs.id IS NOT NULL AND whd.id IS NULL) OR ls.usage = 'transit' THEN 'out'
+ WHEN (whs.id IS NULL AND whd.id IS NOT NULL) OR ld.usage = 'transit' THEN 'in'
+ END AS state,
+ m.date::date AS date,
+ CASE
+ WHEN (whs.id IS NOT NULL AND whd.id IS NULL) OR ls.usage = 'transit' THEN -product_qty
+ WHEN (whs.id IS NULL AND whd.id IS NOT NULL) OR ld.usage = 'transit' THEN product_qty
+ END AS product_qty,
+ m.company_id,
+ CASE
+ WHEN (whs.id IS NOT NULL AND whd.id IS NULL) OR ls.usage = 'transit' THEN whs.id
+ WHEN (whs.id IS NULL AND whd.id IS NOT NULL) OR ld.usage = 'transit' THEN whd.id
+ END AS warehouse_id
+ FROM
+ stock_move m
+ LEFT JOIN stock_location ls on (ls.id=m.location_id)
+ LEFT JOIN stock_location ld on (ld.id=m.location_dest_id)
+ LEFT JOIN stock_warehouse whs ON ls.parent_path like concat('%/', whs.view_location_id, '/%')
+ LEFT JOIN stock_warehouse whd ON ld.parent_path like concat('%/', whd.view_location_id, '/%')
+ LEFT JOIN product_product pp on pp.id=m.product_id
+ LEFT JOIN product_template pt on pt.id=pp.product_tmpl_id
+ WHERE
+ pt.type = 'product' AND
+ product_qty != 0 AND
+ (whs.id IS NOT NULL OR whd.id IS NOT NULL) AND
+ (whs.id IS NULL OR whd.id IS NULL OR whs.id != whd.id) AND
+ m.state NOT IN ('cancel', 'draft', 'done')
+ UNION ALL
+ SELECT
+ -q.id as id,
+ q.product_id,
+ 'forecast' as state,
+ date.*::date,
+ q.quantity as product_qty,
+ q.company_id,
+ wh.id as warehouse_id
+ FROM
+ GENERATE_SERIES((now() at time zone 'utc')::date - interval '3month',
+ (now() at time zone 'utc')::date + interval '3 month', '1 day'::interval) date,
+ stock_quant q
+ LEFT JOIN stock_location l on (l.id=q.location_id)
+ LEFT JOIN stock_warehouse wh ON l.parent_path like concat('%/', wh.view_location_id, '/%')
+ WHERE
+ (l.usage = 'internal' AND wh.id IS NOT NULL) OR
+ l.usage = 'transit'
+ UNION ALL
+ SELECT
+ m.id,
+ m.product_id,
+ 'forecast' as state,
+ GENERATE_SERIES(
+ CASE
+ WHEN m.state = 'done' THEN (now() at time zone 'utc')::date - interval '3month'
+ ELSE m.date::date
+ END,
+ CASE
+ WHEN m.state != 'done' THEN (now() at time zone 'utc')::date + interval '3 month'
+ ELSE m.date::date - interval '1 day'
+ END, '1 day'::interval)::date date,
+ CASE
+ WHEN ((whs.id IS NOT NULL AND whd.id IS NULL) OR ls.usage = 'transit') AND m.state = 'done' THEN product_qty
+ WHEN ((whs.id IS NULL AND whd.id IS NOT NULL) OR ld.usage = 'transit') AND m.state = 'done' THEN -product_qty
+ WHEN (whs.id IS NOT NULL AND whd.id IS NULL) OR ls.usage = 'transit' THEN -product_qty
+ WHEN (whs.id IS NULL AND whd.id IS NOT NULL) OR ld.usage = 'transit' THEN product_qty
+ END AS product_qty,
+ m.company_id,
+ CASE
+ WHEN (whs.id IS NOT NULL AND whd.id IS NULL) OR ls.usage = 'transit' THEN whs.id
+ WHEN (whs.id IS NULL AND whd.id IS NOT NULL) OR ld.usage = 'transit' THEN whd.id
+ END AS warehouse_id
+ FROM
+ stock_move m
+ LEFT JOIN stock_location ls on (ls.id=m.location_id)
+ LEFT JOIN stock_location ld on (ld.id=m.location_dest_id)
+ LEFT JOIN stock_warehouse whs ON ls.parent_path like concat('%/', whs.view_location_id, '/%')
+ LEFT JOIN stock_warehouse whd ON ld.parent_path like concat('%/', whd.view_location_id, '/%')
+ LEFT JOIN product_product pp on pp.id=m.product_id
+ LEFT JOIN product_template pt on pt.id=pp.product_tmpl_id
+ WHERE
+ pt.type = 'product' AND
+ product_qty != 0 AND
+ (whs.id IS NOT NULL OR whd.id IS NOT NULL) AND
+ (whs.id IS NULL or whd.id IS NULL OR whs.id != whd.id) AND
+ m.state NOT IN ('cancel', 'draft')) AS forecast_qty
+GROUP BY product_id, state, date, company_id, warehouse_id
+);
+"""
+ self.env.cr.execute(query)
+
+ @api.model
+ def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+ for i in range(len(domain)):
+ if domain[i][0] == 'product_tmpl_id' and domain[i][1] in ('=', 'in'):
+ tmpl = self.env['product.template'].browse(domain[i][2])
+ # Avoid the subquery done for the related, the postgresql will plan better with the SQL view
+ # and then improve a lot the performance for the forecasted report of the product template.
+ domain[i] = ('product_id', 'in', tmpl.with_context(active_test=False).product_variant_ids.ids)
+ return super().read_group(domain, fields, groupby, offset, limit, orderby, lazy)
diff --git a/addons/stock/report/report_stock_quantity.xml b/addons/stock/report/report_stock_quantity.xml
new file mode 100644
index 00000000..61018561
--- /dev/null
+++ b/addons/stock/report/report_stock_quantity.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="stock_report_view_graph" model="ir.ui.view">
+ <field name="name">stock_report_view_graph</field>
+ <field name="model">report.stock.quantity</field>
+ <field name="arch" type="xml">
+ <graph string="report_stock_quantity_graph" type="line" sample="1" disable_linking="1">
+ <field name="date" interval="day"/>
+ <field name="product_id" type="row"/>
+ <field name="product_qty" type="measure"/>
+ </graph>
+ </field>
+ </record>
+
+ <record id="stock_report_view_search" model="ir.ui.view">
+ <field name="name">report.stock.quantity.search</field>
+ <field name="model">report.stock.quantity</field>
+ <field name="arch" type="xml">
+ <search string="Report Quantity">
+ <field name="product_id"/>
+ <field name="state"/>
+ <field name="product_tmpl_id"/>
+ <field name="warehouse_id"/>
+ <group expand="0" string="State">
+ <filter string="Forecasted Stock" name="filter_forecast" domain="[('state', '=', 'forecast')]"/>
+ <filter string="Forecasted Receipts" name="filter_in" domain="[('state', '=', 'in')]"/>
+ <filter string="Forecasted Deliveries" name="filter_out" domain="[('state', '=', 'out')]"/>
+ <separator/>
+ <filter string="Date" name="filter_date" date="date"/>
+ </group>
+ <group expand="0" string="Group By">
+ <filter string="Date" name="groupby_date" domain="[]" context="{'group_by':'date:day'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="report_stock_quantity_action_product" model="ir.actions.act_window">
+ <field name="name">Forecasted Inventory</field>
+ <field name="res_model">report.stock.quantity</field>
+ <field name="view_mode">graph</field>
+ <field name="context">{
+ 'search_default_filter_forecast': 1,
+ 'graph_groupbys': ['date:day', 'state', 'product_id'],
+ }</field>
+ </record>
+
+ <record id="report_stock_quantity_action" model="ir.actions.act_window">
+ <field name="name">Forecasted Inventory</field>
+ <field name="res_model">report.stock.quantity</field>
+ <field name="view_mode">graph</field>
+ <field name="context">{
+ 'search_default_filter_forecast': 1,
+ 'graph_groupbys': ['date:day', 'state', 'product_id'],
+ }</field>
+ </record>
+</odoo>
+
diff --git a/addons/stock/report/report_stock_rule.py b/addons/stock/report/report_stock_rule.py
new file mode 100644
index 00000000..ad2fc264
--- /dev/null
+++ b/addons/stock/report/report_stock_rule.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models, _
+from odoo.exceptions import UserError
+
+
+class ReportStockRule(models.AbstractModel):
+ _name = 'report.stock.report_stock_rule'
+ _description = 'Stock rule report'
+
+ @api.model
+ def _get_report_values(self, docids, data=None):
+ # Overriding data values here since used also in _get_routes.
+ data['product_id'] = data.get('product_id', docids)
+ data['warehouse_ids'] = data.get('warehouse_ids', [])
+
+ product = self.env['product.product'].browse(data['product_id'])
+ warehouses = self.env['stock.warehouse'].browse(data['warehouse_ids'])
+
+ routes = self._get_routes(data)
+
+ # Some routes don't have a warehouse_id but contain rules of different warehouses,
+ # we filter here the ones we want to display and build for each one a dict containing the rule,
+ # their source and destination location.
+ relevant_rules = routes.mapped('rule_ids').filtered(lambda r: not r.warehouse_id or r.warehouse_id in warehouses)
+ rules_and_loc = []
+ for rule in relevant_rules:
+ rules_and_loc.append(self._get_rule_loc(rule, product))
+
+ locations = self._sort_locations(rules_and_loc, warehouses)
+ reordering_rules = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', product.id)])
+ locations |= reordering_rules.mapped('location_id').filtered(lambda l: l not in locations)
+ locations_names = locations.mapped('display_name')
+ # Here we handle reordering rules and putaway strategies by creating the header_lines dict. This dict is indexed
+ # by location_id and contains itself another dict with the relevant reordering rules and putaway strategies.
+ header_lines = {}
+ for location in locations:
+ # TODO: group the RR by location_id to avoid a filtered at each loop
+ rr = reordering_rules.filtered(lambda r: r.location_id.id == location.id)
+ putaways = product.putaway_rule_ids.filtered(lambda p: p.location_in_id.id == location.id)
+ if putaways or rr:
+ header_lines[location.id] = {'putaway': [], 'orderpoint': []}
+ for putaway in putaways:
+ header_lines[location.id]['putaway'].append(putaway)
+ for r in rr:
+ header_lines[location.id]['orderpoint'].append(r)
+ route_lines = []
+ colors = self._get_route_colors()
+ for color_index, route in enumerate(routes):
+ rules_to_display = route.rule_ids & relevant_rules
+ if rules_to_display:
+ route_color = colors[color_index % len(colors)]
+ color_index = color_index + 1
+ for rule in rules_to_display:
+ rule_loc = [r for r in rules_and_loc if r['rule'] == rule][0]
+ res = []
+ for x in range(len(locations_names)):
+ res.append([])
+ idx = locations_names.index(rule_loc['destination'].display_name)
+ tpl = (rule, 'destination', route_color, )
+ res[idx] = tpl
+ idx = locations_names.index(rule_loc['source'].display_name)
+ tpl = (rule, 'origin', route_color, )
+ res[idx] = tpl
+ route_lines.append(res)
+ return {
+ 'docs': product,
+ 'locations': locations,
+ 'header_lines': header_lines,
+ 'route_lines': route_lines,
+ }
+
+ @api.model
+ def _get_route_colors(self):
+ return ['#FFA500', '#800080', '#228B22', '#008B8B', '#4682B4', '#FF0000', '#32CD32']
+
+ @api.model
+ def _get_routes(self, data):
+ """ Extract the routes to display from the wizard's content.
+ """
+ product = self.env['product.product'].browse(data['product_id'])
+ warehouse_ids = self.env['stock.warehouse'].browse(data['warehouse_ids'])
+ return product.route_ids | product.categ_id.total_route_ids | warehouse_ids.mapped('route_ids')
+
+ @api.model
+ def _get_rule_loc(self, rule, product):
+ rule.ensure_one()
+ return {'rule': rule, 'source': rule.location_src_id, 'destination': rule.location_id}
+
+ @api.model
+ def _sort_locations(self, rules_and_loc, warehouses):
+ """ We order the locations by setting first the locations of type supplier and manufacture,
+ then we add the locations grouped by warehouse and we finish by the locations of type
+ customer and the ones that were not added by the sort.
+ """
+ all_src = self.env['stock.location'].concat(*([r['source'] for r in rules_and_loc]))
+ all_dest = self.env['stock.location'].concat(*([r['destination'] for r in rules_and_loc]))
+ all_locations = all_src | all_dest
+ ordered_locations = self.env['stock.location']
+ locations = all_locations.filtered(lambda l: l.usage in ('supplier', 'production'))
+ for warehouse_id in warehouses:
+ all_warehouse_locations = all_locations.filtered(lambda l: l.get_warehouse() == warehouse_id)
+ starting_rules = [d for d in rules_and_loc if d['source'] not in all_warehouse_locations]
+ if starting_rules:
+ start_locations = self.env['stock.location'].concat(*([r['destination'] for r in starting_rules]))
+ else:
+ starting_rules = [d for d in rules_and_loc if d['source'] not in all_dest]
+ start_locations = self.env['stock.location'].concat(*([r['source'] for r in starting_rules]))
+ used_rules = self.env['stock.rule']
+ locations |= self._sort_locations_by_warehouse(rules_and_loc, used_rules, start_locations, ordered_locations, warehouse_id)
+ if any(location not in locations for location in all_warehouse_locations):
+ remaining_locations = self.env['stock.location'].concat(*([r['source'] for r in rules_and_loc])).filtered(lambda l: l not in locations)
+ locations |= self._sort_locations_by_warehouse(rules_and_loc, used_rules, remaining_locations, ordered_locations, warehouse_id)
+ locations |= all_locations.filtered(lambda l: l.usage in ('customer'))
+ locations |= all_locations.filtered(lambda l: l not in locations)
+ return locations
+
+ @api.model
+ def _sort_locations_by_warehouse(self, rules_and_loc, used_rules, start_locations, ordered_locations, warehouse_id):
+ """ We order locations by putting first the locations that are not the destination of others and do it recursively.
+ """
+ start_locations = start_locations.filtered(lambda l: l.get_warehouse() == warehouse_id)
+ ordered_locations |= start_locations
+ rules_start = []
+ for rule in rules_and_loc:
+ if rule['source'] in start_locations:
+ rules_start.append(rule)
+ used_rules |= rule['rule']
+ if rules_start:
+ rules_start_dest_locations = self.env['stock.location'].concat(*([r['destination'] for r in rules_start]))
+ remaining_rules = self.env['stock.rule'].concat(*([r['rule'] for r in rules_and_loc])) - used_rules
+ remaining_rules_location = self.env['stock.location']
+ for r in rules_and_loc:
+ if r['rule'] in remaining_rules:
+ remaining_rules_location |= r['destination']
+ start_locations = rules_start_dest_locations - ordered_locations - remaining_rules_location
+ ordered_locations = self._sort_locations_by_warehouse(rules_and_loc, used_rules, start_locations, ordered_locations, warehouse_id)
+ return ordered_locations
diff --git a/addons/stock/report/report_stock_rule.xml b/addons/stock/report/report_stock_rule.xml
new file mode 100644
index 00000000..21cd39ae
--- /dev/null
+++ b/addons/stock/report/report_stock_rule.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <template id="assets_common" name="report_stock_rule assets" inherit_id="web.report_assets_common">
+ <xpath expr="." position="inside">
+ <link rel="stylesheet" type="text/scss" href="/stock/static/src/scss/report_stock_rule.scss"/>
+ </xpath>
+ </template>
+
+ <template id="report_stock_rule">
+ <t t-set="data_report_landscape" t-value="True"/>
+ <t t-set="full_width" t-value="True"/>
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="o">
+ <div class="article o_report_stock_rule">
+ <div class="page">
+ <h2 t-field="o.name"/>
+
+ <table class="table table-condensed table-bordered">
+ <thead>
+ <tr>
+ <t t-foreach="locations" t-as="location">
+ <th class="o_report_stock_rule_location_header">
+ <div t-att-res-id="location.id" t-att-res-model="location._name" view-type="form">
+ <t t-esc="location.display_name"/>
+ </div>
+ <t t-if="header_lines.get(location.id)">
+ <t t-foreach="header_lines[location.id]['putaway']" t-as="lines">
+ <t t-foreach="lines" t-as="line">
+ <div class="o_report_stock_rule_putaway" t-att-res-id="location.id" t-att-res-model="location._name" view-type="form">
+ <p>Putaway: <t t-esc="line.location_out_id.display_name"/></p>
+ </div>
+ </t>
+ </t>
+ <t t-foreach="header_lines[location.id]['orderpoint']" t-as="lines">
+ <t t-foreach="lines" t-as="line">
+ <div class="o_report_stock_rule_putaway" t-att-res-id="line.id" t-att-res-model="line._name" view-type="form">
+ <p>[<t t-esc="line.display_name"/>]<br/>min: <t t-esc="line.product_min_qty"/>, max:<t t-esc="line.product_max_qty"/></p>
+ </div>
+ </t>
+ </t>
+ </t>
+ </th>
+ </t>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-foreach="route_lines" t-as="route_line">
+ <tr>
+ <t t-set="acc" t-value="0"/>
+ <t t-foreach="route_line" t-as="rule">
+ <t t-if="rule">
+ <t t-if="rule[0]._name == 'stock.rule'">
+ <t t-set="color" t-value="rule[2]"/>
+ <t t-if="acc > 0">
+ <t t-set="acc" t-value="acc+1"/>
+ <td t-att-colspan="acc" class="o_report_stock_rule_rule_cell">
+ <t t-set="padding" t-value="50.0/acc"/>
+ <div class="o_report_stock_rule_rule_main" t-att-res-id="rule[0].id" t-att-res-model="rule[0]._name" view-type="form" t-att-title="rule[0].route_id.display_name">
+ <div class="o_report_stock_rule_rule" t-attf-style="padding-left: #{padding}%; padding-right: #{padding}%;">
+ <t t-if="rule[1] == 'destination'">
+ <t t-if="rule[0].procure_method == 'make_to_order'">
+ <t t-call="stock.report_stock_rule_suspension_points"/>
+ </t>
+ <t t-if="rule[0].procure_method == 'mts_else_mto'">
+ <t t-call="stock.report_stock_rule_suspension_points"/>
+ <t t-call="stock.report_stock_rule_vertical_bar"/>
+ </t>
+ <t t-if="rule[0].action in ('push', 'pull_push')">
+ <t t-call="stock.report_stock_rule_right_arrow"/>
+ </t>
+ </t>
+ <t t-if="rule[1] == 'origin' and rule[0].action in ('pull', 'pull_push')">
+ <t t-call="stock.report_stock_rule_left_arrow"/>
+ </t>
+ <t t-call="stock.report_stock_rule_line"/>
+ <t t-if="rule[1] == 'destination' and rule[0].action in ('pull', 'pull_push')">
+ <t t-call="stock.report_stock_rule_right_arrow"/>
+ </t>
+ <t t-if="rule[1] == 'origin'">
+ <t t-if="rule[0].action in ('push', 'pull_push')">
+ <t t-call="stock.report_stock_rule_left_arrow"/>
+ </t>
+ <t t-if="rule[0].procure_method == 'make_to_order'">
+ <t t-call="stock.report_stock_rule_suspension_points"/>
+ </t>
+ </t>
+ </div>
+ <div class="o_report_stock_rule_rule_name">
+ <span t-attf-style="color: #{color};"><t t-esc="rule[0].picking_type_id.name"/></span>
+ </div>
+ </div>
+ </td>
+ <t t-set="acc" t-value="0"/>
+ </t>
+ <t t-else="">
+ <t t-set="acc" t-value="acc+1"/>
+ </t>
+ </t>
+ </t>
+ <t t-else="">
+ <t t-if="acc > 0">
+ <t t-set="acc" t-value="acc+1"/>
+ </t>
+ <t t-if="acc == 0">
+ <td>
+ </td>
+ </t>
+ </t>
+ </t>
+ </tr>
+ </t>
+ </tbody>
+ </table>
+ <h3>Legend</h3>
+ <div class="o_report_stock_rule_legend">
+ <t t-set="color" t-value="'black'"/>
+ <div class="o_report_stock_rule_legend_line">
+ <div class="o_report_stock_rule_legend_label">Push Rule</div>
+ <div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
+ <t t-call="stock.report_stock_rule_right_arrow"/>
+ <t t-call="stock.report_stock_rule_line"/>
+ </div>
+ </div>
+ <div class="o_report_stock_rule_legend_line">
+ <div class="o_report_stock_rule_legend_label">Pull Rule</div>
+ <div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
+ <t t-call="stock.report_stock_rule_line"/>
+ <t t-call="stock.report_stock_rule_right_arrow"/>
+ </div>
+ </div>
+ <div class="o_report_stock_rule_legend_line">
+ <div class="o_report_stock_rule_legend_label">Trigger Another Rule</div>
+ <div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
+ <t t-call="stock.report_stock_rule_suspension_points"/>
+ <t t-call="stock.report_stock_rule_line"/>
+ </div>
+ </div>
+ <div class="o_report_stock_rule_legend_line">
+ <div class="o_report_stock_rule_legend_label">Trigger Another Rule If No Stock</div>
+ <div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
+ <t t-call="stock.report_stock_rule_suspension_points"/>
+ <t t-call="stock.report_stock_rule_vertical_bar"/>
+ <t t-call="stock.report_stock_rule_line"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ </t>
+ </template>
+ <template id="report_stock_rule_line">
+ <div class="o_report_stock_rule_line">
+ <svg width="100%" height="100%" viewBox="0 0 100 10" preserveAspectRatio="none">
+ <line x1="0" y1="5" x2="100" y2="5" t-attf-style="stroke: #{color};"/>
+ </svg>
+ </div>
+ </template>
+ <template id="report_stock_rule_vertical_bar">
+ <div class="o_report_stock_rule_vertical_bar">
+ <svg width="100%" height="100%" viewBox="0 0 1 1">
+ <line y1="-12" x2="0" y2="12" x1="0" t-attf-style="stroke: #{color};"/>
+ </svg>
+ </div>
+ </template>
+ <template id="report_stock_rule_right_arrow">
+ <div class="o_report_stock_rule_arrow">
+ <svg width="100%" height="100%" viewBox="0 0 10 10">
+ <polygon points="0,0 0,10 10,5" t-attf-style="stroke: #{color}; fill: #{color};"/>
+ </svg>
+ </div>
+ </template>
+ <template id="report_stock_rule_left_arrow">
+ <div class="o_report_stock_rule_arrow">
+ <svg width="100%" height="100%" viewBox="0 0 10 10">
+ <polygon points="0,5 10,10 10,0" t-attf-style="stroke: #{color}; fill: #{color};"/>
+ </svg>
+ </div>
+ </template>
+ <template id="report_stock_rule_suspension_points">
+ <div class="o_report_stock_rule_arrow">
+ <svg width="100%" height="100%" viewBox="0 0 10 10" >
+ <line x1="1" y1="5" x2="4.5" y2="5" t-attf-style="stroke: #{color};"/>
+ <line x1="5.5" y1="5" x2="9" y2="5" t-attf-style="stroke: #{color};"/>
+ </svg>
+ </div>
+ </template>
+</odoo>
+
diff --git a/addons/stock/report/report_stockinventory.xml b/addons/stock/report/report_stockinventory.xml
new file mode 100644
index 00000000..2aff98f1
--- /dev/null
+++ b/addons/stock/report/report_stockinventory.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <template id="report_inventory">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="o">
+ <t t-call="web.external_layout">
+ <div class="page">
+ <br/>
+ <h2 t-field="o.name"/>
+
+ <div class="row mt32 mb32" id="informations">
+ <div t-if="o.date" class="col-auto mw-100 mb-2">
+ <strong>Date:</strong>
+ <p class="m-0" t-field="o.date"/>
+ </div>
+ </div>
+
+ <t t-set="locations" t-value="o.line_ids.mapped('location_id')"/>
+ <table class="table table-sm">
+ <thead>
+ <tr>
+ <th groups="stock.group_stock_multi_locations"><strong>Location</strong></th>
+ <th><strong>Product</strong></th>
+ <th groups="stock.group_production_lot"><strong>Lot/Serial Number</strong></th>
+ <th groups="stock.group_tracking_lot"><strong>Package</strong></th>
+ <th class="text-right"><strong>On Hand Quantity</strong></th>
+ <th class="text-right"><strong>Counted Quantity</strong></th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-foreach="locations" t-as="location">
+ <tr groups="stock.group_stock_multi_locations">
+ <td colspan="2"><strong t-esc="location.display_name"/></td>
+ <td groups="stock.group_production_lot"></td>
+ <td groups="stock.group_tracking_lot"></td>
+ <td></td>
+ </tr>
+ <tr t-foreach="o.line_ids.filtered(lambda line: line.location_id.id == location.id)" t-as="line">
+ <td groups="stock.group_stock_multi_locations"></td>
+ <td><span t-field="line.product_id"/></td>
+ <td groups="stock.group_production_lot"><span t-field="line.prod_lot_id"/></td>
+ <td groups="stock.group_tracking_lot"><span t-field="line.package_id"/></td>
+ <td class="text-right"><span t-field="line.theoretical_qty"/> <span t-field="line.product_uom_id" groups="uom.group_uom"/></td>
+ <td class="text-right"><span t-field="line.product_qty"/> <span t-field="line.product_uom_id" groups="uom.group_uom"/></td>
+ </tr>
+ </t>
+ </tbody>
+ </table>
+ </div>
+ </t>
+ </t>
+ </t>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml
new file mode 100644
index 00000000..dacc8ff8
--- /dev/null
+++ b/addons/stock/report/report_stockpicking_operations.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+
+ <template id="report_picking">
+ <t t-call="web.html_container">
+ <t t-foreach="docs" t-as="o">
+ <t t-call="web.external_layout">
+ <div class="page">
+ <div class="row justify-content-end mb16">
+ <div class="col-4" name="right_box">
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', o.name, 600, 100)" style="width:300px;height:50px;" alt="Barcode"/>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-6" name="div_outgoing_address">
+ <div t-if="o.move_ids_without_package and o.move_ids_without_package[0].partner_id and o.move_ids_without_package[0].partner_id.id != o.partner_id.id">
+ <span><strong>Delivery Address:</strong></span>
+ <div t-field="o.move_ids_without_package[0].partner_id"
+ t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
+ </div>
+ <div t-if="o.picking_type_id.code != 'internal' and (not o.move_ids_without_package or not o.move_ids_without_package[0].partner_id) and o.picking_type_id.warehouse_id.partner_id">
+ <span><strong>Warehouse Address:</strong></span>
+ <div t-field="o.picking_type_id.warehouse_id.partner_id"
+ t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
+ </div>
+ </div>
+ <div class="col-5 offset-1" name="div_incoming_address">
+ <div t-if="o.picking_type_id.code=='incoming' and o.partner_id">
+ <span><strong>Vendor Address:</strong></span>
+ </div>
+ <div t-if="o.picking_type_id.code=='internal' and o.partner_id">
+ <span><strong>Warehouse Address:</strong></span>
+ </div>
+ <div t-if="o.picking_type_id.code=='outgoing' and o.partner_id">
+ <span><strong>Customer Address:</strong></span>
+ </div>
+ <div t-if="o.partner_id" name="partner_header">
+ <div t-field="o.partner_id"
+ t-options='{"widget": "contact", "fields": ["name", "phone"], "no_marker": True, "phone_icons": True}'/>
+ <p t-if="o.sudo().partner_id.vat"><t t-esc="o.company_id.country_id.vat_label or 'Tax ID'"/>: <span t-field="o.sudo().partner_id.vat"/></p>
+ </div>
+ </div>
+ </div>
+ <br/>
+ <h1 t-field="o.name" class="mt0 float-left"/>
+ <div class="row mt48 mb32">
+ <div t-if="o.origin" class="col-auto" name="div_origin">
+ <strong>Order:</strong>
+ <p t-field="o.origin"/>
+ </div>
+ <div class="col-auto" name="div_state">
+ <strong>Status:</strong>
+ <p t-field="o.state"/>
+ </div>
+ <div class="col-auto" name="div_sched_date">
+ <strong>Scheduled Date:</strong>
+ <p t-field="o.scheduled_date"/>
+ </div>
+ </div>
+ <table class="table table-sm" t-if="o.move_line_ids and o.move_ids_without_package">
+ <t t-set="has_barcode" t-value="any(move_line.product_id and move_line.product_id.sudo().barcode or move_line.package_id for move_line in o.move_line_ids)"/>
+ <t t-set="has_serial_number" t-value="any(move_line.lot_id or move_line.lot_name for move_line in o.move_line_ids)" groups="stock.group_production_lot"/>
+ <thead>
+ <tr>
+ <th name="th_product">
+ <strong>Product</strong>
+ </th>
+ <th>
+ <strong>Quantity</strong>
+ </th>
+ <th name="th_from" t-if="o.picking_type_id.code != 'incoming'" align="left" groups="stock.group_stock_multi_locations">
+ <strong>From</strong>
+ </th>
+ <th name="th_to" t-if="o.picking_type_id.code != 'outgoing'" groups="stock.group_stock_multi_locations">
+ <strong>To</strong>
+ </th>
+ <th name="th_serial_number" class="text-center" t-if="has_serial_number">
+ <strong>Lot/Serial Number</strong>
+ </th>
+ <th name="th_barcode" class="text-center" t-if="has_barcode">
+ <strong>Product Barcode</strong>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <t t-foreach="o.move_ids_without_package" t-as="move">
+ <!-- In case you come across duplicated lines, ask NIM or LAP -->
+ <t t-foreach="move.move_line_ids.sorted(key=lambda ml: ml.location_id.id)" t-as="ml">
+ <tr>
+ <td>
+ <span t-field="ml.product_id.display_name"/><br/>
+ <span t-field="ml.product_id.description_picking"/>
+ </td>
+ <td>
+ <span t-if="o.state != 'done'" t-field="ml.product_uom_qty"/>
+ <span t-if="o.state == 'done'" t-field="ml.qty_done"/>
+ <span t-field="ml.product_uom_id" groups="uom.group_uom"/>
+
+ </td>
+ <td t-if="o.picking_type_id.code != 'incoming'" groups="stock.group_stock_multi_locations">
+ <span t-esc="ml.location_id.display_name"/>
+ <t t-if="ml.package_id">
+ <span t-field="ml.package_id"/>
+ </t>
+ </td>
+ <td t-if="o.picking_type_id.code != 'outgoing'" groups="stock.group_stock_multi_locations">
+ <div>
+ <span t-field="ml.location_dest_id"/>
+ <t t-if="ml.result_package_id">
+ <span t-field="ml.result_package_id"/>
+ </t>
+ </div>
+ </td>
+ <td class=" text-center h6" t-if="has_serial_number">
+ <img t-if="has_serial_number and (ml.lot_id or ml.lot_name)" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;humanreadable=1' % ('Code128', ml.lot_id.name or ml.lot_name, 400, 100)" style="width:100%;height:35px;" alt="Barcode"/>
+
+ </td>
+ <td class="text-center" t-if="has_barcode">
+ <t t-if="product_barcode != move.product_id.barcode">
+ <span t-if="move.product_id and move.product_id.barcode">
+ <img t-if="len(move.product_id.barcode) == 13" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;quiet=%s' % ('EAN13', move.product_id.barcode, 400, 100, 0)" style="height:35px" alt="Barcode"/>
+ <img t-elif="len(move.product_id.barcode) == 8" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;quiet=%s' % ('EAN8', move.product_id.barcode, 400, 100, 0)" style="height:35px" alt="Barcode"/>
+ <img t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;quiet=%s' % ('Code128', move.product_id.barcode, 400, 100, 0)" style="height:35px" alt="Barcode"/>
+
+ </span>
+ <t t-set="product_barcode" t-value="move.product_id.barcode"/>
+ </t>
+ </td>
+ </tr>
+ </t>
+ </t>
+ </tbody>
+ </table>
+ <table class="table table-sm" t-if="o.package_level_ids and o.picking_type_entire_packs and o.state in ['assigned', 'done']">
+ <thead>
+ <tr>
+ <th name="th_package">Package</th>
+ <th name="th_pko_from" t-if="o.picking_type_id.code != 'incoming'" groups="stock.group_stock_multi_locations">From</th>
+ <th name="th_pki_from" t-if="o.picking_type_id.code != 'outgoing'" groups="stock.group_stock_multi_locations">To</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="o.package_level_ids.sorted(key=lambda p: p.package_id.name)" t-as="package">
+ <t t-set="package" t-value="package.with_context(picking_id=o.id)" />
+ <td name="td_pk_barcode">
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s&amp;humanreadable=1' % ('Code128', package.package_id.name, 600, 100)" style="width:300px;height:50px; margin-left: -50px;" alt="Barcode"/><br/>
+ </td>
+ <td t-if="o.picking_type_id.code != 'incoming'" groups="stock.group_stock_multi_locations">
+ <span t-field="package.location_id"/>
+ </td>
+ <td t-if="o.picking_type_id.code != 'outgoing'" groups="stock.group_stock_multi_locations">
+ <span t-field="package.location_dest_id"/>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <t t-set="no_reserved_product" t-value="o.move_lines.filtered(lambda x: x.product_uom_qty != x.reserved_availability and x.move_line_ids and x.state!='done')"/>
+ <p t-if="o.state in ['draft', 'waiting', 'confirmed'] or no_reserved_product"><i class="fa fa-exclamation-triangle" />
+ All products could not be reserved. Click on the "Check Availability" button to try to reserve products.
+ </p>
+ <p t-field="o.note"/>
+ </div>
+ </t>
+ </t>
+ </t>
+ </template>
+ <template id="report_picking_type_label">
+ <t t-set="title">Operation Types</t>
+ <t t-call="stock.report_generic_barcode"/>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/stock/report/stock_report_views.xml b/addons/stock/report/stock_report_views.xml
new file mode 100644
index 00000000..dea0838f
--- /dev/null
+++ b/addons/stock/report/stock_report_views.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <record id="action_report_picking" model="ir.actions.report">
+ <field name="name">Picking Operations</field>
+ <field name="model">stock.picking</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_picking</field>
+ <field name="report_file">stock.report_picking_operations</field>
+ <field name="print_report_name">'Picking Operations - %s - %s' % (object.partner_id.name or '', object.name)</field>
+ <field name="binding_model_id" ref="model_stock_picking"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_delivery" model="ir.actions.report">
+ <field name="name">Delivery Slip</field>
+ <field name="model">stock.picking</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_deliveryslip</field>
+ <field name="report_file">stock.report_deliveryslip</field>
+ <field name="print_report_name">'Delivery Slip - %s - %s' % (object.partner_id.name or '', object.name)</field>
+ <field name="binding_model_id" ref="model_stock_picking"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_inventory" model="ir.actions.report">
+ <field name="name">Count Sheet</field>
+ <field name="model">stock.inventory</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_inventory</field>
+ <field name="report_file">stock.report_inventory</field>
+ <field name="print_report_name">'Inventory - %s' % (object.name)</field>
+ <field name="binding_model_id" ref="model_stock_inventory"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_quant_package_barcode" model="ir.actions.report">
+ <field name="name">Package Barcode with Content</field>
+ <field name="model">stock.quant.package</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_package_barcode</field>
+ <field name="report_file">stock.report_package_barcode</field>
+ <field name="binding_model_id" ref="model_stock_quant_package"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_quant_package_barcode_small" model="ir.actions.report">
+ <field name="name">Package Barcode (PDF)</field>
+ <field name="model">stock.quant.package</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_package_barcode_small</field>
+ <field name="report_file">stock.report_package_barcode</field>
+ <field name="binding_model_id" ref="model_stock_quant_package"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_location_barcode" model="ir.actions.report">
+ <field name="name">Location Barcode</field>
+ <field name="model">stock.location</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_location_barcode</field>
+ <field name="report_file">stock.report_location_barcode</field>
+ <field name="print_report_name">'Location - %s' % object.name</field>
+ <field name="binding_model_id" ref="model_stock_location"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_lot_label" model="ir.actions.report">
+ <field name="name">Lot/Serial Number (PDF)</field>
+ <field name="model">stock.production.lot</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_lot_label</field>
+ <field name="report_file">stock.report_lot_label</field>
+ <field name="print_report_name">'Lot-Serial - %s' % object.name</field>
+ <field name="binding_model_id" ref="model_stock_production_lot"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_picking_type_label" model="ir.actions.report">
+ <field name="name">Operation type (PDF)</field>
+ <field name="model">stock.picking.type</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.report_picking_type_label</field>
+ <field name="report_file">stock.report_picking_type_label</field>
+ <field name="print_report_name">'Operation-type - %s' % object.name</field>
+ <field name="binding_model_id" ref="model_stock_picking_type"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_report_stock_rule" model="ir.actions.report">
+ <field name="name">Product Routes Report</field>
+ <field name="model">product.template</field>
+ <field name="report_type">qweb-html</field>
+ <field name="report_name">stock.report_stock_rule</field>
+ <field name="report_file">stock.report_stock_rule</field>
+ </record>
+ <record id="label_product_template" model="ir.actions.report">
+ <field name="name">Product Label (ZPL)</field>
+ <field name="model">product.template</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_product_template_view</field>
+ <field name="report_file">stock.label_product_template_view</field>
+ <field name="binding_model_id" ref="product.model_product_template"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_product_product" model="ir.actions.report">
+ <field name="name">Product Label (ZPL)</field>
+ <field name="model">product.product</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_product_product_view</field>
+ <field name="report_file">stock.label_product_product_view</field>
+ <field name="binding_model_id" ref="product.model_product_product"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_barcode_product_template" model="ir.actions.report">
+ <field name="name">Product Barcode (ZPL)</field>
+ <field name="model">product.template</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_barcode_product_template_view</field>
+ <field name="report_file">stock.label_barcode_product_template_view</field>
+ <field name="binding_model_id" ref="product.model_product_template"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_barcode_product_product" model="ir.actions.report">
+ <field name="name">Product Barcode (ZPL)</field>
+ <field name="model">product.product</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_barcode_product_product_view</field>
+ <field name="report_file">stock.label_barcode_product_product_view</field>
+ <field name="binding_model_id" ref="product.model_product_product"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_lot_template" model="ir.actions.report">
+ <field name="name">Lot/Serial Number (ZPL)</field>
+ <field name="model">stock.production.lot</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_lot_template_view</field>
+ <field name="report_file">stock.label_lot_template_view</field>
+ <field name="binding_model_id" ref="model_stock_production_lot"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_label_transfer_template_zpl" model="ir.actions.report">
+ <field name="name">Barcodes (ZPL)</field>
+ <field name="model">stock.picking</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_transfer_template_view_zpl</field>
+ <field name="report_file">stock.label_transfer_template_view_zpl</field>
+ <field name="binding_model_id" ref="model_stock_picking"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="action_label_transfer_template_pdf" model="ir.actions.report">
+ <field name="name">Barcodes (PDF)</field>
+ <field name="model">stock.picking</field>
+ <field name="report_type">qweb-pdf</field>
+ <field name="report_name">stock.label_transfer_template_view_pdf</field>
+ <field name="report_file">stock.label_transfer_template_view_pdf</field>
+ <field name="binding_model_id" ref="model_stock_picking"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_package_template" model="ir.actions.report">
+ <field name="name">Package Barcode (ZPL)</field>
+ <field name="model">stock.quant.package</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_package_template_view</field>
+ <field name="report_file">stock.label_package_template_view</field>
+ <field name="binding_model_id" ref="model_stock_quant_package"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_product_packaging" model="ir.actions.report">
+ <field name="name">Product Packaging (ZPL)</field>
+ <field name="model">product.packaging</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_product_packaging_view</field>
+ <field name="report_file">stock.label_product_packaging_view</field>
+ <field name="binding_model_id" ref="product.model_product_packaging"/>
+ <field name="binding_type">report</field>
+ </record>
+ <record id="label_picking_type" model="ir.actions.report">
+ <field name="name">Operation type (ZPL)</field>
+ <field name="model">stock.picking.type</field>
+ <field name="report_type">qweb-text</field>
+ <field name="report_name">stock.label_picking_type_view</field>
+ <field name="report_file">stock.label_picking_type_view</field>
+ <field name="binding_model_id" ref="model_stock_picking_type"/>
+ <field name="binding_type">report</field>
+ </record>
+ </data>
+</odoo>
diff --git a/addons/stock/report/stock_traceability.py b/addons/stock/report/stock_traceability.py
new file mode 100644
index 00000000..42495ea5
--- /dev/null
+++ b/addons/stock/report/stock_traceability.py
@@ -0,0 +1,243 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models, _
+from odoo.tools import config
+from odoo.tools import format_datetime
+
+
+rec = 0
+def autoIncrement():
+ global rec
+ pStart = 1
+ pInterval = 1
+ if rec == 0:
+ rec = pStart
+ else:
+ rec += pInterval
+ return rec
+
+
+class MrpStockReport(models.TransientModel):
+ _name = 'stock.traceability.report'
+ _description = 'Traceability Report'
+
+ @api.model
+ def _get_move_lines(self, move_lines, line_id=None):
+ lines_seen = move_lines
+ lines_todo = list(move_lines)
+ while lines_todo:
+ move_line = lines_todo.pop(0)
+ # if MTO
+ if move_line.move_id.move_orig_ids:
+ lines = move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
+ lambda m: m.lot_id == move_line.lot_id and m.state == 'done'
+ ) - lines_seen
+ # if MTS
+ elif move_line.location_id.usage == 'internal':
+ lines = self.env['stock.move.line'].search([
+ ('product_id', '=', move_line.product_id.id),
+ ('lot_id', '=', move_line.lot_id.id),
+ ('location_dest_id', '=', move_line.location_id.id),
+ ('id', 'not in', lines_seen.ids),
+ ('date', '<=', move_line.date),
+ ('state', '=', 'done')
+ ])
+ else:
+ continue
+ if line_id is None or line_id in lines.ids:
+ lines_todo += list(lines)
+ lines_seen |= lines
+ return lines_seen - move_lines
+
+ @api.model
+ def get_lines(self, line_id=None, **kw):
+ context = dict(self.env.context)
+ model = kw and kw['model_name'] or context.get('model')
+ rec_id = kw and kw['model_id'] or context.get('active_id')
+ level = kw and kw['level'] or 1
+ lines = self.env['stock.move.line']
+ move_line = self.env['stock.move.line']
+ if rec_id and model == 'stock.production.lot':
+ lines = move_line.search([
+ ('lot_id', '=', context.get('lot_name') or rec_id),
+ ('state', '=', 'done'),
+ ])
+ elif rec_id and model == 'stock.move.line' and context.get('lot_name'):
+ record = self.env[model].browse(rec_id)
+ dummy, is_used = self._get_linked_move_lines(record)
+ if is_used:
+ lines = is_used
+ elif rec_id and model in ('stock.picking', 'mrp.production'):
+ record = self.env[model].browse(rec_id)
+ if model == 'stock.picking':
+ lines = record.move_lines.mapped('move_line_ids').filtered(lambda m: m.lot_id and m.state == 'done')
+ else:
+ lines = record.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.state == 'done')
+ move_line_vals = self._lines(line_id, model_id=rec_id, model=model, level=level, move_lines=lines)
+ final_vals = sorted(move_line_vals, key=lambda v: v['date'], reverse=True)
+ lines = self._final_vals_to_lines(final_vals, level)
+ return lines
+
+ @api.model
+ def _get_reference(self, move_line):
+ res_model = ''
+ ref = ''
+ res_id = False
+ picking_id = move_line.picking_id or move_line.move_id.picking_id
+ if picking_id:
+ res_model = 'stock.picking'
+ res_id = picking_id.id
+ ref = picking_id.name
+ elif move_line.move_id.inventory_id:
+ res_model = 'stock.inventory'
+ res_id = move_line.move_id.inventory_id.id
+ ref = 'Inv. Adj.: ' + move_line.move_id.inventory_id.name
+ elif move_line.move_id.scrapped and move_line.move_id.scrap_ids:
+ res_model = 'stock.scrap'
+ res_id = move_line.move_id.scrap_ids[0].id
+ ref = move_line.move_id.scrap_ids[0].name
+ return res_model, res_id, ref
+
+ @api.model
+ def _quantity_to_str(self, from_uom, to_uom, qty):
+ """ workaround to apply the float rounding logic of t-esc on data prepared server side """
+ qty = from_uom._compute_quantity(qty, to_uom, rounding_method='HALF-UP')
+ return self.env['ir.qweb.field.float'].value_to_html(qty, {'decimal_precision': 'Product Unit of Measure'})
+
+ def _get_usage(self, move_line):
+ usage = ''
+ if (move_line.location_id.usage == 'internal') and (move_line.location_dest_id.usage == 'internal'):
+ usage = 'internal'
+ elif (move_line.location_id.usage != 'internal') and (move_line.location_dest_id.usage == 'internal'):
+ usage = 'in'
+ else:
+ usage = 'out'
+ return usage
+
+ def _make_dict_move(self, level, parent_id, move_line, unfoldable=False):
+ res_model, res_id, ref = self._get_reference(move_line)
+ dummy, is_used = self._get_linked_move_lines(move_line)
+ data = [{
+ 'level': level,
+ 'unfoldable': unfoldable,
+ 'date': move_line.move_id.date,
+ 'parent_id': parent_id,
+ 'is_used': bool(is_used),
+ 'usage': self._get_usage(move_line),
+ 'model_id': move_line.id,
+ 'model': 'stock.move.line',
+ 'product_id': move_line.product_id.display_name,
+ 'product_qty_uom': "%s %s" % (self._quantity_to_str(move_line.product_uom_id, move_line.product_id.uom_id, move_line.qty_done), move_line.product_id.uom_id.name),
+ 'lot_name': move_line.lot_id.name,
+ 'lot_id': move_line.lot_id.id,
+ 'location_source': move_line.location_id.name,
+ 'location_destination': move_line.location_dest_id.name,
+ 'reference_id': ref,
+ 'res_id': res_id,
+ 'res_model': res_model}]
+ return data
+
+ @api.model
+ def _final_vals_to_lines(self, final_vals, level):
+ lines = []
+ for data in final_vals:
+ lines.append({
+ 'id': autoIncrement(),
+ 'model': data['model'],
+ 'model_id': data['model_id'],
+ 'parent_id': data['parent_id'],
+ 'usage': data.get('usage', False),
+ 'is_used': data.get('is_used', False),
+ 'lot_name': data.get('lot_name', False),
+ 'lot_id': data.get('lot_id', False),
+ 'reference': data.get('reference_id', False),
+ 'res_id': data.get('res_id', False),
+ 'res_model': data.get('res_model', False),
+ 'columns': [data.get('reference_id', False),
+ data.get('product_id', False),
+ format_datetime(self.env, data.get('date', False), tz=False, dt_format=False),
+ data.get('lot_name', False),
+ data.get('location_source', False),
+ data.get('location_destination', False),
+ data.get('product_qty_uom', 0)],
+ 'level': level,
+ 'unfoldable': data['unfoldable'],
+ })
+ return lines
+
+ def _get_linked_move_lines(self, move_line):
+ """ This method will return the consumed line or produced line for this operation."""
+ return False, False
+
+ @api.model
+ def _lines(self, line_id=None, model_id=False, model=False, level=0, move_lines=[], **kw):
+ final_vals = []
+ lines = move_lines or []
+ if model and line_id:
+ move_line = self.env[model].browse(model_id)
+ move_lines, is_used = self._get_linked_move_lines(move_line)
+ if move_lines:
+ lines = move_lines
+ else:
+ # Traceability in case of consumed in.
+ lines = self._get_move_lines(move_line, line_id=line_id)
+ for line in lines:
+ unfoldable = False
+ if line.consume_line_ids or ( line.lot_id and self._get_move_lines(line) and model != "stock.production.lot"):
+ unfoldable = True
+ final_vals += self._make_dict_move(level, parent_id=line_id, move_line=line, unfoldable=unfoldable)
+ return final_vals
+
+ def get_pdf_lines(self, line_data=[]):
+ lines = []
+ for line in line_data:
+ model = self.env[line['model_name']].browse(line['model_id'])
+ unfoldable = False
+ if line.get('unfoldable'):
+ unfoldable = True
+ final_vals = self._make_dict_move(line['level'], parent_id=line['id'], move_line=model, unfoldable=unfoldable)
+ lines.append(self._final_vals_to_lines(final_vals, line['level'])[0])
+ return lines
+
+ def get_pdf(self, line_data=[]):
+ lines = self.with_context(print_mode=True).get_pdf_lines(line_data)
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ rcontext = {
+ 'mode': 'print',
+ 'base_url': base_url,
+ }
+
+ context = dict(self.env.context)
+ if not config['test_enable']:
+ context['commit_assetsbundle'] = True
+
+ body = self.env['ir.ui.view'].with_context(context)._render_template(
+ "stock.report_stock_inventory_print",
+ values=dict(rcontext, lines=lines, report=self, context=self),
+ )
+
+ header = self.env['ir.actions.report']._render_template("web.internal_layout", values=rcontext)
+ header = self.env['ir.actions.report']._render_template("web.minimal_layout", values=dict(rcontext, subst=True, body=header))
+
+ return self.env['ir.actions.report']._run_wkhtmltopdf(
+ [body],
+ header=header,
+ landscape=True,
+ specific_paperformat_args={'data-report-margin-top': 10, 'data-report-header-spacing': 10}
+ )
+
+ def _get_html(self):
+ result = {}
+ rcontext = {}
+ context = dict(self.env.context)
+ rcontext['lines'] = self.with_context(context).get_lines()
+ result['html'] = self.env.ref('stock.report_stock_inventory')._render(rcontext)
+ return result
+
+ @api.model
+ def get_html(self, given_context=None):
+ res = self.search([('create_uid', '=', self.env.uid)], limit=1)
+ if not res:
+ return self.create({}).with_context(given_context)._get_html()
+ return res.with_context(given_context)._get_html()