Skip to content

Commit

Permalink
[FIX] stock: fix text product label reports with special characters
Browse files Browse the repository at this point in the history
Current behavior:
When printing products ZPL Labels, special characters are not printed
correctly. e.g. quotes become '

Steps to reproduce:
- Modify a product name with special characters
- Print a product label
- Select ZPL Labels

As the report is only rendered as text, the special characters are not
dangerous and can be printed as is.

opw-3684870

closes #155853

X-original-commit: 4c2e92d
Signed-off-by: Quentin Wolfs (quwo) <quwo@odoo.com>
  • Loading branch information
robinengels committed Mar 5, 2024
1 parent be656b6 commit b05ff00
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 29 deletions.
40 changes: 37 additions & 3 deletions addons/stock/report/product_label_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from odoo import _, models
from odoo.exceptions import UserError

import markupsafe

class ReportProductLabel(models.AbstractModel):
_name = 'report.stock.label_product_product_view'
Expand All @@ -22,11 +23,44 @@ def _get_report_values(self, docids, data):
quantity_by_product = defaultdict(list)
for p, q in data.get('quantity_by_product').items():
product = Product.browse(int(p))
quantity_by_product[product].append((product.barcode, q))
default_code_markup = markupsafe.Markup(product.default_code) if product.default_code else ''
product_info = {
'barcode': markupsafe.Markup(product.barcode),
'quantity': q,
'display_name_markup': markupsafe.Markup(product.display_name),
'default_code': (default_code_markup[:15], default_code_markup[15:30])
}
quantity_by_product[product].append(product_info)
if data.get('custom_barcodes'):
# we expect custom barcodes to be: {product: [(barcode, qty_of_barcode)]}
for product, barcodes_qtys in data.get('custom_barcodes').items():
quantity_by_product[Product.browse(int(product))] += (barcodes_qtys)
product = Product.browse(int(product))
default_code_markup = markupsafe.Markup(product.default_code) if product.default_code else ''
for barcode_qty in barcodes_qtys:
quantity_by_product[product].append({
'barcode': markupsafe.Markup(barcode_qty[0]),
'quantity': barcode_qty[1],
'display_name_markup': markupsafe.Markup(product.display_name),
'default_code': (default_code_markup[:15], default_code_markup[15:30])
}
)
data['quantity'] = quantity_by_product

return data


class ReportLotLabel(models.AbstractModel):
_name = 'report.stock.label_lot_template_view'
_description = 'Lot Label Report'

