This commit is contained in:
gsinghpal
2026-03-11 12:15:53 -04:00
parent f81e0cd918
commit db4b9aa278
1210 changed files with 173089 additions and 4044 deletions

BIN
fusion_labels/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,16 @@
============
Fusion Labels
============
Print custom product labels with barcode support.
Features
--------
* Customizable product label layouts
* Barcode support on labels
* Multiple label types
* Configurable label printing
Author
------
* Nexa Systems Inc - https://nexasystems.ca

View File

@@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@@ -0,0 +1,34 @@
{
# App information
"name": "Fusion Labels",
"version": "19.0.1.0.0",
"category": "Extra Tools",
"summary": "Print custom product labels with barcode.",
"license": "OPL-1",
"depends": [
"product",
],
# Views
"data": [
"security/ir.model.access.csv",
"data/product_data.xml",
"data/print_label_type_data.xml",
"report/product_label_reports.xml",
"report/product_label_templates.xml",
"wizard/print_product_label_views.xml",
"views/res_config_settings_views.xml",
],
"demo": [
"demo/product_demo.xml",
],
"images": ["static/description/icon.png"],
# Author
"author": "Nexa Systems Inc",
"website": "https://nexasystems.ca",
"maintainer": "Nexa Systems Inc",
"support": "support@nexasystems.ca",
# Technical
"installable": True,
"auto_install": False,
"application": True,
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="filter_label_apps" model="ir.filters">
<field name="name">Fusion Labelss</field>
<field name="model_id">ir.module.module</field>
<field name="domain">[("name", "ilike", "fusion_labels")]</field>
<field name="user_id" eval="False"/>
<field name="is_default" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="type_product" model="print.label.type" forcecreate="True">
<field name="name">Products</field>
<field name="code">product.product</field>
</record>
</odoo>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<record id="product_blank" model="product.product">
<field name="name">Blank Product</field>
<field name="type">consu</field>
<field name="description">Please do not delete this product! This product has been created by Fusion Labels solution for technical purposes, and it's used for label printing.</field>
<field name="active" eval="False"/>
</record>
<record id="fusion_labels.product_blank_product_template" model="product.template">
<field name="active" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="fusion_labels.filter_data_feed_apps" model="ir.filters">
<field name="is_default" eval="True"/>
</record>
</odoo>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<!-- Corner Desk Right Sit -->
<record id="product.product_product_5" model="product.product">
<field name="barcode">3333000022222</field>
</record>
<!-- Storage Box -->
<record id="product.product_product_7" model="product.product">
<field name="barcode">0123456789017</field>
</record>
<!-- Monitor Stand -->
<record id="product.monitor_stand" model="product.product">
<field name="barcode">0700020543219</field>
</record>
<!-- Flipover -->
<record id="product.product_product_20" model="product.product">
<field name="barcode">01234090543216</field>
</record>
</odoo>

View File

@@ -0,0 +1,4 @@
from . import res_config_settings
from . import product_template
from . import product_product
from . import print_label_type

View File

@@ -0,0 +1,9 @@
from odoo import fields, models
class PrintLabelTypePy(models.Model):
_name = "print.label.type"
_description = 'Label Types'
name = fields.Char(required=True, translate=True)
code = fields.Char(required=True)
_sql_constraints = [('print_label_type_code_uniq', 'UNIQUE (code)', 'Code of a print label type must be unique.')]

View File

@@ -0,0 +1,11 @@
from odoo import models
class ProductProduct(models.Model):
_inherit = "product.product"
def action_open_label_layout(self):
# flake8: noqa: E501
if not self.env['ir.config_parameter'].sudo().get_param('fusion_labels.replace_standard_wizard'):
return super(ProductProduct, self).action_open_label_layout()
action = self.env['ir.actions.act_window']._for_xml_id('fusion_labels.action_print_label_from_product')
action['context'] = {'default_product_product_ids': self.ids}
return action

View File

@@ -0,0 +1,11 @@
from odoo import models
class ProductTemplate(models.Model):
_inherit = "product.template"
def action_open_label_layout(self):
# flake8: noqa: E501
if not self.env['ir.config_parameter'].sudo().get_param('fusion_labels.replace_standard_wizard'):
return super(ProductTemplate, self).action_open_label_layout()
action = self.env['ir.actions.act_window']._for_xml_id('fusion_labels.action_print_label_from_template')
action['context'] = {'default_product_template_ids': self.ids}
return action

View File

@@ -0,0 +1,5 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
replace_standard_wizard = fields.Boolean(config_parameter='fusion_labels.replace_standard_wizard')

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="paperformat_label_a4_blank" model="report.paperformat">
<field name="name">Label A4</field>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">10</field>
<field name="margin_bottom">10</field>
<field name="margin_left">5</field>
<field name="margin_right">5</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="paperformat_label_50x38" model="report.paperformat">
<field name="name">Label 50x38 mm</field>
<field name="format">custom</field>
<field name="page_height">38</field>
<field name="page_width">50</field>
<field name="orientation">Portrait</field>
<field name="margin_top">1</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="action_report_product_label_A4_57x35" model="ir.actions.report">
<field name="name">Product Labels 57x35mm (A4, 21 pcs)</field>
<field name="model">print.product.label.line</field>
<field name="report_type">qweb-pdf</field>
<field name="paperformat_id" ref="paperformat_label_a4_blank"/>
<field name="report_name">fusion_labels.report_product_label_57x35_template</field>
<field name="report_file">fusion_labels.report_product_label_57x35_template</field>
<field name="print_report_name">'Product Labels 57x35mm'</field>
</record>
<record id="action_report_product_label_50x38" model="ir.actions.report">
<field name="name">Product Labels 50x38mm</field>
<field name="model">print.product.label.line</field>
<field name="report_type">qweb-pdf</field>
<field name="paperformat_id" ref="paperformat_label_50x38"/>
<field name="report_name">fusion_labels.report_product_label_50x38_template</field>
<field name="report_file">fusion_labels.report_product_label_50x38_template</field>
<field name="print_report_name">'Product Labels 50x38mm'</field>
</record>
<record id="action_report_product_label_from_template" model="ir.actions.report">
<field name="name">Product Label from your own template</field>
<field name="model">print.product.label.line</field>
<field name="report_type">qweb-pdf</field>
<field name="paperformat_id" ref="fusion_labels.paperformat_label_a4_blank"/>
<field name="report_name">fusion_labels.report_product_label_from_template</field>
<field name="report_file">fusion_labels.report_product_label_from_template</field>
<field name="print_report_name">'Product Labels Custom Design'</field>
</record>
</odoo>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="label_57x35">
<table class="table" style="margin: 0; padding: 0;">
<tr height="144px" style="border:0;margin:0;padding:0;">
<td class="col-xs-4 text-center" style="border:0;margin:0;padding:0;">
<div style="overflow:hidden; height:65px !important; padding: 5px 5px 0; line-height: 1.2;">
<span t-field="label.product_id.name" style="line-height: 1.2;"/>
<span t-if="label.product_id.product_template_attribute_value_ids"
t-esc="u', '.join(map(lambda x: x.attribute_line_id.attribute_id.name + u': ' + x.name, label.product_id.product_template_attribute_value_ids))"
class="badge"/>
</div>
<div style="width: 100%; padding: 0; margin: 0; font-size: 21px; font-weight: bold;">
<span t-if="label.product_id.currency_id.position == 'before'" t-field="label.product_id.currency_id.symbol"/>
<span t-field="label.product_id.lst_price"/>
<span t-if="label.product_id.currency_id.position == 'after'" t-field="label.product_id.currency_id.symbol"/>
</div>
<div t-if="label.product_id.default_code" style="width: 100%; padding: 0; margin: 0; font-size: 12px;">
<span t-field="label.product_id.default_code"/>
</div>
<t t-if="label.product_id.barcode">
<div t-out="label.product_id.barcode" t-options="{'widget': 'barcode', 'symbology': 'auto', 'img_style': 'overflow: hidden; width: 100%; height: 1.4rem;', 'humanreadable': label.wizard_id.humanreadable}" class="px-1"/>
</t>
</td>
</tr>
</table>
</template>
<template id="report_product_label_57x35_template">
<t t-call="web.html_container">
<t t-call="web.basic_layout">
<t t-set="count" t-value="0" />
<div class="page">
<div class="oe_structure"/>
<t t-foreach="docs" t-as="label">
<t t-set="qty" t-value="1" />
<t t-if="label.qty">
<t t-set="qty" t-value="label.qty" />
</t>
<t t-foreach="list(range(qty))" t-as="index_qty">
<div t-if="count % 21 == 0" style="page-break-after:always;"/>
<div t-if="count % 3 == 0" style="clear:both;"/>
<div t-att-style="'width: 233px; float: left; height: 145px; margin: 0 4px 4px; border: {};'.format('%dpx solid #777;' % label.wizard_id.border_width if label.wizard_id.border_width else '0')">
<t t-call="fusion_labels.label_57x35"/>
</div>
<t t-set="count" t-value="count+1" />
</t>
</t>
</div>
</t>
</t>
</template>
<template id="report_product_label_50x38_template">
<t t-call="web.html_container">
<t t-call="web.basic_layout">
<t t-foreach="docs" t-as="label">
<t t-set="qty" t-value="1"/>
<t t-if="label.qty" t-set="qty" t-value="label.qty" />
<t t-foreach="list(range(qty))" t-as="index_qty">
<div class="page" style="page-break-after: always;">
<div style="height: 136px !important; width: 165px !important;">
<table class="table" t-att-style="'height: 100%; border: {};'.format('%dpx solid #777;' % label.wizard_id.border_width if label.wizard_id.border_width else '0')">
<tr style="border: 0;">
<td class="text-center" style="padding: 0; margin: 0; border: 0;">
<div style="overflow: hidden; height: 64px !important; font-size: 13px; line-height: 1.2; margin-bottom: 2px;">
<span t-field="label.product_id.name" style="line-height: 1.2;"/>
<span t-if="label.product_id.product_template_attribute_value_ids"
t-esc="u', '.join(map(lambda x: x.attribute_line_id.attribute_id.name + u': ' + x.name, label.product_id.product_template_attribute_value_ids))"
class="badge"/>
</div>
<div style="width: 100%; overflow: hidden; height: 26px !important;">
<div style="float: right; width: 60%; font-size: 20px; font-weight: bold; line-height: 1.0; text-align: right; margin-bottom: 0px;">
<span t-if="label.product_id.currency_id.position == 'before'" t-field="label.product_id.currency_id.symbol"/>
<span t-field="label.product_id.lst_price"/>
<span t-if="label.product_id.currency_id.position == 'after'" t-field="label.product_id.currency_id.symbol"/>
</div>
<div style="float: left; width: 40%; font-size: 10px; font-weight: 900; line-height: 1.0; text-align: left; margin-bottom: 0px; padding-top: 6px;">
<span t-if="label.product_id.default_code"><span t-field="label.product_id.default_code"/> </span>
</div>
</div>
<t t-if="label.product_id.barcode">
<div t-out="label.product_id.barcode" style="margin-top: 5px;" t-options="{'widget': 'barcode', 'quiet': 0, 'symbology': 'auto', 'img_style': 'overflow: hidden; width: 100%; height: 2.0rem;', 'humanreadable': label.wizard_id.humanreadable}"/>
</t>
</td>
</tr>
</table>
</div>
</div>
</t>
</t>
</t>
</t>
</template>
<template id="report_product_label_from_template">
<t t-call="web.basic_layout"/>
</template>
</odoo>

View File

@@ -0,0 +1,4 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_print_product_label_line_user,access_print_product_label_line_user,model_print_product_label_line,base.group_user,1,1,1,1
access_print_product_label_user,access_print_product_label_user,model_print_product_label,base.group_user,1,1,1,0
access_print_label_type_user_read,access_print_label_type_user_read,model_print_label_type,base.group_user,1,0,0,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_print_product_label_line_user access_print_product_label_line_user model_print_product_label_line base.group_user 1 1 1 1
3 access_print_product_label_user access_print_product_label_user model_print_product_label base.group_user 1 1 1 0
4 access_print_label_type_user_read access_print_label_type_user_read model_print_label_type base.group_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,118 @@
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row mt-3">
<div class="col-12 text-right d-none d-md-block">
<span class="m-2" style="float: right; background-color: #694D65; border-color: #694D65; color: #FFFFFF; padding: 5px 18px; border-radius: 15px 0 15px 0;"><i class="fa fa-check"></i> Enterprise</span>
<span class="m-2" style="float: right; background-color: #6F649A; border-color: #6F649A; color: #FFFFFF; padding: 5px 18px; border-radius: 15px 0 15px 0;"><i class="fa fa-check"></i> Community</span>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12 d-flex d-column">
<h2 class="h1 align-self-center text-center" style="color: #154577; font-weight: 900;">Fusion Labels - Product Label Printing</h2>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12">
<div class="h2 text-left ml8" style="color: #FEA621;"><i class="fa fa-newspaper-o mr8"></i>Description</div>
<div class="media p-2 pt-4 overflow-hidden" style="border-color: #FEA621 !important; border-radius: 0 15px 0 0; border-top-style: solid; border-right-style: solid;">
<div class="py-2 px-md-5 lead w-100" style="line-height: 2.5rem;">
<p>Print custom product labels with barcode support on various paper formats. Includes configurable label templates for different sizes and designs.</p>
<p class="pt-3">This module includes two label templates:</p>
<ul>
<li><b>57x35mm</b> (21 pcs on A4 paper, 3 pcs x 7 rows)</li>
<li><b>50x38mm</b> (Dymo label, 2" x 1.5")</li>
</ul>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12">
<div class="h2 text-left ml8" style="color: #FEA621;"><i class="fa fa-check-square-o mr8"></i>Features</div>
<div class="s_features_grid p-2 pt-4 o_colored_level" style="border-color: #FEA621 !important; border-radius: 0 15px 0 0; border-top-style: solid; border-right-style: solid; background-color: #ffa81f14;">
<div class="container p-0">
<div class="row px-md-5">
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-barcode rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Barcode Labels</div>
<p>Generate product labels with barcodes</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-print rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Multiple Formats</div>
<p>Support for various paper sizes and label types</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-sliders rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Customizable</div>
<p>Configure label content and appearance</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-check-square-o rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Tested</div>
<p>Includes unit tests for reliability</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12">
<div class="h2 text-left ml8" style="color: #FEA621;"><i class="fa fa-barcode mr8"></i>Label Samples</div>
<div class="media p-2 pt-4 border-top border-right" style="border-color: #FEA621 !important; border-radius: 0 15px 0 0;">
<div class="media-body">
<div class="py-2 px-md-5" style="line-height: 2rem;">Labels are generated in PDF format:</div>
<div class="text-center my-5 mx-md-5">
<img src="product_barcode_label_50x38mm.png" class="w-100 w-lg-25 img-fluid rounded shadow-lg border" alt="Product Labels 50x38mm">
</div>
<div class="text-center my-5 mx-md-5">
<img src="product_barcode_label_A4_57x35mm.png" class="w-100 w-lg-50 img-fluid rounded shadow-lg border" alt="Product Labels A4 57x35mm">
</div>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row mt-5 mb-4 align-items-center">
<div class="col-6 text-left">Version: 19.0.1.0.0</div>
<div class="col-6 text-right"><small>Copyright &copy; <a href="https://nexasystems.ca" target="_blank">Nexa Systems Inc</a></small></div>
</div>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1 @@
from . import test_print_product_label

View File

@@ -0,0 +1,29 @@
from odoo.tests.common import TransactionCase
from odoo.tools import test_reports
from odoo.tests import tagged
@tagged('post_install', '-at_install', 'fusion_labels')
class TestPrintProductLabel(TransactionCase):
def setUp(self):
super(TestPrintProductLabel, self).setUp()
self.product_chair = self.env.ref('product.product_product_12')
self.product_drawer = self.env.ref('product.product_product_27')
def test_print_wizard(self):
ctx = {
'active_model': 'product.product',
'default_product_product_ids': [
self.product_chair.id,
self.product_drawer.id,
],
}
wizard = self.env['print.product.label'].with_context(**ctx).create({})
self.assertEqual(len(wizard.label_ids), 2)
test_reports.try_report(
self.env.cr,
self.env.uid,
'fusion_labels.report_product_label_57x35_template',
ids=wizard.label_ids.ids,
our_module='fusion_labels'
)

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Inherit base settings form and add Fusion Labels app block -->
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.fusion_labels</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="priority">60</field>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="Fusion Labels" name="fusion_labels" logo="/fusion_labels/static/description/icon.png">
<block title="General" id="fusion_labels_general">
<setting id="fusion_labels_replace_wizard"
string="Alternative Print Wizard"
help="Use the custom print wizard by clicking on the 'Print Labels' button instead of the standard Odoo label printing wizard.">
<field name="replace_standard_wizard"/>
</setting>
</block>
</app>
</xpath>
</field>
</record>
<!-- Settings action -->
<record id="res_config_settings_action_fusion_labels" model="ir.actions.act_window">
<field name="name">Settings</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">main</field>
<field name="context">{'module': 'fusion_labels'}</field>
</record>
<!-- Settings menu under Settings -->
<menuitem id="menu_fusion_labels_config"
name="Fusion Labels"
parent="base.menu_administration"
action="res_config_settings_action_fusion_labels"
sequence="55"
groups="base.group_system"/>
</odoo>

View File

@@ -0,0 +1,2 @@
from . import print_product_label_line
from . import print_product_label

View File

@@ -0,0 +1,182 @@
# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)
# License OPL-1 (https://www.odoo.com/documentation/19.0/legal/licenses.html).
import base64
from typing import List
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.addons.base.models.res_partner import _lang_get
class PrintProductLabel(models.TransientModel):
_name = "print.product.label"
_description = 'Wizard to print Product Labels'
@api.model
def _complete_label_fields(self, label_ids: List[int]) -> List[int]:
"""Set additional fields for product labels. Method to override."""
# Increase a label sequence
labels = self.env['print.product.label.line'].browse(label_ids)
for seq, label in enumerate(labels):
label.sequence = 1000 + seq
return label_ids
@api.model
def _get_product_label_ids(self):
res = []
# flake8: noqa: E501
if self._context.get('active_model') == 'product.template':
products = self.env[self._context.get('active_model')].browse(
self._context.get('default_product_template_ids')
)
for product in products:
label = self.env['print.product.label.line'].create({
'product_id': product.product_variant_id.id,
})
res.append(label.id)
elif self._context.get('active_model') == 'product.product':
products = self.env[self._context.get('active_model')].browse(
self._context.get('default_product_product_ids')
)
for product in products:
label = self.env['print.product.label.line'].create({'product_id': product.id})
res.append(label.id)
res = self._complete_label_fields(res)
return res
@api.model
def default_get(self, fields_list):
default_vals = super(PrintProductLabel, self).default_get(fields_list)
if 'label_type_id' in fields_list and not default_vals.get('label_type_id'):
default_vals['label_type_id'] = self.env.ref('fusion_labels.type_product').id
return default_vals
name = fields.Char(default='Print Product Labels')
message = fields.Char(readonly=True)
output = fields.Selection(selection=[('pdf', 'PDF')], string='Print to', default='pdf')
mode = fields.Selection(
selection=[('product.product', 'Products')],
help='Technical field to specify the mode of the label printing wizard.',
default='product.product',
)
label_type_id = fields.Many2one(comodel_name='print.label.type', string='Label Type')
label_ids = fields.One2many(
comodel_name='print.product.label.line',
inverse_name='wizard_id',
string='Labels for Products',
default=_get_product_label_ids,
)
report_id = fields.Many2one(
comodel_name='ir.actions.report',
string='Label',
domain=[('model', '=', 'print.product.label.line')],
)
is_template_report = fields.Boolean(compute='_compute_is_template_report')
qty_per_product = fields.Integer(
string='Label quantity per product',
default=1,
)
# Options
humanreadable = fields.Boolean(
string='Human readable barcode',
help='Print digital code of barcode.',
default=False,
)
border_width = fields.Integer(
string='Border',
help='Border width for labels (in pixels). Set "0" for no border.'
)
lang = fields.Selection(
selection=_lang_get,
string='Language',
help="The language that will be used to translate label names.",
)
company_id = fields.Many2one(
comodel_name='res.company',
help='Specify a company for product labels.'
)
@api.depends('report_id')
def _compute_is_template_report(self):
for wizard in self:
# flake8: noqa: E501
wizard.is_template_report = self.report_id == self.env.ref('fusion_labels.action_report_product_label_from_template')
def get_labels_to_print(self):
self.ensure_one()
labels = self.label_ids.filtered(lambda l: l.selected and l.qty)
if not labels:
raise UserError(_('Nothing to print, set the quantity of labels in the table.'))
return labels
def _get_report_action_params(self):
"""Return two params for a report action: record "ids" and "data"."""
self.ensure_one()
return self.get_labels_to_print().ids, None
def _prepare_report(self):
self.ensure_one()
output_mode = self._context.get('print_mode', 'pdf')
if not self.report_id:
raise UserError(_('Please select a label type.'))
report = self.report_id.with_context(discard_logo_check=True, lang=self.lang)
report.sudo().write({'report_type': f'qweb-{output_mode}'})
return report
def action_print(self):
"""Print labels."""
self.ensure_one()
report = self._prepare_report()
return report.report_action(*self._get_report_action_params())
def action_set_qty(self):
"""Set a specific number of labels for all lines."""
self.ensure_one()
self.label_ids.write({'qty': self.qty_per_product})
def action_restore_initial_qty(self):
"""Restore the initial number of labels for all lines."""
self.ensure_one()
for label in self.label_ids:
if label.qty_initial:
label.update({'qty': label.qty_initial})
@api.model
def get_quick_report_action(
self, model_name: str, ids: List[int], qty: int = None, template=None, force_direct: bool = False,
):
""" Allow to get a report action for custom labels. Method to override. """
wizard = self.with_context(
**{'active_model': model_name, f'default_{model_name.replace(".", "_")}_ids': ids}
).create({'report_id': self.env.ref('fusion_labels.action_report_product_label_50x38').id})
return wizard.action_print()
@api.model
def _set_sequence(self, lbl, seq, processed):
if lbl in processed:
return seq, processed
lbl.sequence = seq
seq += 1
processed += lbl
return seq, processed
def action_sort_by_product(self):
self.ensure_one()
sequence = 1000
processed_labels = self.env['print.product.label.line'].browse()
# flake8: noqa: E501
for label in self.label_ids:
sequence, processed_labels = self._set_sequence(label, sequence, processed_labels)
tmpl_labels = self.label_ids.filtered(lambda l: l.product_id.product_tmpl_id == label.product_id.product_tmpl_id).sorted(lambda l: l.product_id.id, reverse=True) - label
for tmpl_label in tmpl_labels:
sequence, processed_labels = self._set_sequence(tmpl_label, sequence, processed_labels)
product_labels = tmpl_labels.filtered(lambda l: l.product_id == label.product_id) - tmpl_label
for product_label in product_labels:
sequence, processed_labels = self._set_sequence(product_label, sequence, processed_labels)
def get_pdf(self):
self.ensure_one()
report = self.with_context(print_mode='pdf')._prepare_report()
pdf_data = report._render_qweb_pdf(report, *self._get_report_action_params())
return base64.b64encode(pdf_data[0])

View File

@@ -0,0 +1,46 @@
# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)
# License OPL-1 (https://www.odoo.com/documentation/19.0/legal/licenses.html).
from odoo import api, fields, models
class PrintProductLabelLine(models.TransientModel):
_name = "print.product.label.line"
_description = 'Line with a Product Label Data'
_order = 'sequence'
sequence = fields.Integer(default=900)
selected = fields.Boolean(string='Print', default=True)
wizard_id = fields.Many2one(comodel_name='print.product.label') # Do not make required
product_id = fields.Many2one(comodel_name='product.product', required=True)
barcode = fields.Char(compute='_compute_barcode')
qty_initial = fields.Integer(string='Initial Qty', default=1)
qty = fields.Integer(string='Label Qty', default=1)
custom_value = fields.Char(help="This field can be filled manually to use in label templates.")
company_id = fields.Many2one(comodel_name='res.company', compute='_compute_company_id')
# Allow users to specify a partner to use it on label templates
partner_id = fields.Many2one(comodel_name='res.partner', readonly=False)
@api.depends('wizard_id.company_id')
def _compute_company_id(self):
for label in self:
label.company_id = label.wizard_id.company_id.id \
if label.wizard_id.company_id else self.env.user.company_id.id
@api.depends('product_id')
def _compute_barcode(self):
for label in self:
label.barcode = label.product_id.barcode
def action_plus_qty(self):
self.ensure_one()
if not self.qty:
self.update({'selected': True})
self.update({'qty': self.qty + 1})
def action_minus_qty(self):
self.ensure_one()
if self.qty > 0:
self.update({'qty': self.qty - 1})
if not self.qty:
self.update({'selected': False})

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="print_product_label_view_form" model="ir.ui.view">
<field name="name">print.product.label.view.form</field>
<field name="model">print.product.label</field>
<field name="arch" type="xml">
<form create="false" string="Print Product Labels">
<header>
<button name="action_print"
string="Print"
help="Print product labels"
type="object"
icon="fa-print"
class="btn-primary mr8"/>
<button name="action_print"
string="Preview"
context="{'print_mode': 'html'}"
help="Preview product labels"
type="object"
icon="fa-search"
class="border btn-light mr8"/>
<field name="label_type_id" widget="selection_badge" invisible="1"/> <!-- Technical field to specify the label type -->
</header>
<div class="oe_button_box" name="button_box"/>
<field name="mode" invisible="1"/> <!-- Technical field to specify the mode of the label printing wizard -->
<field name="is_template_report" invisible="1"/> <!-- Technical field to specify if the custom label template is selected -->
<div class="alert alert-info text-center mb-3" invisible="not message" role="alert">
<field name="message"/>
</div>
<group col="6" colspan="2" name="top_group">
<group name="output_format" col="2" colspan="1">
<field name="output" widget="badge"/>
</group>
<group name="label_report" col="2" colspan="2">
<field name="report_id" string="Label" widget="radio" />
</group>
<group name="label_template" col="2" colspan="2" invisible="not is_template_report">
<label for="report_id" string="Template"/>
<div class="o_row">
<a name="label_builder_link" invisible="not is_template_report" href="https://nexasystems.ca" title="How to get Product Label Builder" target="_blank" rel="noopener noreferrer">
<span>Get the Label Builder to create your own labels</span>
</a>
</div>
</group>
</group>
<div name="extra_action" class="oe_row">
<button name="action_sort_by_product" title="Sort labels by a product" icon="fa-sort-amount-desc" type="object" class="btn-xs btn-light"/>
<span class="text-muted px-3">|</span>
<field name="qty_per_product" class="mr-2 mt-1 text-right" help="Label quantity per product" style="width: 40px !important;"/>
<button name="action_set_qty" string="Set quantity" help="Set a certain quantity for each line." type="object" class="btn-xs btn-light mr-2"/>
<button name="action_restore_initial_qty" title="Restore initial quantity" help="Restore initial quantity" icon="fa-undo" type="object" class="btn-xs btn-light"/>
</div>
<notebook>
<page string="Labels" name="labels">
<field name="label_ids" mode="list">
<list editable="top" decoration-muted="qty==0">
<field name="sequence" widget="handle" width="0.5"/>
<field name="selected" widget="boolean_toggle"/>
<field name="partner_id" optional="hide"/>
<field name="product_id"/>
<field name="barcode" optional="show"/>
<field name="custom_value" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<button name="action_minus_qty" type="object" title="Decrease Qty" icon="fa-minus" class="btn-light"/>
<field name="qty" sum="Total" width="0.6" class="text-center font-weight-bold"/>
<button name="action_plus_qty" type="object" title="Increase Qty" icon="fa-plus" class="btn-light"/>
</list>
</field>
</page>
<page string="Options" name="options">
<group name="general_options" class="o_label_nowrap">
<group>
<field name="lang" widget="radio" options="{'horizontal': true}"/>
<field name="humanreadable" widget="boolean_toggle" />
<field name="border_width" />
</group>
<group>
<field name="company_id" widget="radio" groups="base.group_multi_company"/>
</group>
</group>
</page>
</notebook>
</form>
</field>
</record>
<record id="action_print_label_from_template" model="ir.actions.act_window">
<field name="name">Custom Product Labels</field>
<field name="res_model">print.product.label</field>
<field name="view_mode">form</field>
<field name="target">current</field>
<field name="context">{'default_product_template_ids': context.get('active_ids', [])}</field>
<field name="binding_model_id" ref="product.model_product_template"/>
<field name="binding_type">report</field>
</record>
<record id="action_print_label_from_product" model="ir.actions.act_window">
<field name="name">Custom Product Labels</field>
<field name="res_model">print.product.label</field>
<field name="view_mode">form</field>
<field name="target">current</field>
<field name="context">{'default_product_product_ids': context.get('active_ids', [])}</field>
<field name="binding_model_id" ref="product.model_product_product"/>
<field name="binding_type">report</field>
</record>
</odoo>

View File

@@ -0,0 +1,14 @@
==================
Fusion Labels Print
==================
Product label printing without download - direct print to printer.
Features
--------
* Direct label printing without PDF download
* Seamless integration with Fusion Labels base module
Author
------
* Nexa Systems Inc - https://nexasystems.ca

View File

@@ -0,0 +1,2 @@
from . import wizard
from . import controllers

View File

@@ -0,0 +1,25 @@
{
# App information
"name": "Fusion Labels Print",
"version": "19.0.1.0.0",
"category": "Hidden",
"summary": "Product label printing without download.",
"license": "OPL-1",
"depends": [
"fusion_labels",
],
# Views
"data": [
"views/print_product_label_templates.xml",
"wizard/print_product_label_views.xml",
],
# Author
"author": "Nexa Systems Inc",
"website": "https://nexasystems.ca",
"maintainer": "Nexa Systems Inc",
"support": "support@nexasystems.ca",
# Technical
"installable": True,
"auto_install": False,
"application": False,
}

View File

@@ -0,0 +1 @@
from . import main

View File

@@ -0,0 +1,40 @@
# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)
# License OPL-1 (https://www.odoo.com/documentation/19.0/legal/licenses.html).
from odoo import http
from odoo.http import request
from ..wizard.print_product_label import LABEL_ATTACHMENT_NAME
class PrintPDF(http.Controller):
@http.route(
'/print_label/<string:attachment_id>',
type='http',
auth='user',
sitemap=False,
)
def print_label_pdf(self, attachment_id=None, **kwargs):
if not attachment_id:
return request.not_found()
try:
attachment_id = int(attachment_id)
except ValueError:
return request.not_found()
attachment = request.env['ir.attachment'].sudo().search([
('name', '=', LABEL_ATTACHMENT_NAME),
('mimetype', '=', 'application/pdf'),
('id', '=', attachment_id),
])
if not attachment:
return request.not_found()
pdf_url = f'/web/content/{attachment_id}'
return request.render(
'fusion_labels_print.print_product_label_pdf_template',
{'title': 'Print Product Labels', 'pdf_url': pdf_url},
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="print_product_label_pdf_template" name="print_pdf_template">
<head>
<title><t t-out="title"/></title>
</head>
<body style="margin:auto;">
<iframe t-att-src="pdf_url" frameborder="0" scrolling="yes" seamless="seamless" style="display:block; width:100%; height:100vh;"></iframe>
</body>
<script>
printPdf = function (url) {
var iframe = this._printIframe;
if (!this._printIframe) {
iframe = this._printIframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.style.display = 'none';
iframe.onload = function() {
setTimeout(function() {
iframe.focus();
iframe.contentWindow.print();
}, 1);
};
}
iframe.src = url;
}
printPdf("<t t-out="pdf_url"/>")
</script>
</template>
</odoo>

View File

@@ -0,0 +1 @@
from . import print_product_label

View File

@@ -0,0 +1,39 @@
# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)
# License OPL-1 (https://www.odoo.com/documentation/19.0/legal/licenses.html).
from datetime import datetime, timedelta
from odoo import api, models
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
LABEL_ATTACHMENT_NAME = 'Product Label Direct Printing'
class PrintProductLabel(models.TransientModel):
_inherit = "print.product.label"
def action_print_direct(self):
""" Print labels directly without download. """
self.ensure_one()
attachment = self.env['ir.attachment'].create({
'name': LABEL_ATTACHMENT_NAME,
'type': 'binary',
'datas': self.get_pdf(),
'mimetype': 'application/pdf',
'public': False,
})
return {
'type': 'ir.actions.act_url',
'url': f'/print_label/{attachment.id}',
'target': 'new',
}
@api.autovacuum
def _gc_print_label_attachments(self):
timeout_ago = datetime.utcnow() - timedelta(days=1)
domain = [
('name', '=', LABEL_ATTACHMENT_NAME),
('mimetype', '=', 'application/pdf'),
('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
]
return self.env['ir.attachment'].sudo().search(domain).unlink()

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="print_product_label_view_form" model="ir.ui.view">
<field name="name">print.product.label.view.form.inherit.fusion_labels_print</field>
<field name="model">print.product.label</field>
<field name="inherit_id" ref="fusion_labels.print_product_label_view_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_print']" position="before">
<button name="action_print_direct"
string="Print"
help="Print product labels"
type="object"
icon="fa-print"
class="btn-primary mr8"/>
</xpath>
<xpath expr="//button[@name='action_print']" position="attributes">
<attribute name="string">Download</attribute>
<attribute name="help">Download product labels</attribute>
<attribute name="icon">fa-download</attribute>
<attribute name="class" separator=" " remove="btn-primary" add="btn-light border"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,17 @@
================
Fusion Labels Pro
================
Professional product barcode label builder and designer.
Features
--------
* Visual label template builder
* Configurable label sections and elements
* Professional label design tools
* Advanced barcode label generation
* Direct print support
Author
------
* Nexa Systems Inc - https://nexasystems.ca

View File

@@ -0,0 +1,3 @@
from . import models
from . import wizard
from . import report

View File

@@ -0,0 +1,45 @@
{
# App information
"name": "Fusion Labels Pro",
"version": "19.0.1.0.0",
"category": "Extra Tools",
"summary": "Professional product barcode label builder and designer.",
"license": "OPL-1",
"depends": [
"fusion_labels_print",
"link_tracker",
],
# Views
"data": [
"security/ir.model.access.csv",
"report/product_label_reports.xml",
"report/product_label_templates.xml",
"views/print_product_label_template_views.xml",
"views/print_product_label_section_views.xml",
"views/res_config_settings_views.xml",
"views/templates.xml",
"views/res_users_views.xml",
"data/print_product_label_template_data.xml",
"data/print_product_label_section_data.xml",
"wizard/print_product_label_preview_views.xml",
"wizard/print_product_label_views.xml",
"wizard/print_product_label_template_add_views.xml",
"wizard/product_label_layout_views.xml",
],
"demo": [
"data/product_pricelist_demo.xml",
"data/res_company_demo.xml",
],
"images": ["static/description/icon.png"],
# Author
"author": "Nexa Systems Inc",
"website": "https://nexasystems.ca",
"maintainer": "Nexa Systems Inc",
"support": "support@nexasystems.ca",
# Technical
"installable": True,
"auto_install": False,
"application": True,
"price": "245.00",
"currency": "USD",
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="fusion_show_label_template_limit" model="ir.config_parameter" forcecreate="True">
<field name="key">fusion_labels.show_label_template_limit</field>
<field name="value">7</field>
</record>
<record id="fusion_barcode_multiplier" model="ir.config_parameter" forcecreate="True">
<field name="key">fusion_labels_pro.barcode_multiplier</field>
<field name="value">1</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,567 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Sections for the Label 50x25 mm -->
<record id="label_section_50x25_product_name" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_50x25"/>
<field name="sequence">1</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__name"/>
<field name="height">11</field>
<field name="align">left</field>
<field name="font_size">11</field>
<field name="line_height">1.2</field>
<field name="padding_top">1</field>
<field name="padding_left">2</field>
<field name="padding_right">2</field>
<field name="active" eval="True"/>
</record>
<record id="label_section_50x25_product_price" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_50x25"/>
<field name="sequence">2</field>
<field name="type">field</field>
<field name="field_id" ref="field_print_product_label_line__price"/>
<field name="value_format">%.2f</field>
<field name="height">7</field>
<field name="align">right</field>
<field name="font_size">22</field>
<field name="font_weight">900</field>
<field name="line_height">1.0</field>
<field name="padding_right">3</field>
<field name="widget">price</field>
<field name="active" eval="True"/>
</record>
<record id="label_section_50x25_product_barcode" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_50x25"/>
<field name="sequence">3</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__barcode"/>
<field name="height">5</field>
<field name="align">center</field>
<field name="widget">barcode</field>
</record>
<record id="label_section_50x25_product_barcode_text" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_50x25"/>
<field name="sequence">4</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__barcode"/>
<field name="height">3</field>
<field name="align">center</field>
<field name="font_size">8</field>
<field name="letter_spacing">1.4</field>
</record>
<!-- Sections for the Label 100x100 mm -->
<record id="label_section_100x100_product_image" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">1</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__image_1024"/>
<field name="height">23</field>
<field name="width">50</field>
<field name="position">left</field>
<field name="padding_bottom">3</field>
<field name="widget">image</field>
</record>
<record id="label_section_100x100_blank" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">2</field>
<field name="type">text</field>
<field name="height">23</field>
<field name="width">25</field>
<field name="position">left</field>
</record>
<record id="label_section_100x100_company_website_qrcode" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">3</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__company_id"/>
<field name="relation_field_id" ref="base.field_res_company__website"/>
<field name="height">23</field>
<field name="width">25</field>
<field name="position">right</field>
<field name="padding_bottom">3</field>
<field name="widget">qr_code</field>
</record>
<record id="label_section_100x100_product_code_label" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">4</field>
<field name="type">text</field>
<field name="value">Code:</field>
<field name="height">5</field>
<field name="position">left</field>
<field name="width">55</field>
<field name="align">left</field>
<field name="font_size">13</field>
<field name="padding_top">1</field>
<field name="with_border_top" eval="True"/>
<field name="with_border_right" eval="True"/>
</record>
<record id="label_section_100x100_product_price_label" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">5</field>
<field name="type">text</field>
<field name="value">Price:</field>
<field name="height">5</field>
<field name="position">right</field>
<field name="width">45</field>
<field name="align">left</field>
<field name="font_size">13</field>
<field name="padding_top">1</field>
<field name="padding_left">3</field>
<field name="with_border_top" eval="True"/>
</record>
<record id="label_section_100x100_product_code" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">6</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__default_code"/>
<field name="height">12</field>
<field name="position">left</field>
<field name="width">55</field>
<field name="align">left</field>
<field name="font_size">26</field>
<field name="font_size_measure">px</field>
<field name="font_weight">bold</field>
<field name="padding_top">3</field>
<field name="padding_right">2</field>
<field name="with_border_right" eval="True"/>
</record>
<record id="label_section_100x100_product_price" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">7</field>
<field name="type">field</field>
<field name="field_id" ref="field_print_product_label_line__price"/>
<field name="value_format">%.2f</field>
<field name="height">12</field>
<field name="position">right</field>
<field name="width">44</field>
<field name="align">left</field>
<field name="font_size">26</field>
<field name="font_weight">900</field>
<field name="padding_top">2</field>
<field name="padding_left">2</field>
<field name="padding_bottom">2</field>
<field name="widget">price</field>
</record>
<record id="label_section_100x100_product_name_label" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">8</field>
<field name="type">text</field>
<field name="value">Description:</field>
<field name="height">7</field>
<field name="align">left</field>
<field name="font_size">13</field>
<field name="padding_top">3</field>
<field name="padding_left">2</field>
<field name="with_border_top" eval="True"/>
</record>
<record id="label_section_100x100_product_name" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">9</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__name"/>
<field name="height">30</field>
<field name="position">left</field>
<field name="width">55</field>
<field name="align">left</field>
<field name="font_size">18</field>
<field name="line_height">1.5</field>
<field name="padding_left">2</field>
<field name="margin_bottom">3</field>
<field name="with_border_right" eval="True"/>
</record>
<record id="label_section_100x100_product_multi_price" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">10</field>
<field name="type">multi_price</field>
<field name="multi_price_order">desc</field>
<field name="value_format">%.2f</field>
<field name="height">30</field>
<field name="position">right</field>
<field name="width">44</field>
<field name="align">left</field>
<field name="font_size">12</field>
<field name="line_height">1.1</field>
<field name="padding_left">3</field>
<field name="with_border_left" eval="False"/>
</record>
<record id="label_section_100x100_product_attributes" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">11</field>
<field name="type">product_attributes</field>
<field name="height">7</field>
<field name="font_size">18</field>
<field name="line_height">1.2</field>
<field name="font_weight">bold</field>
<field name="padding_top">1</field>
<field name="with_background" eval="True"/>
<field name="background_color">#EBEBEB</field>
</record>
<record id="label_section_100x100_company_footer" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">12</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__create_date"/>
<field name="value_format">%d-%m-%Y</field>
<field name="height">8</field>
<field name="position">left</field>
<field name="width">25</field>
<field name="align">left</field>
<field name="font_size">10</field>
<field name="padding_top">4</field>
</record>
<record id="label_section_100x100_company_name" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">13</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__company_id"/>
<field name="relation_field_id" ref="base.field_res_company__name"/>
<field name="height">8</field>
<field name="position">left</field>
<field name="width">50</field>
<field name="align">center</field>
<field name="font_size">10</field>
<field name="font_weight">bold</field>
<field name="padding_top">4</field>
</record>
<record id="label_section_100x100_company_phone" model="print.product.label.section">
<field name="template_id" ref="label_template_custom_100x100"/>
<field name="sequence">14</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__company_id"/>
<field name="relation_field_id" ref="base.field_res_company__phone"/>
<field name="height">8</field>
<field name="position">right</field>
<field name="width">25</field>
<field name="align">right</field>
<field name="font_size">10</field>
<field name="padding_top">4</field>
</record>
<!-- Sections for the Label A4 63x38 mm -->
<record id="label_section_a4_63x38_product_barcode" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_63x38"/>
<field name="sequence">1</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__barcode"/>
<field name="height">11</field>
<field name="align">center</field>
<field name="padding_top">2</field>
<field name="widget">barcode</field>
<field name="barcode_is_humanreadable" eval="True"/>
</record>
<record id="label_section_a4_63x38_product_price" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_63x38"/>
<field name="sequence">2</field>
<field name="type">field</field>
<field name="field_id" ref="field_print_product_label_line__price"/>
<field name="value_format">%.2f</field>
<field name="height">8</field>
<field name="position">left</field>
<field name="width">50</field>
<field name="align">left</field>
<field name="font_size">24</field>
<field name="font_size_measure">px</field>
<field name="font_weight">900</field>
<field name="line_height">1.0</field>
<field name="widget">price</field>
<field name="padding_top">0</field>
<field name="padding_left">5</field>
<field name="active" eval="True"/>
</record>
<record id="label_section_a4_63x38_product_code" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_63x38"/>
<field name="sequence">3</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__default_code"/>
<field name="height">8</field>
<field name="position">right</field>
<field name="width">50</field>
<field name="align">center</field>
<field name="font_size">15</field>
<field name="font_size_measure">px</field>
<field name="font_weight">bold</field>
<field name="letter_spacing">-0.2</field>
<field name="padding_top">4</field>
<field name="padding_right">2</field>
<field name="active" eval="True"/>
</record>
<record id="label_section_a4_63x38_product_type_text" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_63x38"/>
<field name="sequence">4</field>
<field name="type">text</field>
<field name="value">Product Type:</field>
<field name="height">5</field>
<field name="position">left</field>
<field name="width">50</field>
<field name="align">right</field>
<field name="font_size">10</field>
<field name="font_size_measure">px</field>
<field name="padding_top">2</field>
<field name="padding_right">2</field>
</record>
<record id="label_section_a4_63x38_product_type" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_63x38"/>
<field name="sequence">5</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__type"/>
<field name="height">5</field>
<field name="position">right</field>
<field name="width">50</field>
<field name="align">left</field>
<field name="font_size">10</field>
<field name="font_weight">bold</field>
<field name="padding_top">2</field>
</record>
<record id="label_section_a4_63x38_product_name" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_63x38"/>
<field name="sequence">6</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__name"/>
<field name="height">14</field>
<field name="align">center</field>
<field name="font_size">14</field>
<field name="font_size_measure">px</field>
<field name="line_height">1.1</field>
<field name="padding_top">1</field>
<field name="padding_left">2</field>
<field name="padding_right">2</field>
</record>
<!-- Sections for the Label A4 99x38 mm -->
<record id="label_section_a4_99x38_image" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_99x38"/>
<field name="sequence">1</field>
<field name="type">image</field>
<field name="image" type="base64" file="base/static/img/res_partner_3-image.png"/>
<field name="height">27</field>
<field name="width">30</field>
<field name="position">left</field>
<field name="padding_left">2</field>
<field name="padding_right">2</field>
<field name="with_border_right" eval="True"/>
</record>
<record id="label_section_a4_99x38_product_price_label" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_99x38"/>
<field name="sequence">2</field>
<field name="type">text</field>
<field name="value">PRICE:</field>
<field name="height">8</field>
<field name="position">left</field>
<field name="width">30</field>
<field name="align">left</field>
<field name="font_size">15</field>
<field name="font_size_measure">px</field>
<field name="font_weight">100</field>
<field name="padding_top">2</field>
<field name="padding_left">2</field>
</record>
<record id="label_section_a4_93x38_product_price" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_99x38"/>
<field name="sequence">3</field>
<field name="type">field</field>
<field name="field_id" ref="field_print_product_label_line__price"/>
<field name="value_format">%.2f</field>
<field name="height">8</field>
<field name="position">right</field>
<field name="width">40</field>
<field name="align">right</field>
<field name="font_size">23</field>
<field name="font_size_measure">px</field>
<field name="font_weight">900</field>
<field name="padding_right">2</field>
<field name="widget">price</field>
</record>
<record id="label_section_a4_99x38_product_name" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_99x38"/>
<field name="sequence">4</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__name"/>
<field name="height">19</field>
<field name="width">70</field>
<field name="position">right</field>
<field name="align">left</field>
<field name="font_size">18</field>
<field name="font_size_measure">px</field>
<field name="line_height">1.2</field>
<field name="padding_top">1</field>
<field name="padding_left">2</field>
<field name="padding_right">2</field>
<field name="with_border_top" eval="True"/>
</record>
<record id="label_section_a4_99x38_product_barcode" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_99x38"/>
<field name="sequence">5</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__barcode"/>
<field name="height">8</field>
<field name="width">70</field>
<field name="position">right</field>
<field name="align">center</field>
<field name="padding_top">1</field>
<field name="with_border_top" eval="True"/>
<field name="widget">barcode</field>
</record>
<record id="label_section_a4_99x38_company_website" model="print.product.label.section">
<field name="template_id" ref="label_template_a4_99x38"/>
<field name="sequence">6</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__company_id"/>
<field name="relation_field_id" ref="base.field_res_company__website"/>
<field name="height">8</field>
<field name="width">30</field>
<field name="position">left</field>
<field name="align">center</field>
<field name="font_size">9</field>
<field name="padding_top">3</field>
<field name="with_border_right" eval="True"/>
</record>
<!-- Sections for the Label Letter 2⅝" x 1" -->
<record id="label_section_letter_66x25_product_name" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_66x25"/>
<field name="sequence">1</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__name"/>
<field name="height">13</field>
<field name="position">left</field>
<field name="width">60</field>
<field name="align">center</field>
<field name="font_size">14</field>
<field name="line_height">1.2</field>
<field name="padding_right">2</field>
<field name="with_border_right" eval="True"/>
</record>
<record id="label_section_letter_66x25_product_code" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_66x25"/>
<field name="sequence">2</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__default_code"/>
<field name="height">13</field>
<field name="position">right</field>
<field name="width">40</field>
<field name="align">center</field>
<field name="font_size">13</field>
<field name="font_weight">bold</field>
<field name="padding_top">5</field>
<field name="padding_left">1</field>
<field name="with_border_bottom" eval="True"/>
</record>
<record id="label_section_letter_66x25_product_barcode" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_66x25"/>
<field name="sequence">3</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__barcode"/>
<field name="height">11</field>
<field name="position">left</field>
<field name="width">60</field>
<field name="align">center</field>
<field name="padding_top">1</field>
<field name="padding_bottom">1</field>
<field name="with_border_right" eval="True"/>
<field name="widget">barcode</field>
</record>
<record id="label_section_letter_66x25_product_price" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_66x25"/>
<field name="sequence">4</field>
<field name="type">field</field>
<field name="field_id" ref="field_print_product_label_line__price"/>
<field name="value_format">%.2f</field>
<field name="height">11</field>
<field name="position">right</field>
<field name="width">40</field>
<field name="align">center</field>
<field name="font_size">23</field>
<field name="font_size_measure">px</field>
<field name="font_weight">900</field>
<field name="padding_top">3</field>
<field name="widget">price</field>
</record>
<!-- Sections for the Label Letter 4" x 2" -->
<record id="label_section_letter_101x50_promo" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_101x50"/>
<field name="sequence">1</field>
<field name="type">text</field>
<field name="value">SPECIAL OFFER!</field>
<field name="text_color">#FFFFFF</field>
<field name="height">12</field>
<field name="align">center</field>
<field name="font_size">30</field>
<field name="font_weight">bold</field>
<field name="line_height">1.2</field>
<field name="letter_spacing">2</field>
<field name="padding_top">1</field>
<field name="padding_bottom">1</field>
<field name="with_background" eval="True"/>
</record>
<record id="label_section_letter_101x50_product_name" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_101x50"/>
<field name="sequence">2</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__product_id"/>
<field name="relation_field_id" ref="product.field_product_product__name"/>
<field name="height">18</field>
<field name="align">left</field>
<field name="font_size">22</field>
<field name="font_size_measure">px</field>
<field name="line_height">1.2</field>
<field name="padding_left">1</field>
<field name="padding_top">3</field>
<field name="with_border_bottom" eval="True"/>
</record>
<record id="label_section_letter_101x50_product_barcode" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_101x50"/>
<field name="sequence">3</field>
<field name="type">field</field>
<field name="field_id" ref="fusion_labels.field_print_product_label_line__barcode"/>
<field name="height">15</field>
<field name="position">left</field>
<field name="width">50</field>
<field name="align">center</field>
<field name="padding_top">2</field>
<field name="widget">barcode</field>
</record>
<record id="label_section_letter_101x50_product_price" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_101x50"/>
<field name="sequence">4</field>
<field name="type">price</field>
<field name="value_format">%.2f</field>
<field name="height">6</field>
<field name="position">right</field>
<field name="width">50</field>
<field name="align">left</field>
<field name="font_size">20</field>
<field name="font_weight">normal</field>
<field name="padding_top">1</field>
<field name="padding_left">1</field>
<field name="widget" eval="False"/>
<field name="text_decoration">line-through</field>
</record>
<record id="label_section_letter_101x50_product_promo_price" model="print.product.label.section">
<field name="template_id" ref="label_template_letter_101x50"/>
<field name="sequence">5</field>
<field name="type">promo_price</field>
<field name="value_format">%.2f</field>
<field name="height">9</field>
<field name="position">right</field>
<field name="width">50</field>
<field name="align">left</field>
<field name="font_size">32</field>
<field name="font_weight">bold</field>
<field name="padding_top">1</field>
</record>
</odoo>

View File

@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="label_template_custom_50x25" model="print.product.label.template">
<field name="sequence">1</field>
<field name="name">Label 50x25 mm</field>
<field name="paperformat_id" ref="paperformat_label_custom_50x25"/>
<field name="orientation">Portrait</field>
<field name="cols">1</field>
<field name="rows">1</field>
<field name="width">50</field>
<field name="height">25</field>
</record>
<record id="label_template_custom_100x100" model="print.product.label.template">
<field name="sequence">2</field>
<field name="name">Label 100x100 mm</field>
<field name="paperformat_id" ref="paperformat_label_custom_100x100"/>
<field name="orientation">Portrait</field>
<field name="cols">1</field>
<field name="rows">1</field>
<field name="width">100</field>
<field name="height">99.5</field>
<field name="padding_top">3</field>
<field name="padding_left">3</field>
<field name="padding_right">3</field>
<field name="padding_bottom">3</field>
</record>
<record id="label_template_a4_63x38" model="print.product.label.template">
<field name="sequence">3</field>
<field name="name">Label 63x38 mm (A4: 21 pcs, 3x7)</field>
<field name="paperformat_id" ref="paperformat_label_a4_63x38"/>
<field name="orientation">Portrait</field>
<field name="cols">3</field>
<field name="rows">7</field>
<field name="col_gap">2</field>
<field name="row_gap">2</field>
<field name="width">63.5</field>
<field name="height">38.1</field>
</record>
<record id="label_template_a4_99x38" model="print.product.label.template">
<field name="sequence">4</field>
<field name="name">Label 99x38 mm (A4: 14 pcs, 2x7)</field>
<field name="paperformat_id" ref="paperformat_label_a4_99x38"/>
<field name="orientation">Portrait</field>
<field name="cols">2</field>
<field name="rows">7</field>
<field name="col_gap">1</field>
<field name="row_gap">1</field>
<field name="width">99.1</field>
<field name="height">38.1</field>
<field name="padding_top">2</field>
<field name="padding_left">2</field>
<field name="padding_right">2</field>
<field name="padding_bottom">2</field>
</record>
<record id="label_template_letter_66x25" model="print.product.label.template">
<field name="sequence">5</field>
<field name="name">Label 66x25 mm / 2⅝" x 1" (Letter: 30 pcs, 3x10)</field>
<field name="paperformat_id" ref="paperformat_label_letter_66x25"/>
<field name="orientation">Portrait</field>
<field name="cols">3</field>
<field name="rows">10</field>
<field name="col_gap">3</field>
<field name="row_gap">0</field>
<field name="width">66.54</field>
<field name="height">25.4</field>
<field name="padding_top">1</field>
<field name="padding_left">2</field>
<field name="padding_right">2</field>
<field name="description">1" x 2⅝", Avery 6460. Avery Template Presta 94200. Compatibility: 15660, 15700, 15960, 16460, 16790, 18160, 18260, 18660, 22837, 28660, 32660, 38260, 45160, 48160, 48260, 48360, 48460, 48860, 48960, 5136, 5160, 5260, 55160, 5520, 55360, 5620, 5630, 5660, 58160, 58660, 5960, 6240, 6521, 6525, 6526, 6585, 80509, 8160, 8215, 8250, 8460, 85560, 8620, 8660, 88560, 8860, 8920, 95520, 95915.</field>
</record>
<record id="label_template_letter_101x50" model="print.product.label.template">
<field name="sequence">6</field>
<field name="name">Label 101x50 mm / 4" x 2" (Letter: 10 pcs, 2x5)</field>
<field name="paperformat_id" ref="paperformat_label_letter_101x50"/>
<field name="orientation">Portrait</field>
<field name="cols">2</field>
<field name="rows">5</field>
<field name="col_gap">2</field>
<field name="width">101.6</field>
<field name="height">50.8</field>
<field name="padding_top">3</field>
<field name="padding_left">3</field>
<field name="padding_right">3</field>
<field name="description">4" x 2", Avery 5163. Compatibility: 15513, 15702, 16791, 18163, 18863, 38863, 48163, 48263, 48363, 48463, 48863, 5137, 5263, 55163, 5523, 55463, 58163, 5963, 6427, 6527, 6528, 8163, 8253, 8363, 8463, 85563, 8923, 95523, 95910, 95945.</field>
</record>
</odoo>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Since we are adding pricelists, we activate the feature -->
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('product.group_product_pricelist'))]"/>
</record>
<record id="main_pricelist" model="product.pricelist" forcecreate="True">
<field name="name">Regular Pricelist</field>
<field name="currency_id" ref="base.USD"/>
<field name="company_id" ref="base.main_company"/>
<field name="active" eval="True"/>
</record>
<record id="promo_pricelist" model="product.pricelist" forcecreate="True">
<field name="name">Promo Pricelist</field>
<field name="currency_id" ref="base.USD"/>
<field name="company_id" ref="base.main_company"/>
<field name="active" eval="True"/>
</record>
<record id="pricelist_item_promo_product" model="product.pricelist.item">
<field name="pricelist_id" ref="promo_pricelist"/>
<field name="applied_on">0_product_variant</field>
<field name="product_id" ref="product.product_product_4c"/>
<field name="compute_price">percentage</field>
<field name="percent_price">33</field>
</record>
<record id="pricelist_item_promo_global" model="product.pricelist.item">
<field name="pricelist_id" ref="promo_pricelist"/>
<field name="applied_on">3_global</field>
<field name="compute_price">percentage</field>
<field name="percent_price">5</field>
</record>
</odoo>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="base.main_company" model="res.company">
<field name="print_label_preview_product_id" ref="product.product_product_4c"/>
<field name="print_label_preview_sale_pricelist_id" ref="main_pricelist"/>
<field name="print_label_preview_sale_pricelist_id" ref="promo_pricelist"/>
</record>
</odoo>

View 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

View File

@@ -0,0 +1,717 @@
# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)
# License OPL-1 (https://www.odoo.com/documentation/19.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(
'fusion_labels_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('fusion_labels_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()

View File

@@ -0,0 +1,267 @@
# Copyright © 2025 Nexa Systems Inc (https://nexasystems.ca)
# @author: Nexa Systems Inc (support@nexasystems.ca)
# License OPL-1 (https://www.odoo.com/documentation/19.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('fusion_labels.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(
'fusion_labels.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('fusion_labels_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())

View File

@@ -0,0 +1,15 @@
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('fusion_labels.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

View File

@@ -0,0 +1,15 @@
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('fusion_labels.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

View File

@@ -0,0 +1,27 @@
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],
)

View File

@@ -0,0 +1,35 @@
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',
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,
)
show_label_template_limit = fields.Integer(
string='Template Display Limit',
config_parameter='fusion_labels.show_label_template_limit',
default=7,
)
barcode_multiplier = fields.Integer(
string='Barcode Size Multiplier',
config_parameter='fusion_labels_pro.barcode_multiplier',
default=1,
)

