[FIX] stock: fix text product label reports with special characters

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 odoo/odoo#156624

X-original-commit: 7875c91be5dd467374b39bc1a7d05e9f983fbaef
Signed-off-by: Quentin Wolfs (quwo) <quwo@odoo.com>
This commit is contained in:
roen-odoo
2024-03-06 16:10:54 +00:00
committed by clesgow
parent 375d1c1c8b
commit 531cb95e87
3 changed files with 95 additions and 29 deletions
+37 -2
View File
@@ -6,6 +6,7 @@ from collections import defaultdict
from odoo import _, models
from odoo.exceptions import UserError
import markupsafe
class ReportProductLabel(models.AbstractModel):
_name = 'report.stock.label_product_product_view'
@@ -22,13 +23,47 @@ class ReportProductLabel(models.AbstractModel):
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
layout_wizard = self.env['product.label.layout'].browse(data.get('layout_wizard'))
data['pricelist'] = layout_wizard.pricelist_id
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,
}
+16 -17
View File
@@ -4,23 +4,22 @@
<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="barcode_and_qty_by_product[1]" t-as="product_info">
<t t-set="barcode" t-value="product_info['barcode']"/>
<t t-set="currency_id" t-value="pricelist.currency_id or product.currency_id"/>
<t t-foreach="range(barcode_and_qty[1])" t-as="qty">
<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="currency_id.position == 'after'">
^A0N,66,48^FH^FD<t t-esc="pricelist._get_product_price(product, 1, currency_id)" t-options='{"widget": "float", "precision": 2}'/><t t-esc="currency_id.symbol"/>^FS
</t>
@@ -31,7 +30,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>
@@ -43,16 +42,16 @@
<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
@@ -60,7 +59,7 @@
<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>
+42 -10
View File
@@ -17,6 +17,15 @@ class TestReportsCommon(TransactionCase):
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'
@@ -39,24 +48,47 @@ class TestReportsCommon(TransactionCase):
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('stock.label_product_product', 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('stock.label_product_product', 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,
})
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'
#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^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 = self.env['ir.actions.report']._render_qweb_text('stock.label_lot_template', lot1.id)
self.assertEqual(target, rendering.replace(b' ', b''), 'The rendering is not good')
rendering, qweb_type = report._render_qweb_text('stock.label_lot_template', lot1.id)
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):