def _get_report_values(self, docids, data):
lots = self.env['stock.lot'].browse(docids)
lot_list = []
for lot in lots:
lot_list.append({
'display_name_markup': markupsafe.Markup(lot.product_id.display_name),
'name': markupsafe.Markup(lot.name),
'lot_record': lot
})
return {
'docs': lot_list,
}
33 changes: 16 additions & 17 deletions addons/stock/report/product_templates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@
<template id="label_product_product_view">
<t t-foreach="quantity.items()" t-as="barcode_and_qty_by_product">
<t t-set="product" t-value="barcode_and_qty_by_product[0]"/>
<t t-foreach="barcode_and_qty_by_product[1]" t-as="barcode_and_qty">
<t t-set="barcode" t-value="barcode_and_qty[0]"/>
<t t-foreach="range(barcode_and_qty[1])" t-as="qty">
<t t-foreach="barcode_and_qty_by_product[1]" t-as="product_info">
<t t-set="barcode" t-value="product_info['barcode']"/>
<t t-foreach="range(product_info['quantity'])" t-as="qty">
<t t-translation="off">
^XA
^FT100,80^A0N,40,30^FD<t t-esc="product.display_name"/>^FS
^XA^CI28
^FT100,80^A0N,40,30^FD<t t-out="product_info['display_name_markup']"/>^FS
<t t-if="product.default_code and len(product.default_code) &gt; 15">
^FT100,115^A0N,30,24^FD<t t-esc="product.default_code[:15]"/>^FS
^FT100,150^A0N,30,24^FD<t t-esc="product.default_code[15:30]"/>^FS
^FT100,115^A0N,30,24^FD<t t-out="product_info['default_code'][0]"/>^FS
^FT100,150^A0N,30,24^FD<t t-out="product_info['default_code'][1]"/>^FS
</t>
<t t-else="">
^FT100,150^A0N,30,24^FD<t t-esc="product.default_code"/>^FS
^FT100,150^A0N,30,24^FD<t t-out="product_info['default_code'][0]"/>^FS
</t>
<t t-if="price_included">
^FO600,100,1
^CI28
<t t-if="product.currency_id.position == 'after'">
^A0N,66,48^FH^FD<t t-esc="product.lst_price if 'lst_price' in product else product.list_price" t-options='{"widget": "float", "precision": 2}'/><t t-esc="product.currency_id.symbol"/>^FS
</t>
Expand All @@ -30,7 +29,7 @@
<t t-if="barcode">
^FO100,160^BY3
^BCN,100,Y,N,N
^FD<t t-esc="barcode"/>^FS
^FD<t t-out="barcode"/>^FS
</t>
^XZ
</t>
Expand All @@ -42,24 +41,24 @@
<template id="label_lot_template_view">
<t t-foreach="docs" t-as="lot">
<t t-translation="off">
^XA
^XA^CI28
^FO100,50
^A0N,44,33^FD<t t-out="lot.product_id.display_name"/>^FS
^A0N,44,33^FD<t t-out="lot['display_name_markup']"/>^FS
^FO100,100
^A0N,44,33^FDLN/SN: <t t-out="lot.name"/>^FS
^A0N,44,33^FDLN/SN: <t t-out="lot['name']"/>^FS
<t t-if="env.user.has_group('stock.group_stock_lot_print_gs1')">
<t t-if="lot.product_id.valid_ean" t-set="final_barcode" t-value="'01' + '0' * (14 - len(lot.product_id.barcode)) + lot.product_id.barcode"/>
<t t-if="lot['lot_record'].product_id.valid_ean" t-set="final_barcode" t-value="'01' + '0' * (14 - len(lot['lot_record'].product_id.barcode)) + lot['lot_record'].product_id.barcode"/>
<!-- TODO: must keep lot/sn as last value in barcode because we cannot pad '0's without changing lot/sn name until we can scan in FNC1. -->
<t t-if="lot.product_id.tracking == 'lot'" name="datamatrix_lot" t-set="final_barcode" t-value="(final_barcode or '') + '10' + lot.name"/>
<t t-elif="lot.product_id.tracking == 'serial'" t-set="final_barcode" t-value="(final_barcode or '') + '21' + lot.name"/>
<t t-if="lot['lot_record'].product_id.tracking == 'lot'" name="datamatrix_lot" t-set="final_barcode" t-value="(final_barcode or '') + '10' + lot['name']"/>
<t t-elif="lot['lot_record'].product_id.tracking == 'serial'" t-set="final_barcode" t-value="(final_barcode or '') + '21' + lot['name']"/>
^FO425,150^BY3
^BXN,8,200
^FD<t t-out="final_barcode"/>^FS
</t>
<t t-else="" name="code128_barcode">
^FO100,150^BY3
^BCN,100,Y,N,N
^FD<t t-out="lot.name"/>^FS
^FD<t t-out="lot['name']"/>^FS
</t>
^XZ
</t>
Expand Down
49 changes: 40 additions & 9 deletions addons/stock/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ def setUpClass(cls):
cls.supplier_location = cls.env['stock.location'].browse(cls.ModelDataObj._xmlid_to_res_id('stock.stock_location_suppliers'))
cls.stock_location = cls.env['stock.location'].browse(cls.ModelDataObj._xmlid_to_res_id('stock.stock_location_stock'))

cls.product1 = cls.env['product.product'].create({
'name': 'Mellohi"',
'type': 'product',
'categ_id': cls.env.ref('product.product_category_all').id,
'tracking': 'lot',
'default_code': 'C4181234""154654654654',
'barcode': 'scan""me'
})