View File

@@ -0,0 +1,20 @@
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).',
)

View File

@@ -0,0 +1 @@
from . import product_label_report

View File

@@ -0,0 +1,13 @@
from odoo import models
class ReportFusionProductLabelFromTemplate(models.AbstractModel):
_name = 'report.fusion_labels.report_product_label_from_template'
_description = 'Custom Product Label Report'
def _get_report_values(self, docids, data):
labels = self.env['print.product.label.line'].browse(data.get('ids', []))
return {
'doc_model': 'print.product.label.line',
'doc_ids': labels.ids,
'docs': labels,
'data': data.get('data'),
}

View File

@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="paperformat_label_custom_50x25" model="report.paperformat">
<field name="name">Label: 50x25 mm</field>
<field name="format">custom</field>
<field name="page_height">25</field>
<field name="page_width">50</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="header_spacing">0</field>
<field name="header_line" eval="False"/>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="paperformat_label_custom_100x100" model="report.paperformat">
<field name="name">Label: 100x100mm</field>
<field name="format">custom</field>
<field name="page_height">100</field>
<field name="page_width">100</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="header_spacing">0</field>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="paperformat_label_a4_63x38" model="report.paperformat">
<field name="name">Label: A4 63x38mm</field>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">9</field>
<field name="margin_bottom">0</field>
<field name="margin_left">8</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="paperformat_label_a4_99x38" model="report.paperformat">
<field name="name">Label: A4 99x38mm</field>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">10</field>
<field name="margin_bottom">0</field>
<field name="margin_left">5</field>
<field name="margin_right">5</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="paperformat_label_letter_66x25" model="report.paperformat">
<field name="name">Label: Letter 1"x2⅝"</field>
<field name="format">Letter</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">12</field>
<field name="margin_bottom">8</field>
<field name="margin_left">4.76</field>
<field name="margin_right">4.76</field>
<field name="disable_shrinking" eval="True"/>
<field name="header_spacing">0</field>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
<record id="paperformat_label_letter_101x50" model="report.paperformat">
<field name="name">Label: Letter 4"x2"</field>
<field name="format">Letter</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">12.7</field>
<field name="margin_bottom">8</field>
<field name="margin_left">4.76</field>
<field name="margin_right">4.76</field>
<field name="disable_shrinking" eval="True"/>
<field name="header_spacing">0</field>
<field name="dpi">96</field>
<field name="default" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="product_label_from_template">
<div name="label_div">
<div name="transform_div">
<t t-foreach="label.wizard_id.template_id.section_ids" t-as="section">
<div t-if="section.active" t-attf-style="{{section.get_html_style(label)}}">
<t t-set="value" t-value="section.get_value(label)"/>
<t t-if="section.widget == 'barcode'">
<t t-set="barcode_div_size" t-value="'width: 85%; height: 100%;'"/>
<t t-set="barcode_size" t-value="section.get_barcode_size()"/>
<!-- Barcode parameters: addons/web/controllers/report.py(#59 - report_barcode()), odoo/addons/base/models/ir_actions_report.py(#675 - barcode()) -->
<div t-out="str(value)" style="padding: 0; height: 100%;" t-options="{'widget': 'barcode', 'width': barcode_size['width'], 'height': barcode_size['height'], 'quiet': 0, 'symbology': section.barcode_symbology, 'img_style': barcode_div_size, 'humanreadable': int(section.barcode_is_humanreadable)}"/>
</t>
<t t-elif="section.widget == 'price'">
<div t-field="label.price" t-options="{'widget': 'monetary', 'label_price': True, 'display_currency': label.currency_id}"/>
</t>
<t t-elif="section.widget == 'qr_code'">
<t t-set="qr_code_size_px" t-value="section.height * 40"/>
<img t-att-src="'/report/barcode/?barcode_type=QR&amp;value=%s&amp;width=%d&amp;height=%d'%(value, qr_code_size_px, qr_code_size_px)" t-attf-style="display: block; height: 100%; width: {{ section.height - section.padding_top - section.padding_bottom }}mm;"/>
</t>
<t t-elif="section.widget == 'image'">
<div t-attf-style="width:100%;height:100%;background-image:url({{ section.get_image_url(label) }});background-size:contain;background-repeat:no-repeat;background-position:center;"/>
</t>
<t t-elif="section.type == 'image'">
<div t-attf-style="width:100%;height:100%;background-image:url(/web/image/print.product.label.section/{{ section.id }}/image);background-size:contain;background-repeat:no-repeat;background-position:center;"/>
</t>
<t t-elif="section.type == 'product_attributes'">
<t t-if="section.widget == 'attribute_list'">
<div t-foreach="section.get_attribute_data(label)" t-as="attr">
<span t-if="section.with_product_attribute_name" t-out="attr['name']" class="mx-1"/>
<t t-foreach="attr['values']" t-as="attr_value">
<span t-att-class="'badge rounded bg-%s text-%s mr-1 px-2 py-1' % ('dark' if attr_value['active'] else 'light', 'light' if attr_value['active'] else 'dark')">
<t t-out="attr_value['name']"/>
</span>
</t>
</div>
</t>
<div t-else="" t-foreach="section.get_attribute_data(label)" t-as="attr" class="d-inline-flex flex-wrap">
<span class="mw-100 mx-1">
<t t-if="section.with_product_attribute_name" class="mr-1"><t t-out="attr['name']"/>: </t>
<t t-foreach="attr['values']" t-as="attr_value">
<span t-if="attr_value['active']" t-out="attr_value['name']" style="padding-left: 3px;"/>
</t>
</span>
</div>
</t>
<t t-elif="section.type == 'multi_price'">
<t t-foreach="section.get_value(label)" t-as="price">
<t t-out="price['amount']"/>
<small>(from <t t-out="price['qty']"/> <t t-out="label.product_id.uom_id.name"/>)</small>
<br/>
</t>
</t>
<div t-else="" t-out="value"/>
</div>
</t>
</div>
</div>
</template>
<template id="report_product_label_from_template" inherit_id="fusion_labels.report_product_label_from_template">
<xpath expr="//t[@t-call='web.basic_layout']" position="inside">
<style>body {margin: 0 !important; padding: 0 !important;}</style>
<t t-set="index" t-value="0"/>
<t t-set="cols" t-value="data.get('cols', 1)"/>
<t t-set="rows" t-value="data.get('rows', 1)"/>
<t t-set="labels_per_sheet" t-value="cols * rows"/>
<t t-set="skip" t-value="data.get('skip_places', 0)"/>
<t t-foreach="docs" t-as="label">
<t t-set="qty" t-value="1"/>
<t t-if="label.qty" t-set="qty" t-value="label.qty"/>
<t t-if="labels_per_sheet" t-foreach="range(not index and (qty + skip) or qty)" t-as="label_index">
<div t-if="index % labels_per_sheet == 0" style="page-break-before: always;"/>
<!-- Horizontal gap between labels -->
<t t-if="index and index % cols == 0 and index % labels_per_sheet != 0" t-set="horizontal_gap" t-value="'height: 1px; margin-bottom: %.2fmm;' % data.get('row_gap', 0)"/>
<t t-else="" t-set="horizontal_gap" t-value="''"/>
<!-- New line -->
<div t-if="index % cols == 0" t-att-style="'clear:both;' + horizontal_gap"/>
<!-- Vertical gap between labels -->
<t t-if="index % cols != 0" t-set="vertical_gap" t-value="'margin-left: %.2fmm;' % data.get('col_gap', 0)"/>
<t t-else="" t-set="vertical_gap" t-value="''"/>
<!-- Label <div> block -->
<div t-att-style="data.get('label_style', '') + vertical_gap">
<t t-if="index &gt;= skip" t-call="fusion_labels_pro.product_label_from_template"/>
</div>
<t t-set="index" t-value="index + 1"/>
</t>
</t>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,5 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_print_product_label_template_user,access_print_product_label_template_user,model_print_product_label_template,base.group_user,1,1,1,1
access_print_product_label_section_user,access_print_product_label_section_user,model_print_product_label_section,base.group_user,1,1,1,1
access_print_product_label_template_add_user,access_print_product_label_template_add_user,model_print_product_label_template_add,base.group_user,1,1,1,1
access_print_product_label_preview_user,access_print_product_label_preview_user,model_print_product_label_preview,base.group_user,1,1,0,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_print_product_label_template_user access_print_product_label_template_user model_print_product_label_template base.group_user 1 1 1 1
3 access_print_product_label_section_user access_print_product_label_section_user model_print_product_label_section base.group_user 1 1 1 1
4 access_print_product_label_template_add_user access_print_product_label_template_add_user model_print_product_label_template_add base.group_user 1 1 1 1
5 access_print_product_label_preview_user access_print_product_label_preview_user model_print_product_label_preview base.group_user 1 1 0 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,113 @@
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row mt-3">
<div class="col-12 text-right d-none d-md-block">
<span class="m-2" style="float: right; background-color: #694D65; border-color: #694D65; color: #FFFFFF; padding: 5px 18px; border-radius: 15px 0 15px 0;"><i class="fa fa-check"></i> Enterprise</span>
<span class="m-2" style="float: right; background-color: #6F649A; border-color: #6F649A; color: #FFFFFF; padding: 5px 18px; border-radius: 15px 0 15px 0;"><i class="fa fa-check"></i> Community</span>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12 d-flex d-column">
<h2 class="h1 align-self-center text-center" style="color: #154577; font-weight: 900;">Fusion Labels Pro - Product Label Builder</h2>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12">
<div class="h2 text-left ml8" style="color: #FEA621;"><i class="fa fa-newspaper-o mr8"></i>Description</div>
<div class="media p-2 pt-4 overflow-hidden" style="border-color: #FEA621 !important; border-radius: 0 15px 0 0; border-top-style: solid; border-right-style: solid;">
<div class="py-2 px-md-5 lead w-100" style="line-height: 2.5rem;">
<p>Professional product barcode label builder and designer. Create custom label templates with configurable sections, advanced barcode generation, and direct printing support.</p>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12">
<div class="h2 text-left ml8" style="color: #FEA621;"><i class="fa fa-check-square-o mr8"></i>Features</div>
<div class="s_features_grid p-2 pt-4 o_colored_level" style="border-color: #FEA621 !important; border-radius: 0 15px 0 0; border-top-style: solid; border-right-style: solid; background-color: #ffa81f14;">
<div class="container p-0">
<div class="row px-md-5">
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-paint-brush rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Label Builder</div>
<p>Visual template designer with drag-and-drop sections</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-barcode rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Advanced Barcodes</div>
<p>Multiple barcode formats and customization</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-print rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Direct Print</div>
<p>Print labels directly without downloading PDF</p>
</div>
</div>
</div>
</div>
<div class="col-lg-6 s_col_no_bgcolor pb24">
<div class="row">
<div class="col-lg-12">
<i class="fa fa-2x float-left mr-3 bg-warning fa-search rounded-circle" style="line-height:5rem; height:5rem; width:5rem; text-align:center;"></i>
<div>
<div class="h4 pt-2">Preview</div>
<p>Preview labels before printing</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row oe_spaced">
<div class="col-12">
<div class="h2 text-left ml8" style="color: #FEA621;"><i class="fa fa-barcode mr8"></i>Label Samples</div>
<div class="media p-2 pt-4 border-top border-right" style="border-color: #FEA621 !important; border-radius: 0 15px 0 0;">
<div class="media-body">
<div class="py-2 px-md-5" style="line-height: 2rem;">Labels are generated in PDF format with various sizes:</div>
<div class="text-center my-5 mx-md-5">
<img src="product_barcode_label_100x100mm.png" class="w-100 w-lg-25 img-fluid rounded shadow-lg border" alt="Product Labels 100x100mm">
</div>
<div class="text-center my-5 mx-md-5">
<img src="product_barcode_label_A4_63x38mm.png" class="w-100 w-lg-50 img-fluid rounded shadow-lg border" alt="Product Labels A4 63x38mm">
</div>
</div>
</div>
</div>
</div>
</section>
<section class="oe_container" style="font-family: 'Montserrat', 'Odoo Unicode Support Noto', sans-serif;">
<div class="row mt-5 mb-4 align-items-center">
<div class="col-6 text-left">Version: 19.0.1.0.0</div>
<div class="col-6 text-right"><small>Copyright &copy; <a href="https://nexasystems.ca" target="_blank">Nexa Systems Inc</a></small></div>
</div>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1 @@
from . import test_access_rights

Some files were not shown because too many files have changed in this diff Show More