diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/report | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/stock/report')
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&value=%s&width=%s&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&value=%s&width=%s&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 < 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 >= 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&value=%s&width=%s&height=%s&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&value=%s&width=%s&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&value=%s&width=%s&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&value=%s&width=%s&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'] < 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 < 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&value=%s&width=%s&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&value=%s&width=%s&height=%s&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&value=%s&width=%s&height=%s&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&value=%s&width=%s&height=%s&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&value=%s&width=%s&height=%s&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&value=%s&width=%s&height=%s&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() |