product_form = Form(cls.env['product.product'])
product_form.detailed_type = 'product'
product_form.name = 'Product'
Expand All @@ -39,25 +48,47 @@ def get_report_forecast(self, product_template_ids=False, product_variant_ids=Fa


class TestReports(TestReportsCommon):
def test_reports(self):
product1 = self.env['product.product'].create({
'name': 'Mellohi',
'default_code': 'C418',

def test_product_label_reports(self):
""" Test that all the special characters are correctly rendered for the product name, the default code and the barcode.
In this test we test that the double quote is rendered correctly.
"""
report = self.env.ref('stock.label_product_product')
target = b'\n\n^XA^CI28\n^FT100,80^A0N,40,30^FD[C4181234""154654654654]Mellohi"^FS\n^FT100,115^A0N,30,24^FDC4181234""15465^FS\n^FT100,150^A0N,30,24^FD4654654^FS\n^FO100,160^BY3\n^BCN,100,Y,N,N\n^FDscan""me^FS\n^XZ\n\n\n^XA^CI28\n^FT100,80^A0N,40,30^FD[C4181234""154654654654]Mellohi"^FS\n^FT100,115^A0N,30,24^FDC4181234""15465^FS\n^FT100,150^A0N,30,24^FD4654654^FS\n^FO100,160^BY3\n^BCN,100,Y,N,N\n^FDscan""me^FS\n^XZ\n'
rendering, qweb_type = report._render_qweb_text(self.product1.product_tmpl_id.id, {'quantity_by_product': {self.product1.product_tmpl_id.id: 2}, 'active_model': 'product.template'})
self.assertEqual(target, rendering.replace(b' ', b''), 'Product name, default code or barcode is not correctly rendered, make sure the quotes are escaped correctly')
self.assertEqual(qweb_type, 'text', 'the report type is not good')

def test_product_label_custom_barcode_reports(self):
""" Test that the custom barcodes are correctly rendered with special characters."""
report = self.env.ref('stock.label_product_product')
target = b'\n\n^XA^CI28\n^FT100,80^A0N,40,30^FD[C4181234""154654654654]Mellohi"^FS\n^FT100,115^A0N,30,24^FDC4181234""15465^FS\n^FT100,150^A0N,30,24^FD4654654^FS\n^FO100,160^BY3\n^BCN,100,Y,N,N\n^FD123"barcode^FS\n^XZ\n\n\n^XA^CI28\n^FT100,80^A0N,40,30^FD[C4181234""154654654654]Mellohi"^FS\n^FT100,115^A0N,30,24^FDC4181234""15465^FS\n^FT100,150^A0N,30,24^FD4654654^FS\n^FO100,160^BY3\n^BCN,100,Y,N,N\n^FD123"barcode^FS\n^XZ\n\n\n^XA^CI28\n^FT100,80^A0N,40,30^FD[C4181234""154654654654]Mellohi"^FS\n^FT100,115^A0N,30,24^FDC4181234""15465^FS\n^FT100,150^A0N,30,24^FD4654654^FS\n^FO100,160^BY3\n^BCN,100,Y,N,N\n^FDbarcode"456^FS\n^XZ\n\n\n^XA^CI28\n^FT100,80^A0N,40,30^FD[C4181234""154654654654]Mellohi"^FS\n^FT100,115^A0N,30,24^FDC4181234""15465^FS\n^FT100,150^A0N,30,24^FD4654654^FS\n^FO100,160^BY3\n^BCN,100,Y,N,N\n^FDbarcode"456^FS\n^XZ\n'
rendering, qweb_type = report._render_qweb_text(self.product1.product_tmpl_id.id, {'custom_barcodes': {self.product1.product_tmpl_id.id: [('123"barcode', 2), ('barcode"456', 2)]}, 'quantity_by_product': {}, 'active_model': 'product.template'})
self.assertEqual(target, rendering.replace(b' ', b''), 'Custom barcodes are most likely not corretly rendered, make sure the quotes are escaped correctly')
self.assertEqual(qweb_type, 'text', 'the report type is not good')

def test_reports_with_special_characters(self):
product_test = self.env['product.product'].create({
'name': 'Mellohi"',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
'tracking': 'lot',
'barcode': 'scan_me'
'default_code': 'C4181234""154654654654',
'barcode': '9745213796142'
})

lot1 = self.env['stock.lot'].create({
'name': 'Volume-Beta',
'product_id': product1.id,
'name': 'Volume-Beta"',
'product_id': product_test.id,
'company_id': self.env.company.id,
})
#add group to the user
self.env.user.groups_id += self.env.ref('stock.group_stock_lot_print_gs1')
report = self.env.ref('stock.label_lot_template')
target = b'\n\n^XA\n^FO100,50\n^A0N,44,33^FD[C418]Mellohi^FS\n^FO100,100\n^A0N,44,33^FDLN/SN:Volume-Beta^FS\n^FO100,150^BY3\n^BCN,100,Y,N,N\n^FDVolume-Beta^FS\n^XZ\n'
target = b'\n\n^XA^CI28\n^FO100,50\n^A0N,44,33^FD[C4181234""154654654654]Mellohi"^FS\n^FO100,100\n^A0N,44,33^FDLN/SN:Volume-Beta"^FS\n\n^FO425,150^BY3\n^BXN,8,200\n^FD010974521379614210Volume-Beta"^FS\n^XZ\n'

rendering, qweb_type = report._render_qweb_text(lot1.id)
self.assertEqual(target, rendering.replace(b' ', b''), 'The rendering is not good')
self.assertEqual(target, rendering.replace(b' ', b''), 'The rendering is not good, make sure quotes are correctly escaped')
self.assertEqual(qweb_type, 'text', 'the report type is not good')

def test_report_quantity_1(self):
Expand Down

0 comments on commit b05ff00

Please sign in to comment.