Initial commit
This commit is contained in:
7
garazd_product_label_pro/models/__init__.py
Normal file
7
garazd_product_label_pro/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from . import print_product_label_template
|
||||
from . import print_product_label_section
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
from . import product_product
|
||||
from . import product_template
|
||||
719
garazd_product_label_pro/models/print_product_label_section.py
Normal file
719
garazd_product_label_pro/models/print_product_label_section.py
Normal file
@@ -0,0 +1,719 @@
|
||||
# Copyright © 2023 Garazd Creation (https://garazd.biz)
|
||||
# @author: Yurii Razumovskyi (support@garazd.biz)
|
||||
# @author: Iryna Razumovska (support@garazd.biz)
|
||||
# License OPL-1 (https://www.odoo.com/documentation/15.0/legal/licenses.html).
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
from odoo import _, _lt, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class PrintProductLabelSection(models.Model):
|
||||
_name = "print.product.label.section"
|
||||
_description = 'Template Sections of Product Labels'
|
||||
_order = 'sequence'
|
||||
|
||||
def _default_sequence(self):
|
||||
return (self.search([], order="sequence desc", limit=1).sequence or 0) + 1
|
||||
|
||||
sequence = fields.Integer(default=_default_sequence)
|
||||
template_id = fields.Many2one(
|
||||
comodel_name='print.product.label.template',
|
||||
string='Template',
|
||||
ondelete='cascade',
|
||||
required=True,
|
||||
)
|
||||
template_preview_html = fields.Html(compute='_compute_template_preview_html')
|
||||
preview = fields.Boolean(default=True, help='Show the sample label.')
|
||||
type = fields.Selection(
|
||||
selection=[
|
||||
('text', 'Text'),
|
||||
('field', 'Model Field'),
|
||||
('price', 'Price'),
|
||||
('promo_price', 'Promo Price'),
|
||||
('multi_price', 'Multiple prices (by quantity)'),
|
||||
('product_attributes', 'Product Attributes'),
|
||||
('image', 'Image'),
|
||||
],
|
||||
default='text',
|
||||
required=True,
|
||||
)
|
||||
value = fields.Char()
|
||||
value_format = fields.Char(string='Format', help='Format for date and digit fields.')
|
||||
value_prefix = fields.Char()
|
||||
value_suffix = fields.Char()
|
||||
image = fields.Binary(attachment=True)
|
||||
field_id = fields.Many2one(
|
||||
comodel_name='ir.model.fields',
|
||||
string='Field',
|
||||
ondelete='cascade',
|
||||
domain="[('id', 'in', field_ids)]",
|
||||
)
|
||||
field_name = fields.Char(related='field_id.name')
|
||||
field_ids = fields.Many2many(
|
||||
comodel_name='ir.model.fields',
|
||||
string='Available Fields',
|
||||
help='Technical field for a domain',
|
||||
compute='_compute_field_ids',
|
||||
)
|
||||
field_ttype = fields.Selection(related='field_id.ttype')
|
||||
relation_model_id = fields.Many2one(
|
||||
comodel_name='ir.model',
|
||||
compute='_compute_relation_model_id',
|
||||
)
|
||||
relation_field_id = fields.Many2one(
|
||||
comodel_name='ir.model.fields',
|
||||
help='The first level of the relation field. Allow you to select related fields'
|
||||
' in case when you have chosen the "Field" with the type "many2one".',
|
||||
)
|
||||
relation_field_ttype = fields.Selection(
|
||||
related='relation_field_id.ttype',
|
||||
string="Relation Field Type",
|
||||
)
|
||||
nested_relation_model_id = fields.Many2one(
|
||||
comodel_name='ir.model',
|
||||
compute='_compute_nested_relation_model_id',
|
||||
)
|
||||
nested_relation_field_id = fields.Many2one(
|
||||
comodel_name='ir.model.fields',
|
||||
help='The second level of relation field. Allow you to select related fields in '
|
||||
'case when you have chosen the "Relation Field" with the type "many2one".',
|
||||
)
|
||||
height = fields.Float(
|
||||
string='Height, mm', digits=(10, 2), help='Section Height in mm.',
|
||||
)
|
||||
width = fields.Float(digits=(10, 2), help='Section Width.')
|
||||
width_measure = fields.Selection(
|
||||
selection=[
|
||||
('%', 'Percents'),
|
||||
('mm', 'mm'),
|
||||
],
|
||||
default='%',
|
||||
required=True,
|
||||
)
|
||||
position = fields.Selection(
|
||||
selection=[
|
||||
('none', 'Full Width'),
|
||||
('left', 'Left Side'),
|
||||
('right', 'Right Side'),
|
||||
],
|
||||
string='Float',
|
||||
default='none',
|
||||
required=True,
|
||||
)
|
||||
line_height = fields.Float(digits=(10, 2), default=1.0, help='Section Line Height Ratio.')
|
||||
align = fields.Selection(
|
||||
selection=[
|
||||
('left', 'Left'),
|
||||
('center', 'Center'),
|
||||
('right', 'Right'),
|
||||
('justify', 'Justify'),
|
||||
],
|
||||
default='center',
|
||||
required=True,
|
||||
)
|
||||
font_name = fields.Selection(
|
||||
selection=[
|
||||
('Lato', 'Lato'),
|
||||
('Roboto', 'Roboto'),
|
||||
('Open_Sans', 'Open Sans'),
|
||||
('Montserrat', 'Montserrat'),
|
||||
('Oswald', 'Oswald'),
|
||||
('Raleway', 'Raleway'),
|
||||
('Tajawal', 'Tajawal'),
|
||||
('Fira_Mono', 'Fira Mono'),
|
||||
],
|
||||
string='Font',
|
||||
help='Specify the name of a custom font for this label section.',
|
||||
)
|
||||
font_size = fields.Float(digits=(10, 2), default=12, required=True)
|
||||
font_size_measure = fields.Selection(
|
||||
selection=[('px', 'Pixels'), ('mm', 'mm')],
|
||||
string='Measure',
|
||||
default='px',
|
||||
required=True,
|
||||
)
|
||||
font_weight = fields.Selection(
|
||||
selection=[
|
||||
('100', '100'),
|
||||
('normal', 'normal'),
|
||||
('bold', 'bold'),
|
||||
('900', '900'),
|
||||
],
|
||||
default='normal',
|
||||
required=True,
|
||||
)
|
||||
letter_spacing = fields.Float(digits=(10, 2), help='Space between letters, in mm.')
|
||||
text_decoration = fields.Selection(
|
||||
selection=[
|
||||
('line-through', 'Line Through'),
|
||||
('none', 'None'),
|
||||
],
|
||||
default='none',
|
||||
)
|
||||
text_color = fields.Char(default='#000000')
|
||||
widget = fields.Selection(
|
||||
selection=[
|
||||
('price', 'Price'),
|
||||
('barcode', 'Barcode'),
|
||||
('qr_code', 'QR code'),
|
||||
('image', 'Image'),
|
||||
('attribute_list', 'Attribute Values'),
|
||||
],
|
||||
)
|
||||
with_product_attribute_name = fields.Boolean(
|
||||
string='With Attribute Name',
|
||||
help='Place a product attribute name before the attribute value on labels.',
|
||||
)
|
||||
attribute_ids = fields.Many2many(
|
||||
comodel_name='product.attribute',
|
||||
string='Allowed Attributes',
|
||||
help='Specify product attributes to show. '
|
||||
'If no one is selected, all attribute that are related to a product will be shown.'
|
||||
)
|
||||
barcode_is_humanreadable = fields.Boolean(string='Human-readable')
|
||||
barcode_symbology = fields.Selection(
|
||||
selection=[
|
||||
('auto', 'Auto'),
|
||||
('EAN8', 'EAN8'),
|
||||
('EAN13', 'EAN13'),
|
||||
('Code128', 'Code128'),
|
||||
],
|
||||
string='Symbology',
|
||||
default='auto',
|
||||
required=True,
|
||||
)
|
||||
shorten_url = fields.Boolean(
|
||||
string='Shorten URL',
|
||||
help='If the section value is a URL, you can short it with the Odoo internal '
|
||||
'link tracker to get a link like this one: https://your-domain/r/aBc.',
|
||||
)
|
||||
make_url_absolute = fields.Boolean(
|
||||
string='Absolute URL',
|
||||
help="If the section value is a URL, and it's a relative, you can make it "
|
||||
"absolute and add your base domain by activating this option.",
|
||||
)
|
||||
currency_position = fields.Selection(
|
||||
selection=[
|
||||
('default', 'By default'),
|
||||
('before', 'Before price'),
|
||||
('after', 'After price'),
|
||||
('none', 'Without currency code'),
|
||||
],
|
||||
default='default',
|
||||
required=True,
|
||||
)
|
||||
multi_price_limit = fields.Integer(
|
||||
help='Specify the limit to restrict a number of prices.',
|
||||
default=10,
|
||||
)
|
||||
multi_price_order = fields.Selection(
|
||||
selection=[
|
||||
('desc', 'In descending order'),
|
||||
('asc', 'In ascending order'),
|
||||
],
|
||||
string='Price Order',
|
||||
default='desc',
|
||||
required=True,
|
||||
)
|
||||
padding_top = fields.Float(digits=(10, 2), help='Page Right Padding, in mm.')
|
||||
padding_bottom = fields.Float(digits=(10, 2), help='Page Bottom Padding, in mm.')
|
||||
padding_left = fields.Float(digits=(10, 2), help='Page Left Padding, in mm.')
|
||||
padding_right = fields.Float(digits=(10, 2), help='Page Right Padding, in mm.')
|
||||
margin_top = fields.Float(digits=(10, 2), help='Page Right Margin, in mm.')
|
||||
margin_bottom = fields.Float(digits=(10, 2), help='Page Bottom Margin, in mm.')
|
||||
margin_left = fields.Float(digits=(10, 2), help='Page Left Margin, in mm.')
|
||||
margin_right = fields.Float(digits=(10, 2), help='Page Right Margin, in mm.')
|
||||
with_border_top = fields.Boolean(string="Border Top")
|
||||
with_border_bottom = fields.Boolean(string="Border Bottom")
|
||||
with_border_left = fields.Boolean(string="Border Left")
|
||||
with_border_right = fields.Boolean(string="Border Right")
|
||||
border_width = fields.Integer(default=1, help='Border Width, in px')
|
||||
with_background = fields.Boolean(string="Background")
|
||||
background_color = fields.Char(default='#BBBBBB')
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.constrains('height')
|
||||
def _check_height(self):
|
||||
for section in self:
|
||||
if not section.height:
|
||||
raise ValidationError(_('The section height must be set.'))
|
||||
|
||||
@api.constrains('type', 'widget')
|
||||
def _check_widget_image(self):
|
||||
for section in self:
|
||||
if section.type != 'field' and section.widget == 'image':
|
||||
raise ValidationError(_('You can use the widget "Image" only for the "Model Fields" section types.'))
|
||||
|
||||
@api.depends('type')
|
||||
def _compute_field_ids(self):
|
||||
for section in self:
|
||||
if section.type == 'field':
|
||||
domain = [('model', '=', 'print.product.label.line')] + self._get_field_domain()
|
||||
available_fields = self.env['ir.model.fields'].search(domain)
|
||||
section.field_ids = [(6, 0, available_fields.ids)]
|
||||
else:
|
||||
section.field_ids = None
|
||||
|
||||
@api.depends('type', 'field_id')
|
||||
def _compute_relation_model_id(self):
|
||||
for section in self:
|
||||
if section.type == 'field' and section.field_id.ttype in self.relation_field_types():
|
||||
section.relation_model_id = self.env['ir.model'].search([
|
||||
('model', '=', section.field_id.relation)
|
||||
])[:1].id
|
||||
else:
|
||||
section.relation_model_id = None
|
||||
|
||||
@api.depends('type', 'field_id', 'relation_field_id')
|
||||
def _compute_nested_relation_model_id(self):
|
||||
for section in self:
|
||||
if (
|
||||
section.type == 'field'
|
||||
and section.field_id
|
||||
and section.field_id.ttype in self.relation_field_types()
|
||||
and section.relation_field_id
|
||||
and section.relation_field_id.ttype in self.relation_field_types()
|
||||
):
|
||||
section.nested_relation_model_id = self.env['ir.model'].search([
|
||||
('model', '=', section.relation_field_id.relation),
|
||||
])[:1].id
|
||||
else:
|
||||
section.nested_relation_model_id = None
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_template_preview_html(self):
|
||||
for section in self:
|
||||
# flake8: noqa: E501
|
||||
section.template_preview_html = section.template_id.with_context(editable_section_id=section.id).preview_html
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
for section in self:
|
||||
if section.type != 'field':
|
||||
section.field_id = section.relation_field_id = section.nested_relation_field_id = False
|
||||
|
||||
@api.onchange('field_id')
|
||||
def _onchange_field_id(self):
|
||||
for section in self:
|
||||
if section.relation_field_id.model_id != section.relation_model_id:
|
||||
section.relation_field_id = section.nested_relation_field_id = False
|
||||
|
||||
@api.onchange('relation_field_id')
|
||||
def _onchange_relation_field_id(self):
|
||||
for section in self:
|
||||
if section.nested_relation_field_id.model_id != section.nested_relation_model_id:
|
||||
section.nested_relation_field_id = False
|
||||
|
||||
@api.onchange('type', 'field_id', 'widget')
|
||||
def _onchange_widget(self):
|
||||
for section in self:
|
||||
# Reset the "Price" widget
|
||||
if section.widget == 'price' and section.field_id != self.env.ref(
|
||||
'garazd_product_label_pro.field_print_product_label_line__price'):
|
||||
section.widget = False
|
||||
# Reset the value format
|
||||
section.value_format = False
|
||||
|
||||
@api.model
|
||||
def binary_field_types(self):
|
||||
return ['binary']
|
||||
|
||||
@api.model
|
||||
def text_field_types(self):
|
||||
return ['char', 'text', 'html', 'selection']
|
||||
|
||||
@api.model
|
||||
def digit_field_types(self):
|
||||
return ['float', 'monetary', 'integer']
|
||||
|
||||
@api.model
|
||||
def date_field_types(self):
|
||||
return ['date', 'datetime']
|
||||
|
||||
@api.model
|
||||
def non_relation_field_types(self):
|
||||
return self.binary_field_types() + self.text_field_types() \
|
||||
+ self.digit_field_types() + self.date_field_types()
|
||||
|
||||
@api.model
|
||||
def relation_field_types(self):
|
||||
return ['many2one']
|
||||
|
||||
@api.model
|
||||
def multi_relation_field_types(self):
|
||||
return ['many2many', 'one2many']
|
||||
|
||||
@api.model
|
||||
def _get_field_domain(self):
|
||||
return [('ttype', 'in', self.non_relation_field_types() + self.relation_field_types())] # flake8: noqa: E501
|
||||
|
||||
def get_float_position(self):
|
||||
self.ensure_one()
|
||||
width = 100 if self.width_measure == '%' and self.width > 100 else self.width
|
||||
return "width: %(width).2f%(measure)s; float: %(float)s;" % {
|
||||
'width': width,
|
||||
'measure': self.width_measure,
|
||||
'float': self.position,
|
||||
}
|
||||
|
||||
def get_barcode_size(self) -> Dict[str, int]:
|
||||
self.ensure_one()
|
||||
# flake8: noqa: E501
|
||||
multiplier = int(self.env['ir.config_parameter'].sudo().get_param('garazd_product_label_pro.barcode_multiplier', 1))
|
||||
return {'width': 600 * multiplier, 'height': 100 * multiplier}
|
||||
|
||||
def get_border_style(self):
|
||||
self.ensure_one()
|
||||
border_style = ''
|
||||
for side in ['top', 'bottom', 'left', 'right']:
|
||||
if self['with_border_%s' % side]:
|
||||
border_style += 'border-%(side)s: %(width)dpx solid #000; ' % {
|
||||
'side': side,
|
||||
'width': self.border_width,
|
||||
}
|
||||
return border_style
|
||||
|
||||
def get_background_style(self):
|
||||
self.ensure_one()
|
||||
bg_style = ''
|
||||
if self.with_background and self.background_color:
|
||||
bg_style += f'background-color: {self.background_color}; '
|
||||
return bg_style
|
||||
|
||||
def get_font_name(self) -> str:
|
||||
self.ensure_one()
|
||||
return self.font_name.replace('_', ' ') if self.font_name else ''
|
||||
|
||||
def get_attribute_data(self, label=None) -> List[Dict]:
|
||||
self.ensure_one()
|
||||
section = self
|
||||
attribute_data = []
|
||||
if label:
|
||||
product = label.product_id
|
||||
for attribute in product.product_tmpl_id.attribute_line_ids.mapped('attribute_id'):
|
||||
# Process only allowed attributes
|
||||
if section.attribute_ids and attribute not in section.attribute_ids:
|
||||
continue
|
||||
|
||||
attr_vals = {'name': attribute.name, 'values': []}
|
||||
attribute_values = product.product_tmpl_id.attribute_line_ids.value_ids.filtered(
|
||||
lambda av: av.attribute_id == attribute
|
||||
)
|
||||
for val in attribute_values:
|
||||
value_data = {'name': val.name, 'active': False}
|
||||
if val in product.product_template_attribute_value_ids.mapped('product_attribute_value_id'):
|
||||
value_data['active'] = True
|
||||
attr_vals['values'].append(value_data)
|
||||
attribute_data.append(attr_vals)
|
||||
return attribute_data
|
||||
|
||||
def get_html_style(self, label=None) -> str:
|
||||
self.ensure_one()
|
||||
style = "overflow: hidden; " \
|
||||
"height: %(height).2fmm; " \
|
||||
"padding: %(padding_top).2fmm %(padding_right).2fmm" \
|
||||
" %(padding_bottom).2fmm %(padding_left).2fmm; " \
|
||||
"margin: %(margin_top).2fmm %(margin_right).2fmm" \
|
||||
" %(margin_bottom).2fmm %(margin_left).2fmm; " \
|
||||
"text-align: %(align)s; " \
|
||||
"font-size: %(font_size)s; " \
|
||||
"color: %(text_color)s; " \
|
||||
"line-height: %(line_height).2f; " \
|
||||
"letter-spacing: %(letter_spacing).2fmm; " \
|
||||
"font-weight: %(font_weight)s; "\
|
||||
"text-decoration: %(text_decoration)s;" % {
|
||||
'height': self.height,
|
||||
'align': self.align,
|
||||
'font_size': '%.2f%s' % (self.font_size, self.font_size_measure),
|
||||
'text_color': self.text_color,
|
||||
'line_height': self.line_height,
|
||||
'letter_spacing': self.letter_spacing,
|
||||
'font_weight': self.font_weight,
|
||||
'padding_top': self.padding_top,
|
||||
'padding_right': self.padding_right,
|
||||
'padding_bottom': self.padding_bottom,
|
||||
'padding_left': self.padding_left,
|
||||
'margin_top': self.margin_top,
|
||||
'margin_right': self.margin_right,
|
||||
'margin_bottom': self.margin_bottom,
|
||||
'margin_left': self.margin_left,
|
||||
'text_decoration': self.text_decoration or 'none',
|
||||
}
|
||||
# Section width settings
|
||||
style += "clear: both;" if self.position == 'none' else self.get_float_position()
|
||||
# Section borders
|
||||
style += self.get_border_style()
|
||||
# Section background
|
||||
style += self.get_background_style()
|
||||
# Hide the crossed regular price section if a price and the promo price are equal
|
||||
# pylint: disable-msg=too-many-boolean-expressions
|
||||
if ((self.type == 'price' or self.type == 'field' and self.field_name == 'price')
|
||||
and 'promo_price' in self.template_id.section_ids.mapped('type')
|
||||
and label and float_compare(label.price, label.promo_price, precision_digits=2) == 0
|
||||
):
|
||||
style += "opacity: 0;"
|
||||
# Section font family
|
||||
if self.font_name:
|
||||
style += f'font-family: "{self.get_font_name()}";'
|
||||
return style
|
||||
|
||||
def _format_digit_value(self, value) -> str:
|
||||
self.ensure_one()
|
||||
try:
|
||||
digit_value = ('%s' % (self.value_format or '%s')) % value
|
||||
except (ValueError, TypeError):
|
||||
digit_value = 'ERROR! Check the format'
|
||||
return digit_value
|
||||
|
||||
def _get_field_value(self, record, field) -> str:
|
||||
"""Return a value of the "field" for the "record"."""
|
||||
self.ensure_one()
|
||||
section = self
|
||||
|
||||
# Retrieving a value
|
||||
if field.ttype == 'selection':
|
||||
selection = record.fields_get([field.name])[field.name].get('selection', [])
|
||||
vals = dict(selection)
|
||||
value = vals.get(record[field.name])
|
||||
else:
|
||||
value = record[field.name]
|
||||
|
||||
# Format empty values
|
||||
value = value if value is not False else ''
|
||||
|
||||
# Format values for the digit and date fields
|
||||
if value and not section.widget:
|
||||
|
||||
# Format values for the digit fields
|
||||
if field.ttype in self.digit_field_types():
|
||||
value = section._format_digit_value(value)
|
||||
|
||||
# Format values for the date fields
|
||||
elif field.ttype in ['date', 'datetime']:
|
||||
lang = self.env['res.lang']._lang_get(self.env.lang) or self.env.ref('base.lang_en')
|
||||
value = value.strftime(section.value_format or lang.date_format)
|
||||
|
||||
return value
|
||||
|
||||
@api.model
|
||||
def process_price_value(self, label, price: float, currency=None) -> float:
|
||||
"""
|
||||
Post-processing of the price value before converting to the string.
|
||||
Method to override.
|
||||
"""
|
||||
return price
|
||||
|
||||
def _get_price_value(self, label, pricelist=False, min_quantity=1.0) -> str:
|
||||
self.ensure_one()
|
||||
section = self
|
||||
product = label.product_id
|
||||
if pricelist:
|
||||
price = pricelist._get_product_price(product, min_quantity)
|
||||
else:
|
||||
price = label.price
|
||||
currency = pricelist.currency_id if pricelist else label.currency_id
|
||||
|
||||
# Post-processing before converting to the string
|
||||
value = self.process_price_value(label, price, currency=currency)
|
||||
|
||||
# Converting to the string
|
||||
value = section._format_digit_value(value)
|
||||
|
||||
if section.currency_position != 'none':
|
||||
# flake8: noqa: E501
|
||||
before = section.currency_position == 'before' or section.currency_position == 'default' and currency.position == 'before'
|
||||
if before:
|
||||
value = '%s %s' % (currency.symbol, value)
|
||||
else:
|
||||
value = '%s %s' % (value, currency.symbol)
|
||||
return value
|
||||
|
||||
@api.model
|
||||
def get_pricelist_items(self, product, pricelist, sort_reverse=False):
|
||||
"""Collect all pricelist rules that affect the current product."""
|
||||
price_rules = pricelist.item_ids.filtered(
|
||||
lambda l: l.product_id == product and l.min_quantity
|
||||
)
|
||||
price_rules |= pricelist.item_ids.filtered(
|
||||
lambda l: l.product_tmpl_id == product.product_tmpl_id
|
||||
and not l.product_id and l.min_quantity
|
||||
)
|
||||
price_rules |= self.env['product.pricelist.item'].search([
|
||||
('pricelist_id', '=', pricelist.id),
|
||||
('categ_id', 'parent_of', product.categ_id.id),
|
||||
('min_quantity', '!=', 0),
|
||||
])
|
||||
# Remove rules with duplicated min quantity values
|
||||
res = self.env['product.pricelist.item'].browse()
|
||||
for rule in price_rules:
|
||||
if rule.min_quantity not in res.mapped('min_quantity'):
|
||||
res += rule
|
||||
return res.sorted('min_quantity', reverse=sort_reverse)
|
||||
|
||||
@api.model
|
||||
def get_short_url(self, url: str, title: str) -> str:
|
||||
# Search a short link, if it exists
|
||||
link = self.env['link.tracker'].search([('url', '=', url if url.startswith('http') else f'http://{url}')])
|
||||
if not link:
|
||||
# Create a new short link, if it does not exist
|
||||
link = self.env['link.tracker'].sudo().create({
|
||||
'url': url,
|
||||
'title': title,
|
||||
})
|
||||
return link.short_url
|
||||
|
||||
def get_value(self, label):
|
||||
"""Return value for a section depending on label.
|
||||
:param label: record of "print.product.label.line" model
|
||||
:return: str
|
||||
"""
|
||||
# flake8: noqa: E501
|
||||
# pylint: disable=too-many-branches
|
||||
self.ensure_one()
|
||||
section = self.sudo() # Add sudo to allow users without "Administration/Access Rights" generate labels
|
||||
value = ''
|
||||
allowed_text_field_types = self.text_field_types() + self.digit_field_types() + self.date_field_types()
|
||||
allowed_relation_field_types = self.relation_field_types()
|
||||
|
||||
# There are three relation levels:
|
||||
# Level 0 - the product label level
|
||||
# Level 1 - the level of relation field of the product label model
|
||||
# Level 2 - the level of relation field of the relation field
|
||||
|
||||
if section.type == 'text':
|
||||
value = section.value or ''
|
||||
|
||||
elif section.type == 'price':
|
||||
value = section._get_price_value(label, label.wizard_id.pricelist_id)
|
||||
|
||||
elif section.type == 'promo_price' and label.wizard_id.sale_pricelist_id:
|
||||
value = section._get_price_value(label, label.wizard_id.sale_pricelist_id)
|
||||
|
||||
elif section.type == 'multi_price' and label.wizard_id.pricelist_id:
|
||||
pricelist = label.wizard_id.pricelist_id
|
||||
qty_prices = []
|
||||
for pl_rule in self.get_pricelist_items(
|
||||
label.product_id, pricelist,
|
||||
sort_reverse=section.multi_price_order == 'asc',
|
||||
)[:section.multi_price_limit]:
|
||||
qty_prices.append({
|
||||
'qty': pl_rule.min_quantity,
|
||||
'amount': section._get_price_value(label, label.wizard_id.pricelist_id, min_quantity=pl_rule.min_quantity),
|
||||
'currency': pricelist.currency_id.symbol,
|
||||
})
|
||||
value = qty_prices
|
||||
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
elif section.type == 'field' and section.field_id:
|
||||
|
||||
# Level 0
|
||||
# Text and digit fields
|
||||
if section.field_ttype in allowed_text_field_types:
|
||||
value = section._get_field_value(label, section.field_id)
|
||||
# Relation fields
|
||||
elif section.relation_field_id and section.field_id.ttype in allowed_relation_field_types:
|
||||
|
||||
# Level 1
|
||||
record = label[section.field_id.name]
|
||||
if record:
|
||||
# Text and digit fields of the relation field
|
||||
if section.relation_field_ttype in allowed_text_field_types:
|
||||
value = section._get_field_value(record, section.relation_field_id)
|
||||
|
||||
# Nested relation fields of the relation field
|
||||
elif section.relation_field_ttype in allowed_relation_field_types:
|
||||
|
||||
# Level 2
|
||||
nested_record = record[section.relation_field_id.name]
|
||||
if nested_record:
|
||||
# Text and digit fields of the relation field
|
||||
if section.nested_relation_field_id.ttype in allowed_text_field_types:
|
||||
value = section._get_field_value(
|
||||
nested_record, section.nested_relation_field_id
|
||||
)
|
||||
|
||||
# Post-processing
|
||||
|
||||
# Prefix and Suffix
|
||||
if value and section.value_prefix:
|
||||
value = f"{section.value_prefix}{value}"
|
||||
if value and section.value_suffix:
|
||||
value = f"{value}{section.value_suffix}"
|
||||
|
||||
# Make a URL an absolute, if it's relative
|
||||
if value and section.make_url_absolute and not value.startswith('http'):
|
||||
value = f"{section.get_base_url().rstrip('/')}{value}"
|
||||
|
||||
if value and section.shorten_url and not self._context.get('preview_mode'):
|
||||
# Generate a shorten URL
|
||||
value = self.get_short_url(value, "%s - %s" % (section.template_id.name, section.display_name))
|
||||
|
||||
return value
|
||||
|
||||
def get_image_url(self, label) -> str:
|
||||
"""
|
||||
Return URL for a section binary field depending on label.
|
||||
:param label: record of "print.product.label.line" model
|
||||
:return: str
|
||||
"""
|
||||
self.ensure_one()
|
||||
section = self.sudo() # Add sudo to allow users without "Administration/Access Rights" generate labels
|
||||
res = ''
|
||||
if (
|
||||
section.type != 'field'
|
||||
or section.relation_field_ttype != 'many2one' and (
|
||||
not section.relation_model_id
|
||||
or not section.relation_field_id or section.relation_field_id.ttype != 'binary'
|
||||
)
|
||||
# Nested fields
|
||||
or section.relation_field_ttype == 'many2one' and (
|
||||
not section.nested_relation_model_id
|
||||
or not section.nested_relation_field_id or section.nested_relation_field_id.ttype != 'binary'
|
||||
)
|
||||
):
|
||||
return res
|
||||
|
||||
model_name = section.relation_model_id.model
|
||||
field_name = section.field_id.name
|
||||
record = label[field_name]
|
||||
relation_field_name = section.relation_field_id.name
|
||||
|
||||
# Nested fields
|
||||
if section.relation_field_ttype == 'many2one' and section.nested_relation_field_id.ttype == 'binary':
|
||||
model_name = section.nested_relation_field_id.model
|
||||
record = label[field_name][section.relation_field_id.name]
|
||||
relation_field_name = section.nested_relation_field_id.name
|
||||
|
||||
if record:
|
||||
res = '/web/image/%s/%d/%s' % (model_name, record.id, relation_field_name)
|
||||
return res
|
||||
|
||||
@api.depends('type', 'value', 'field_id', 'field_id.field_description',
|
||||
'relation_field_id', 'relation_field_id.field_description', 'widget')
|
||||
def _compute_display_name(self):
|
||||
for section in self:
|
||||
section.display_name = "%s%s" % (
|
||||
section.type == 'text'
|
||||
and (section.value and f"{_lt('Text:')} {section.value}" or _lt('Blank'))
|
||||
or section.type == 'price' and _lt('Price')
|
||||
or section.type == 'promo_price' and _lt('Promo Price')
|
||||
or section.type == 'multi_price' and _lt('Multiple prices (by qty)')
|
||||
or section.type == 'product_attributes' and (_('Product Attributes%s') % (
|
||||
f" [{', '.join(attr.name for attr in section.attribute_ids)}]" if section.attribute_ids else ''
|
||||
))
|
||||
or section.type == 'image' and _lt('Image')
|
||||
or section.type == 'field'
|
||||
and "%s %s" % (
|
||||
section.field_id.field_description,
|
||||
section.relation_field_id
|
||||
and section.relation_field_id.field_description or '',
|
||||
),
|
||||
section.widget and (" (widget: %s)" % section.widget) or '',
|
||||
)
|
||||
|
||||
def action_pdf_preview(self):
|
||||
self.ensure_one()
|
||||
return self.template_id.action_pdf_preview()
|
||||
269
garazd_product_label_pro/models/print_product_label_template.py
Normal file
269
garazd_product_label_pro/models/print_product_label_template.py
Normal file
@@ -0,0 +1,269 @@
|
||||
# Copyright © 2023 Garazd Creation (https://garazd.biz)
|
||||
# @author: Yurii Razumovskyi (support@garazd.biz)
|
||||
# @author: Iryna Razumovska (support@garazd.biz)
|
||||
# License OPL-1 (https://www.odoo.com/documentation/15.0/legal/licenses.html).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import float_round
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class PrintProductLabelTemplate(models.Model):
|
||||
_name = "print.product.label.template"
|
||||
_description = 'Product Label Templates'
|
||||
_order = 'sequence'
|
||||
|
||||
def _default_sequence(self):
|
||||
return (self.search([], order="sequence desc", limit=1).sequence or 0) + 1
|
||||
|
||||
sequence = fields.Integer(default=_default_sequence)
|
||||
name = fields.Char(required=True)
|
||||
type_id = fields.Many2one(
|
||||
comodel_name='print.label.type',
|
||||
default=lambda self: self.env.ref('garazd_product_label.type_product'),
|
||||
help='You can specify a type of this label. It makes sense if you use '
|
||||
'additional extensions to print labels not for products only but for other objects as well. '
|
||||
'Like as Stock Packages, Sales Orders, Manufacturing Orders, etc. '
|
||||
'By this type, you can filter your label templates in the print wizard.',
|
||||
ondelete='set null',
|
||||
required=False,
|
||||
)
|
||||
section_ids = fields.One2many(
|
||||
comodel_name='print.product.label.section',
|
||||
inverse_name='template_id',
|
||||
string='Sections',
|
||||
)
|
||||
section_count = fields.Integer(compute='_compute_section_count')
|
||||
paperformat_id = fields.Many2one(
|
||||
comodel_name='report.paperformat',
|
||||
# All label paperformats should have prefix "Label: "
|
||||
domain="[('name', 'like', 'Label: %')]",
|
||||
readonly=True,
|
||||
required=True,
|
||||
)
|
||||
format = fields.Selection(related='paperformat_id.format', store=True)
|
||||
orientation = fields.Selection(
|
||||
related='paperformat_id.orientation',
|
||||
readonly=False,
|
||||
required=True,
|
||||
help='Page Orientation. Only system administrators can change this value.',
|
||||
)
|
||||
rows = fields.Integer(default=1, required=True)
|
||||
cols = fields.Integer(default=1, required=True)
|
||||
margin_top = fields.Float(
|
||||
related='paperformat_id.margin_top',
|
||||
help='Page Top Margin in mm. Only system administrators can change this value.',
|
||||
readonly=False,
|
||||
)
|
||||
margin_bottom = fields.Float(
|
||||
related='paperformat_id.margin_bottom',
|
||||
help='Page Bottom Margin in mm. '
|
||||
'Only system administrators can change this value.',
|
||||
readonly=False,
|
||||
)
|
||||
margin_left = fields.Float(
|
||||
related='paperformat_id.margin_left',
|
||||
help='Page Left Margin in mm. Only system administrators can change this value.',
|
||||
readonly=False,
|
||||
)
|
||||
margin_right = fields.Float(
|
||||
related='paperformat_id.margin_right',
|
||||
help='Page Right Margin in mm. '
|
||||
'Only system administrators can change this value.',
|
||||
readonly=False,
|
||||
)
|
||||
padding_top = fields.Float(
|
||||
default=0, digits=(10, 2), help='Label Right Padding in mm.')
|
||||
padding_bottom = fields.Float(
|
||||
default=0, digits=(10, 2), help='Label Bottom Padding in mm.')
|
||||
padding_left = fields.Float(
|
||||
default=0, digits=(10, 2), help='Label Left Padding in mm.')
|
||||
padding_right = fields.Float(
|
||||
default=0, digits=(10, 2), help='Label Right Padding in mm.')
|
||||
label_style = fields.Char(string='Custom Label Style')
|
||||
width = fields.Float(digits=(10, 2), help='Label Width in mm.')
|
||||
height = fields.Float(digits=(10, 2), help='Label Height in mm.')
|
||||
row_gap = fields.Float(
|
||||
string='Horizontal',
|
||||
digits=(10, 2),
|
||||
default=0,
|
||||
help='Horizontal gap between labels, in mm.',
|
||||
)
|
||||
col_gap = fields.Float(
|
||||
string='Vertical',
|
||||
digits=(10, 2),
|
||||
default=0,
|
||||
help='Vertical gap between labels, in mm.',
|
||||
)
|
||||
is_oversized = fields.Boolean(compute='_compute_is_oversized')
|
||||
description = fields.Char()
|
||||
preview = fields.Boolean(default=True, help='Show the sample label.')
|
||||
preview_html = fields.Html(compute='_compute_preview_html', compute_sudo=True)
|
||||
ratio_px_in_mm = fields.Float(
|
||||
string='Ratio (px in mm)',
|
||||
digits=(10, 4),
|
||||
compute='_compute_ratio_px_in_mm',
|
||||
help="Technical field that indicates how many pixels in 1 mm.",
|
||||
store=True,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.depends('section_ids')
|
||||
def _compute_section_count(self):
|
||||
for template in self:
|
||||
template.section_count = len(template.section_ids)
|
||||
|
||||
@api.depends('width', 'height', 'section_ids', 'section_ids.height')
|
||||
def _compute_is_oversized(self):
|
||||
for template in self:
|
||||
total_height = sum(template.section_ids.mapped('height')) \
|
||||
+ template.padding_top + template.padding_bottom
|
||||
template.is_oversized = template.height < total_height
|
||||
|
||||
@api.depends('paperformat_id', 'paperformat_id.dpi')
|
||||
def _compute_ratio_px_in_mm(self):
|
||||
for template in self:
|
||||
template.ratio_px_in_mm = template.paperformat_id.dpi / 25.4
|
||||
|
||||
def _set_paperformat(self):
|
||||
self.ensure_one()
|
||||
self.env.ref(
|
||||
'garazd_product_label.action_report_product_label_from_template'
|
||||
).sudo().paperformat_id = self.paperformat_id.id
|
||||
|
||||
def write(self, vals):
|
||||
"""If the Dymo label width or height were changed,
|
||||
we should change it to the related paperformat."""
|
||||
res = super(PrintProductLabelTemplate, self).write(vals)
|
||||
if 'width' in vals or 'height' in vals:
|
||||
for template in self:
|
||||
if template.paperformat_id.format == 'custom' \
|
||||
and template.cols == 1 and template.rows == 1:
|
||||
template.paperformat_id.sudo().write({
|
||||
# flake8: noqa: E501
|
||||
'page_width': float_round(template.width, precision_rounding=1, rounding_method='UP'),
|
||||
'page_height': float_round(template.height, precision_rounding=1, rounding_method='UP'),
|
||||
})
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
paperformats = self.mapped('paperformat_id')
|
||||
res = super(PrintProductLabelTemplate, self).unlink()
|
||||
paperformats.sudo().unlink()
|
||||
return res
|
||||
|
||||
def get_demo_product(self):
|
||||
self.ensure_one()
|
||||
product = self.env['product.product'].browse()
|
||||
|
||||
# If the label template is opened from the print wizard (the 'print_wizard_id' value in the context),
|
||||
# use the first product from the list. Otherwise, use the demo product
|
||||
|
||||
# Case 1: Get a real product to display
|
||||
if self.env.company.print_label_preview_type == 'real_product':
|
||||
if self._context.get('print_product_id'):
|
||||
product = self.env['product.product'].browse(self._context.get('print_product_id'))
|
||||
if not product and self._context.get('print_wizard_id'):
|
||||
wizard = self.env['print.product.label'].browse(self._context.get('print_wizard_id'))
|
||||
if wizard.label_ids:
|
||||
product = wizard.label_ids[0].product_id
|
||||
|
||||
# Case 2: Get a demo product that is specified in the general settings
|
||||
if not product:
|
||||
product = self.env.company.print_label_preview_product_id
|
||||
|
||||
return product
|
||||
|
||||
def get_demo_product_label(self, product: models.Model = None):
|
||||
self.ensure_one()
|
||||
PrintWizard = self.env['print.product.label']
|
||||
ProductLabel = self.env['print.product.label.line']
|
||||
wizard = PrintWizard.browse()
|
||||
label = ProductLabel.browse()
|
||||
|
||||
# Get a real product to display
|
||||
# If the label template is opened from the print wizard (the 'print_wizard_id' value in the context),
|
||||
# use the first product from the list. Otherwise, use the demo product
|
||||
if self.env.company.print_label_preview_type == 'real_product' and self._context.get('print_wizard_id'):
|
||||
wizard = PrintWizard.browse(self._context.get('print_wizard_id'))
|
||||
label = wizard.label_ids[:1]
|
||||
|
||||
demo_product = product or self.get_demo_product()
|
||||
if not demo_product:
|
||||
raise UserError(_("Please select a demo product in the General Settings."))
|
||||
|
||||
if not wizard or not label:
|
||||
pricelist_id = self._context.get('pricelist_id')
|
||||
pricelist = self.env['product.pricelist'].browse(pricelist_id) \
|
||||
if pricelist_id else self.env.company.print_label_preview_pricelist_id
|
||||
sale_pricelist_id = self._context.get('sale_pricelist_id')
|
||||
sale_pricelist = self.env['product.pricelist'].browse(sale_pricelist_id) \
|
||||
if sale_pricelist_id else self.env.company.print_label_preview_sale_pricelist_id
|
||||
wizard = PrintWizard.create({
|
||||
'template_id': self.id,
|
||||
'company_id': self.env.company.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'sale_pricelist_id': sale_pricelist.id,
|
||||
})
|
||||
label = ProductLabel.create({
|
||||
'wizard_id': wizard.id,
|
||||
'product_id': demo_product.id,
|
||||
'price': demo_product.lst_price,
|
||||
'barcode': demo_product.barcode,
|
||||
})
|
||||
return label
|
||||
|
||||
@api.depends('preview')
|
||||
def _compute_preview_html(self):
|
||||
for template in self:
|
||||
demo_product = template.get_demo_product()
|
||||
template.preview_html = template.get_preview_html() if demo_product else ''
|
||||
|
||||
def get_preview_html(self):
|
||||
self.ensure_one()
|
||||
values = {
|
||||
'back_style':
|
||||
'background-color: #CCCCCC; '
|
||||
'width: 100%; height: 100%; '
|
||||
'padding: 15px; '
|
||||
'overflow: hidden;',
|
||||
'label_style':
|
||||
'width: %(width)fmm; '
|
||||
'height: %(height)fmm; '
|
||||
'background-color: #FFFFFF; '
|
||||
'margin: auto; '
|
||||
'padding: %(padding_top)fmm '
|
||||
'%(padding_right)fmm '
|
||||
'%(padding_bottom)fmm '
|
||||
'%(padding_left)fmm; '
|
||||
'%(label_custom_style)s' % {
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'padding_top': self.padding_top,
|
||||
'padding_right': self.padding_right,
|
||||
'padding_bottom': self.padding_bottom,
|
||||
'padding_left': self.padding_left,
|
||||
'label_custom_style': self.label_style or '',
|
||||
},
|
||||
'sections': self.section_ids.filtered('active'),
|
||||
'label': self.get_demo_product_label(),
|
||||
'editable_section_id': self._context.get('editable_section_id', False),
|
||||
}
|
||||
return self.env['ir.ui.view']._render_template('garazd_product_label_pro.label_preview', values)
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _get_user_allowed_templates(self):
|
||||
"""
|
||||
System administrators are not restricted anyway.
|
||||
Other users are restricted if allowed templates are specified in their settings,
|
||||
otherwise these users can use all templates.
|
||||
"""
|
||||
all_templates = self.search([])
|
||||
return all_templates if self.env.user.has_group('base.group_system') \
|
||||
else all_templates if not self.env.user.print_label_allowed_template_ids \
|
||||
else self.env.user.print_label_allowed_template_ids
|
||||
|
||||
def action_pdf_preview(self):
|
||||
self.ensure_one()
|
||||
label = self.get_demo_product_label()
|
||||
return self.env['print.product.label']._pdf_preview(label.wizard_id.get_pdf())
|
||||
17
garazd_product_label_pro/models/product_product.py
Normal file
17
garazd_product_label_pro/models/product_product.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
def action_open_label_layout(self):
|
||||
"""
|
||||
If a user has direct print option and a label template, return the direct print action,
|
||||
Otherwise, return the standard Odoo print wizard.
|
||||
"""
|
||||
print_wizard = super(ProductProduct, self).action_open_label_layout()
|
||||
if not self.env['ir.config_parameter'].sudo().get_param('garazd_product_label.replace_standard_wizard'):
|
||||
return print_wizard
|
||||
return self.env['print.product.label'].get_quick_report_action(
|
||||
model_name='product.product', ids=self.ids, close_window=True,
|
||||
) if self.env.user.print_label_directly and self.env.user.print_label_template_id else print_wizard
|
||||
17
garazd_product_label_pro/models/product_template.py
Normal file
17
garazd_product_label_pro/models/product_template.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
def action_open_label_layout(self):
|
||||
"""
|
||||
If a user has direct print option and a label template, return the direct print action,
|
||||
Otherwise, return the standard Odoo print wizard.
|
||||
"""
|
||||
print_wizard = super(ProductTemplate, self).action_open_label_layout()
|
||||
if not self.env['ir.config_parameter'].sudo().get_param('garazd_product_label.replace_standard_wizard'):
|
||||
return print_wizard
|
||||
return self.env['print.product.label'].get_quick_report_action(
|
||||
model_name='product.template', ids=self.ids, close_window=True,
|
||||
) if self.env.user.print_label_directly and self.env.user.print_label_template_id else print_wizard
|
||||
29
garazd_product_label_pro/models/res_company.py
Normal file
29
garazd_product_label_pro/models/res_company.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
print_label_preview_type = fields.Selection(
|
||||
selection=[('demo_product', 'Demo Product'), ('real_product', 'Real Product')],
|
||||
default='demo_product',
|
||||
required=True,
|
||||
help='Specify what product should be used during label designing.',
|
||||
)
|
||||
print_label_preview_product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
string='Demo Product',
|
||||
default=lambda self: self.env['product.product'].search([
|
||||
('barcode', '!=', False)
|
||||
], limit=1),
|
||||
)
|
||||
print_label_preview_pricelist_id = fields.Many2one(
|
||||
comodel_name='product.pricelist',
|
||||
string='Demo Pricelist',
|
||||
default=lambda self: self.env['product.pricelist'].search([])[:1],
|
||||
)
|
||||
print_label_preview_sale_pricelist_id = fields.Many2one(
|
||||
comodel_name='product.pricelist',
|
||||
string='Demo Promo Pricelist',
|
||||
default=lambda self: self.env['product.pricelist'].search([])[1:2],
|
||||
)
|
||||
26
garazd_product_label_pro/models/res_config_settings.py
Normal file
26
garazd_product_label_pro/models/res_config_settings.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
print_label_preview_type = fields.Selection(
|
||||
related='company_id.print_label_preview_type',
|
||||
# selection=[('real_product', 'Draft'), ('demo_product', 'Done')],
|
||||
readonly=False,
|
||||
)
|
||||
print_label_preview_product_id = fields.Many2one(
|
||||
string='Demo Product',
|
||||
related='company_id.print_label_preview_product_id',
|
||||
readonly=False,
|
||||
)
|
||||
print_label_preview_pricelist_id = fields.Many2one(
|
||||
string='Demo Pricelist',
|
||||
related='company_id.print_label_preview_pricelist_id',
|
||||
readonly=False,
|
||||
)
|
||||
print_label_preview_sale_pricelist_id = fields.Many2one(
|
||||
string='Demo Promo Pricelist',
|
||||
related='company_id.print_label_preview_sale_pricelist_id',
|
||||
readonly=False,
|
||||
)
|
||||
22
garazd_product_label_pro/models/res_users.py
Normal file
22
garazd_product_label_pro/models/res_users.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
print_label_template_id = fields.Many2one(
|
||||
comodel_name='print.product.label.template',
|
||||
string='Default Template',
|
||||
)
|
||||
print_label_allowed_template_ids = fields.Many2many(
|
||||
comodel_name='print.product.label.template',
|
||||
string='Allowed Templates',
|
||||
help='Restrict this user to the specified label templates. If no templates are specified, allow all templates.'
|
||||
'Please take a note that the system administrators cannot be restricted, they always see all templates.',
|
||||
)
|
||||
print_label_directly = fields.Boolean(
|
||||
string='Immediate Printing',
|
||||
help='If this option is active and the default label template is specified, '
|
||||
'after clicking on the Print Labels button the alternative print wizard will be skipped, '
|
||||
'and labels will be send to print without downloading (the browser printing window will be shown).',
|
||||
)
|
||||
Reference in New Issue
Block a user