[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>
This commit is contained in:
Odoo's Mergebot
2020-07-03 17:34:51 +02:00
committed by GitHub
17 changed files with 386 additions and 35 deletions
+14
View File
@@ -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
+5 -3
View File
@@ -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')
+41 -6
View File
@@ -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>
+1
View File
@@ -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',
+18
View File
@@ -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'
+81
View File
@@ -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>
+3
View File
@@ -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'],
}
+1
View File
@@ -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
@@ -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
@@ -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>
@@ -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>
@@ -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"/>
@@ -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>
+2 -1
View File
@@ -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">
@@ -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"/>
+29
View File
@@ -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
+110 -24
View File
@@ -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>