summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorOdoo's Mergebot <xmo+roboto@odoo.com>2020-07-03 17:34:51 +0200
committerGitHub <noreply@github.com>2020-07-03 17:34:51 +0200
commit9d7f3be8b3449d2809f6742b358e9da5bef53797 (patch)
treecf93bf9424fa8f93898f931d3ac3c051e6901371 /addons
parent600bfa11963e98a02cbec8a2273a27dcbd91be7a (diff)
parent0aa0984f832209bee30de22c0550d76a46ddc05f (diff)
[IMP] delivery, product_expiry, stock: improve delivery slip
Improve delivery slip with following features: 1. Group Kit components on the delivery slip - Split kit components by kit "sections" 2. Print one line per product whatever the tracking - Combine duplicate product move lines when "Display Lots & Serial Numbers on Delivery Slips" is not activated or no lots/serial numbers are assigned in the picking 3. Group packages on the delivery slip - Split products up by package "sections" - Print package info such as "weight" 4. Expiration Dates - Give option to print lot/serial number expiration dates on delivery slip Task: 2008609 Description of the issue/feature this PR addresses: Current behavior before PR: Desired behavior after PR is merged: -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#49832 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/delivery/models/stock_move.py14
-rw-r--r--addons/delivery/models/stock_picking.py8
-rw-r--r--addons/delivery/views/report_deliveryslip.xml47
-rw-r--r--addons/mrp/__manifest__.py1
-rw-r--r--addons/mrp/models/stock_move.py18
-rw-r--r--addons/mrp/report/report_deliveryslip.xml81
-rw-r--r--addons/product_expiry/__manifest__.py3
-rw-r--r--addons/product_expiry/models/__init__.py1
-rw-r--r--addons/product_expiry/models/res_config_settings.py21
-rw-r--r--addons/product_expiry/report/report_deliveryslip.xml22
-rw-r--r--addons/product_expiry/security/stock_security.xml9
-rw-r--r--addons/product_expiry/views/product_template_views.xml2
-rw-r--r--addons/product_expiry/views/res_config_settings_views.xml23
-rw-r--r--addons/sale/views/sale_views.xml3
-rw-r--r--addons/sale_stock/views/sale_order_views.xml5
-rw-r--r--addons/stock/models/stock_move_line.py29
-rw-r--r--addons/stock/report/report_deliveryslip.xml134
17 files changed, 386 insertions, 35 deletions
diff --git a/addons/delivery/models/stock_move.py b/addons/delivery/models/stock_move.py
index 49ace8ec7ef..3b51fe7763e 100644
--- a/addons/delivery/models/stock_move.py
+++ b/addons/delivery/models/stock_move.py
@@ -41,3 +41,17 @@ class StockMoveLine(models.Model):
unit_price = move_line.product_id.list_price
qty = move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id)
move_line.sale_price = unit_price * qty
+
+ def _get_aggregated_product_quantities(self, **kwargs):
+ """Returns dictionary of products and corresponding values of interest + hs_code
+
+ Unfortunately because we are working with aggregated data, we have to loop through the
+ aggregation to add more values to each datum. This extension adds on the hs_code value.
+
+ returns: dictionary {same_key_as_super: {same_values_as_super, hs_code}, ...}
+ """
+ aggregated_move_lines = super()._get_aggregated_product_quantities(**kwargs)
+ for aggregated_move_line in aggregated_move_lines:
+ hs_code = aggregated_move_lines[aggregated_move_line]['product'].product_tmpl_id.hs_code
+ aggregated_move_lines[aggregated_move_line]['hs_code'] = hs_code
+ return aggregated_move_lines
diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py
index d4c90d77d55..20ad15160f2 100644
--- a/addons/delivery/models/stock_picking.py
+++ b/addons/delivery/models/stock_picking.py
@@ -66,7 +66,8 @@ class StockPicking(models.Model):
@api.depends('move_line_ids.result_package_id', 'move_line_ids.result_package_id.shipping_weight', 'weight_bulk')
def _compute_shipping_weight(self):
for picking in self:
- picking.shipping_weight = picking.weight_bulk + sum([pack.shipping_weight for pack in picking.package_ids])
+ # if shipping weight is not assigned => default to calculated product weight
+ picking.shipping_weight = picking.weight_bulk + sum([pack.shipping_weight or pack.weight for pack in picking.package_ids])
def _get_default_weight_uom(self):
return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
@@ -83,8 +84,9 @@ class StockPicking(models.Model):
carrier_tracking_url = fields.Char(string='Tracking URL', compute='_compute_carrier_tracking_url')
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom)
package_ids = fields.Many2many('stock.quant.package', compute='_compute_packages', string='Packages')
- weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight')
- shipping_weight = fields.Float("Weight for Shipping", compute='_compute_shipping_weight', help="Total weight of the packages and products which are not in a package. That's the weight used to compute the cost of the shipping.")
+ weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight', help="Total weight of products which are not in a package.")
+ shipping_weight = fields.Float("Weight for Shipping", compute='_compute_shipping_weight',
+ help="Total weight of packages and products not in a package. Packages with no shipping weight specified will default to their products' total weight. This is the weight used to compute the cost of the shipping.")
is_return_picking = fields.Boolean(compute='_compute_return_picking')
return_label_ids = fields.One2many('ir.attachment', compute='_compute_return_label')
diff --git a/addons/delivery/views/report_deliveryslip.xml b/addons/delivery/views/report_deliveryslip.xml
index 43031e5803e..4bf0057dd3b 100644
--- a/addons/delivery/views/report_deliveryslip.xml
+++ b/addons/delivery/views/report_deliveryslip.xml
@@ -6,7 +6,7 @@
<p t-field="o.carrier_id"/>
</div>
<div t-if="o.shipping_weight" class="col-auto">
- <strong>Weight:</strong>
+ <strong>Total Weight:</strong>
<br/>
<span t-field="o.shipping_weight"/>
<span t-field="o.weight_uom_name"/>
@@ -22,16 +22,51 @@
<th t-if="has_hs_code"><strong>HS Code</strong></th>
</xpath>
- <xpath expr="//table[@name='stock_move_table']/tbody/tr" position="inside">
- <td t-if="has_hs_code"><span t-field="move.product_id.hs_code"/></td>
- </xpath>
-
<xpath expr="//table[@name='stock_move_line_table']/thead/tr" position="inside">
<th t-if="has_hs_code"><strong>HS Code</strong></th>
</xpath>
- <xpath expr="//table[@name='stock_move_line_table']/tbody/tr" position="inside">
+ </template>
+
+ <!-- HS Code to table rows-->
+ <template id="stock_report_delivery_has_serial_move_line_inherit_delivery" inherit_id="stock.stock_report_delivery_has_serial_move_line">
+ <xpath expr="//td[@name='move_line_lot_qty_done']" position="after">
<td t-if="has_hs_code"><span t-field="move_line.product_id.hs_code"/></td>
</xpath>
+ </template>
+ <template id="stock_report_delivery_aggregated_move_lines_inherit_delivery" inherit_id="stock.stock_report_delivery_aggregated_move_lines">
+ <xpath expr="//td[@name='move_line_aggregated_qty_done']" position="after">
+ <td t-if="has_hs_code"><span t-esc="aggregated_lines[line]['hs_code']"/></td>
+ </xpath>
+ </template>
+ <!-- package related "section lines" -->
+ <template id="stock_report_delivery_package_section_line_inherit_delivery" inherit_id="stock.stock_report_delivery_package_section_line">
+ <!-- Add additional Package section line info -->
+ <xpath expr="//td[@name='package_info']" position="inside">
+ <t t-if="package.shipping_weight or package.weight">
+ <!-- assume manually typed in value = priority -->
+ <t t-if="package.shipping_weight">
+ <span> - Weight: </span>
+ <span t-field="package.shipping_weight"/>
+ <span t-field="package.weight_uom_name"/>
+ </t>
+ <!-- otherwise default to calculated value -->
+ <t t-else="">
+ <span> - Weight (estimated): </span>
+ <span t-field="package.weight"/>
+ <span t-field="package.weight_uom_name"/>
+ </t>
+ </t>
+ </xpath>
+ </template>
+ <template id="delivery_stock_report_delivery_no_package_section_line" inherit_id="stock.stock_report_delivery_no_package_section_line">
+ <!-- Add additional No Package section line info -->
+ <xpath expr="//td[@name='no_package_info']" position="inside">
+ <t t-if="o.weight_bulk">
+ <span> - Weight: </span>
+ <span t-field="o.weight_bulk"/>
+ <span t-field="o.weight_uom_name"/>
+ </t>
+ </xpath>
</template>
</odoo>
diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py
index ce6d450810d..a5fdb6e3ebc 100644
--- a/addons/mrp/__manifest__.py
+++ b/addons/mrp/__manifest__.py
@@ -36,6 +36,7 @@
'views/res_config_settings_views.xml',
'views/mrp_templates.xml',
'views/stock_scrap_views.xml',
+ 'report/report_deliveryslip.xml',
'report/mrp_report_views_main.xml',
'report/mrp_report_bom_structure.xml',
'report/mrp_production_templates.xml',
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 3ad3ba69067..f006fdb22d9 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -60,6 +60,24 @@ class StockMoveLine(models.Model):
move_line._log_message(production, move_line, 'mrp.track_production_move_template', vals)
return super(StockMoveLine, self).write(vals)
+ def _get_aggregated_product_quantities(self, **kwargs):
+ """Returns dictionary of products and corresponding values of interest grouped by optional kit_name
+
+ Removes descriptions where description == kit_name. kit_name is expected to be passed as a
+ kwargs value because this is not directly stored in move_line_ids. Unfortunately because we
+ are working with aggregated data, we have to loop through the aggregation to do this removal.
+
+ arguments: kit_name (optional): string value of a kit name passed as a kwarg
+ returns: dictionary {same_key_as_super: {same_values_as_super, ...}
+ """
+ aggregated_move_lines = super()._get_aggregated_product_quantities(**kwargs)
+ kit_name = kwargs.get('kit_name')
+ if kit_name:
+ for aggregated_move_line in aggregated_move_lines:
+ if aggregated_move_lines[aggregated_move_line]['description'] == kit_name:
+ aggregated_move_lines[aggregated_move_line]['description'] = ""
+ return aggregated_move_lines
+
class StockMove(models.Model):
_inherit = 'stock.move'
diff --git a/addons/mrp/report/report_deliveryslip.xml b/addons/mrp/report/report_deliveryslip.xml
new file mode 100644
index 00000000000..88370ca6e78
--- /dev/null
+++ b/addons/mrp/report/report_deliveryslip.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="stock_report_delivery_document_inherit_mrp" inherit_id="stock.report_delivery_document">
+ <!-- needs to be set before so elif directly follows if later on -->
+ <xpath expr="//t[@name='has_packages']" position="before">
+ <!-- get only the top level kits' (i.e. no subkit) move lines for easier mapping later on + we ignore subkit groupings-->
+ <!-- note that move.name uses top level kit's product.template.display_name value instead of product.template.name -->
+ <t t-set="has_kits" t-value="o.move_line_ids.filtered(lambda l: l.move_id.bom_line_id and l.move_id.bom_line_id.bom_id.product_tmpl_id.display_name == l.move_id.name)"/>
+ </xpath>
+ <xpath expr="//t[@name='no_package_section']" position="before">
+ <t t-set="has_kits" t-value="move_lines.filtered(lambda l: l.move_id.bom_line_id and l.move_id.bom_line_id.bom_id.product_tmpl_id.display_name == l.move_id.name)"/>
+ <t t-if="has_kits">
+ <!-- print the products not in a package or kit first -->
+ <t t-set="move_lines" t-value="move_lines.filtered(lambda m: not m.move_id.bom_line_id)"/>
+ </t>
+ </xpath>
+ <xpath expr="//t[@name='no_package_move_lines']" position="inside">
+ <t t-call="mrp.stock_report_delivery_kit_sections"/>
+ </xpath>
+ <xpath expr="//t[@name='has_packages']" position="after">
+ <!-- Additional use case: group by kits when no packages exist and then apply use case 1. (serial/lot numbers used/printed) -->
+ <t t-elif="has_kits and not has_packages">
+ <t t-call="mrp.stock_report_delivery_kit_sections"/>
+ <t t-call="mrp.stock_report_delivery_no_kit_section"/>
+ </t>
+ </xpath>
+ </template>
+
+ <template id="stock_report_delivery_kit_sections">
+ <!-- do another map to get unique top level kits -->
+ <t t-set="kits" t-value="has_kits.mapped('move_id.bom_line_id.bom_id.product_tmpl_id')"/>
+ <t t-foreach="kits" t-as="kit">
+ <tr t-att-class="'bg-200 font-weight-bold o_line_section'">
+ <td colspan="99">
+ <span t-esc="kit.display_name"/>
+ </td>
+ </tr>
+ <t t-set="kit_move_lines" t-value="has_kits.filtered(lambda l: l.move_id.name == kit.display_name)"/>
+ <t t-if="has_serial_number">
+ <tr t-foreach="kit_move_lines" t-as="move_line">
+ <t t-set="description" t-as="move_line.move_id.description_picking"/>
+ <t t-if="description == kit.name">
+ <t t-set="description" t-value=""/>
+ </t>
+ <t t-call="stock.stock_report_delivery_has_serial_move_line"/>
+ </tr>
+ </t>
+ <t t-else="">
+ <!-- move line description by default is the product_template.name (kit name), instead of display_name-->
+ <t t-set="aggregated_lines" t-value="kit_move_lines._get_aggregated_product_quantities(kit_name=kit.name)"/>
+ <t t-if="aggregated_lines">
+ <t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
+ </t>
+ </t>
+ </t>
+ </template>
+
+ <!-- No kit section is expected to only be called in no packages case -->
+ <template id="stock_report_delivery_no_kit_section">
+ <!-- Do another section for kit-less products if they exist -->
+ <t t-set="no_kit_move_lines" t-value="o.move_line_ids.filtered(lambda l: not l.move_id.bom_line_id)"/>
+ <t t-if="no_kit_move_lines">
+ <tr t-att-class="'bg-200 font-weight-bold o_line_section'">
+ <td colspan="99">
+ <span>Products not associated with a kit</span>
+ </td>
+ </tr>
+ <t t-if="has_serial_number">
+ <tr t-foreach="no_kit_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="no_kit_move_lines._get_aggregated_product_quantities()"/>
+ <t t-if="aggregated_lines">
+ <t t-call="stock.stock_report_delivery_aggregated_move_lines"/>
+ </t>
+ </t>
+ </t>
+ </template>
+</odoo>
diff --git a/addons/product_expiry/__manifest__.py b/addons/product_expiry/__manifest__.py
index 2021381a583..0a620081fa8 100644
--- a/addons/product_expiry/__manifest__.py
+++ b/addons/product_expiry/__manifest__.py
@@ -18,11 +18,14 @@ Following dates can be tracked:
Also implements the removal strategy First Expiry First Out (FEFO) widely used, for example, in food industries.
""",
'data': ['security/ir.model.access.csv',
+ 'security/stock_security.xml',
'views/production_lot_views.xml',
'views/product_template_views.xml',
+ 'views/res_config_settings_views.xml',
'views/stock_move_views.xml',
'views/stock_quant_views.xml',
'wizard/confirm_expiry_view.xml',
+ 'report/report_deliveryslip.xml',
'report/report_lot_barcode.xml',
'data/product_expiry_data.xml'],
}
diff --git a/addons/product_expiry/models/__init__.py b/addons/product_expiry/models/__init__.py
index ce07f86f046..55fcdd6ea27 100644
--- a/addons/product_expiry/models/__init__.py
+++ b/addons/product_expiry/models/__init__.py
@@ -3,6 +3,7 @@
from . import production_lot
from . import product_product
+from . import res_config_settings
from . import stock_move_line
from . import stock_move
from . import stock_picking
diff --git a/addons/product_expiry/models/res_config_settings.py b/addons/product_expiry/models/res_config_settings.py
new file mode 100644
index 00000000000..bb565c56346
--- /dev/null
+++ b/addons/product_expiry/models/res_config_settings.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ group_expiry_date_on_delivery_slip = fields.Boolean("Display Expiration Dates on Delivery Slips",
+ implied_group='product_expiry.group_expiry_date_on_delivery_slip')
+
+ @api.onchange('group_lot_on_delivery_slip')
+ def _onchange_group_lot_on_delivery_slip(self):
+ if not self.group_lot_on_delivery_slip:
+ self.group_expiry_date_on_delivery_slip = False
+
+ @api.onchange('module_product_expiry')
+ def _onchange_module_product_expiry(self):
+ if not self.module_product_expiry:
+ self.group_expiry_date_on_delivery_slip = False
diff --git a/addons/product_expiry/report/report_deliveryslip.xml b/addons/product_expiry/report/report_deliveryslip.xml
new file mode 100644
index 00000000000..1d390ee181c
--- /dev/null
+++ b/addons/product_expiry/report/report_deliveryslip.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="stock_report_delivery_document_inherit_product_expiry" inherit_id="stock.report_delivery_document">
+ <xpath expr="//t[@name='lot_serial']" position="after">
+ <t t-set="has_expiry_date" t-value="False"/>
+ <t t-set="has_expiry_date"
+ t-value="o.move_line_ids.filtered(lambda ml: ml.lot_id.expiration_date)"
+ groups="product_expiry.group_expiry_date_on_delivery_slip"/>
+ <t name="expiry_date" t-if="has_expiry_date">
+ <th>Expiration Date</th>
+ </t>
+ </xpath>
+ </template>
+
+ <template id="stock_report_delivery_has_serial_move_line_inherit_product_expiry" inherit_id="stock.stock_report_delivery_has_serial_move_line">
+ <xpath expr="//t[@name='move_line_lot']" position="after">
+ <t t-if="has_expiry_date">
+ <td><span t-field="move_line.lot_id.expiration_date" t-options='{"widget": "date"}'/></td>
+ </t>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/product_expiry/security/stock_security.xml b/addons/product_expiry/security/stock_security.xml
new file mode 100644
index 00000000000..0757b8ac205
--- /dev/null
+++ b/addons/product_expiry/security/stock_security.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data noupdate="0">
+ <record id="group_expiry_date_on_delivery_slip" model="res.groups">
+ <field name="name">Include expiration dates on delivery slip</field>
+ <field name="category_id" ref="base.module_category_hidden"/>
+ </record>
+</data>
+</odoo>
diff --git a/addons/product_expiry/views/product_template_views.xml b/addons/product_expiry/views/product_template_views.xml
index c6af30107e6..a6deea0d936 100644
--- a/addons/product_expiry/views/product_template_views.xml
+++ b/addons/product_expiry/views/product_template_views.xml
@@ -8,7 +8,7 @@
<group name="traceability" position="inside">
<field name="use_expiration_date" attrs="{'invisible': [('tracking', '=','none')]}"/>
</group>
- <group name="group_lots_and_weight" position="after">
+ <group name="stock_property" position="after">
<group string="Dates" name="expiry_and_lots" groups="stock.group_production_lot"
attrs="{'invisible': ['|', ('tracking', '=','none'), ('use_expiration_date', '=', False)]}">
<label for="expiration_time"/>
diff --git a/addons/product_expiry/views/res_config_settings_views.xml b/addons/product_expiry/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..1c636bed186
--- /dev/null
+++ b/addons/product_expiry/views/res_config_settings_views.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="res_config_settings_view_form_stock" model="ir.ui.view">
+ <field name="name">res.config.settings.view.form.inherit.product.expiry.stock</field>
+ <field name="model">res.config.settings</field>
+ <field name="inherit_id" ref="stock.res_config_settings_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//div[@id='group_lot_on_delivery_slip']" position="after">
+ <div class="col-12 col-lg-6 o_setting_box" attrs="{'invisible': ['|', ('group_lot_on_delivery_slip', '=', False), ('module_product_expiry', '=', False)]}" id="group_expiry_date_on_delivery_slip">
+ <div class="o_setting_left_pane">
+ <field name="group_expiry_date_on_delivery_slip"/>
+ </div>
+ <div class="o_setting_right_pane">
+ <label for="group_expiry_date_on_delivery_slip"/>
+ <div class="text-muted">
+ Expiration dates will appear on the delivery slip
+ </div>
+ </div>
+ </div>
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 2dc95eef0b6..0d7a0bb1005 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -677,7 +677,8 @@
<group name="sale_shipping">
<label for="commitment_date" string="Delivery Date"/>
<div name="commitment_date_div" class="o_row">
- <field name="commitment_date"/> <span class="text-muted">Expected: <field name="expected_date" widget="date"/></span>
+ <field name="commitment_date"/>
+ <span name="expected_date_span" class="text-muted">Expected: <field name="expected_date" widget="date"/></span>
</div>
</group>
<group string="Reporting" name="sale_reporting" groups="base.group_no_one">
diff --git a/addons/sale_stock/views/sale_order_views.xml b/addons/sale_stock/views/sale_order_views.xml
index 6a071b18457..88d5d56bb70 100644
--- a/addons/sale_stock/views/sale_order_views.xml
+++ b/addons/sale_stock/views/sale_order_views.xml
@@ -25,6 +25,11 @@
<field name="incoterm" widget="selection" groups="sale_stock.group_display_incoterm"/>
<field name="picking_policy" required="True"/>
</xpath>
+ <xpath expr="//span[@name='expected_date_span']" position="attributes">
+ <attribute name="attrs">
+ {'invisible': [('effective_date', '!=', False), ('commitment_date', '!=', False)]}
+ </attribute>
+ </xpath>
<xpath expr="//div[@name='commitment_date_div']" position="replace">
<div class="o_row">
<field name="commitment_date"/>
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index 079aad39305..6204341b12e 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -605,3 +605,32 @@ class StockMoveLine(models.Model):
def _should_bypass_reservation(self, location):
self.ensure_one()
return location.should_bypass_reservation() or self.product_id.type != 'product'
+
+ def _get_aggregated_product_quantities(self, **kwargs):
+ """ Returns a dictionary of products (key = id+name+description+uom) and corresponding values of interest.
+
+ Allows aggregation of data across separate move lines for the same product. This is expected to be useful
+ in things such as delivery reports. Dict key is made as a combination of values we expect to want to group
+ the products by (i.e. so data is not lost). This function purposely ignores lots/SNs because these are
+ expected to already be properly grouped by line.
+
+ returns: dictionary {product_id+name+description+uom: {product, name, description, qty_done, product_uom}, ...}
+ """
+ aggregated_move_lines = {}
+ for move_line in self:
+ name = move_line.product_id.display_name
+ description = move_line.move_id.description_picking
+ if description == name or description == move_line.product_id.name:
+ description = False
+ uom = move_line.product_uom_id
+ line_key = str(move_line.product_id.id) + "_" + name + (description or "") + "uom " + str(uom.id)
+
+ if line_key not in aggregated_move_lines:
+ aggregated_move_lines[line_key] = {'name': name,
+ 'description': description,
+ 'qty_done': move_line.qty_done,
+ 'product_uom': uom.name,
+ 'product': move_line.product_id}
+ else:
+ aggregated_move_lines[line_key]['qty_done'] += move_line.qty_done
+ return aggregated_move_lines
diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml
index f6f56b10e86..24bd5e2366b 100644
--- a/addons/stock/report/report_deliveryslip.xml
+++ b/addons/stock/report/report_deliveryslip.xml
@@ -1,6 +1,5 @@
<?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">
@@ -56,12 +55,13 @@
</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="o.move_line_ids.mapped('lot_id')" />
+ <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 t-if="has_serial_number" groups="stock.group_lot_on_delivery_slip">
- <th name="lot_serial">
+ <t name="lot_serial" t-if="has_serial_number">
+ <th>
Lot/Serial Number
</th>
</t>
@@ -69,26 +69,59 @@
</tr>
</thead>
<tbody>
- <tr t-foreach="o.move_line_ids" t-as="move_line">
- <td>
- <span t-field="move_line.product_id"/>
- <p t-if="move_line.move_id.description_picking != move_line.product_id.name">
- <span t-field="move_line.move_id.description_picking"/>
- </p>
- </td>
- <t t-if="has_serial_number" groups="stock.group_lot_on_delivery_slip">
- <td t-if="move_line.lot_name">
- <span t-field="move_line.lot_name"/>
- </td>
- <td t-else="">
- <span t-field="move_line.lot_id.name"/>
- </td>
+ <!-- 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>
- <td class="text-center">
- <span t-field="move_line.qty_done"/>
- <span t-field="move_line.product_uom_id"/>
- </td>
- </tr>
+ <!-- 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'))"/>
@@ -139,10 +172,63 @@
</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>