summaryrefslogtreecommitdiff
path: root/addons/web/models/base_document_layout.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/models/base_document_layout.py')
-rw-r--r--addons/web/models/base_document_layout.py278
1 files changed, 278 insertions, 0 deletions
diff --git a/addons/web/models/base_document_layout.py b/addons/web/models/base_document_layout.py
new file mode 100644
index 00000000..3396e0db
--- /dev/null
+++ b/addons/web/models/base_document_layout.py
@@ -0,0 +1,278 @@
+# -*- coding: utf-8 -*-
+from PIL import Image
+
+from odoo import api, fields, models, tools
+
+from odoo.modules import get_resource_path
+
+try:
+ import sass as libsass
+except ImportError:
+ # If the `sass` python library isn't found, we fallback on the
+ # `sassc` executable in the path.
+ libsass = None
+
+DEFAULT_PRIMARY = '#000000'
+DEFAULT_SECONDARY = '#000000'
+
+
+class BaseDocumentLayout(models.TransientModel):
+ """
+ Customise the company document layout and display a live preview
+ """
+
+ _name = 'base.document.layout'
+ _description = 'Company Document Layout'
+
+ company_id = fields.Many2one(
+ 'res.company', default=lambda self: self.env.company, required=True)
+
+ logo = fields.Binary(related='company_id.logo', readonly=False)
+ preview_logo = fields.Binary(related='logo', string="Preview logo")
+ report_header = fields.Text(related='company_id.report_header', readonly=False)
+ report_footer = fields.Text(related='company_id.report_footer', readonly=False)
+
+ # The paper format changes won't be reflected in the preview.
+ paperformat_id = fields.Many2one(related='company_id.paperformat_id', readonly=False)
+
+ external_report_layout_id = fields.Many2one(related='company_id.external_report_layout_id', readonly=False)
+
+ font = fields.Selection(related='company_id.font', readonly=False)
+ primary_color = fields.Char(related='company_id.primary_color', readonly=False)
+ secondary_color = fields.Char(related='company_id.secondary_color', readonly=False)
+
+ custom_colors = fields.Boolean(compute="_compute_custom_colors", readonly=False)
+ logo_primary_color = fields.Char(compute="_compute_logo_colors")
+ logo_secondary_color = fields.Char(compute="_compute_logo_colors")
+
+ report_layout_id = fields.Many2one('report.layout')
+
+ # All the sanitization get disabled as we want true raw html to be passed to an iframe.
+ preview = fields.Html(compute='_compute_preview',
+ sanitize=False,
+ sanitize_tags=False,
+ sanitize_attributes=False,
+ sanitize_style=False,
+ sanitize_form=False,
+ strip_style=False,
+ strip_classes=False)
+
+ # Those following fields are required as a company to create invoice report
+ partner_id = fields.Many2one(related='company_id.partner_id', readonly=True)
+ phone = fields.Char(related='company_id.phone', readonly=True)
+ email = fields.Char(related='company_id.email', readonly=True)
+ website = fields.Char(related='company_id.website', readonly=True)
+ vat = fields.Char(related='company_id.vat', readonly=True)
+ name = fields.Char(related='company_id.name', readonly=True)
+ country_id = fields.Many2one(related="company_id.country_id", readonly=True)
+
+ @api.depends('logo_primary_color', 'logo_secondary_color', 'primary_color', 'secondary_color',)
+ def _compute_custom_colors(self):
+ for wizard in self:
+ logo_primary = wizard.logo_primary_color or ''
+ logo_secondary = wizard.logo_secondary_color or ''
+ # Force lower case on color to ensure that FF01AA == ff01aa
+ wizard.custom_colors = (
+ wizard.logo and wizard.primary_color and wizard.secondary_color
+ and not(
+ wizard.primary_color.lower() == logo_primary.lower()
+ and wizard.secondary_color.lower() == logo_secondary.lower()
+ )
+ )
+
+ @api.depends('logo')
+ def _compute_logo_colors(self):
+ for wizard in self:
+ if wizard._context.get('bin_size'):
+ wizard_for_image = wizard.with_context(bin_size=False)
+ else:
+ wizard_for_image = wizard
+ wizard.logo_primary_color, wizard.logo_secondary_color = wizard_for_image._parse_logo_colors()
+
+ @api.depends('report_layout_id', 'logo', 'font', 'primary_color', 'secondary_color', 'report_header', 'report_footer')
+ def _compute_preview(self):
+ """ compute a qweb based preview to display on the wizard """
+
+ styles = self._get_asset_style()
+
+ for wizard in self:
+ if wizard.report_layout_id:
+ preview_css = self._get_css_for_preview(styles, wizard.id)
+ ir_ui_view = wizard.env['ir.ui.view']
+ wizard.preview = ir_ui_view._render_template('web.report_invoice_wizard_preview', {'company': wizard, 'preview_css': preview_css})
+ else:
+ wizard.preview = False
+
+ @api.onchange('company_id')
+ def _onchange_company_id(self):
+ for wizard in self:
+ wizard.logo = wizard.company_id.logo
+ wizard.report_header = wizard.company_id.report_header
+ wizard.report_footer = wizard.company_id.report_footer
+ wizard.paperformat_id = wizard.company_id.paperformat_id
+ wizard.external_report_layout_id = wizard.company_id.external_report_layout_id
+ wizard.font = wizard.company_id.font
+ wizard.primary_color = wizard.company_id.primary_color
+ wizard.secondary_color = wizard.company_id.secondary_color
+ wizard_layout = wizard.env["report.layout"].search([
+ ('view_id.key', '=', wizard.company_id.external_report_layout_id.key)
+ ])
+ wizard.report_layout_id = wizard_layout or wizard_layout.search([], limit=1)
+
+ if not wizard.primary_color:
+ wizard.primary_color = wizard.logo_primary_color or DEFAULT_PRIMARY
+ if not wizard.secondary_color:
+ wizard.secondary_color = wizard.logo_secondary_color or DEFAULT_SECONDARY
+
+ @api.onchange('custom_colors')
+ def _onchange_custom_colors(self):
+ for wizard in self:
+ if wizard.logo and not wizard.custom_colors:
+ wizard.primary_color = wizard.logo_primary_color or DEFAULT_PRIMARY
+ wizard.secondary_color = wizard.logo_secondary_color or DEFAULT_SECONDARY
+
+ @api.onchange('report_layout_id')
+ def _onchange_report_layout_id(self):
+ for wizard in self:
+ wizard.external_report_layout_id = wizard.report_layout_id.view_id
+
+ @api.onchange('logo')
+ def _onchange_logo(self):
+ for wizard in self:
+ # It is admitted that if the user puts the original image back, it won't change colors
+ company = wizard.company_id
+ # at that point wizard.logo has been assigned the value present in DB
+ if wizard.logo == company.logo and company.primary_color and company.secondary_color:
+ continue
+
+ if wizard.logo_primary_color:
+ wizard.primary_color = wizard.logo_primary_color
+ if wizard.logo_secondary_color:
+ wizard.secondary_color = wizard.logo_secondary_color
+
+ def _parse_logo_colors(self, logo=None, white_threshold=225):
+ """
+ Identifies dominant colors
+
+ First resizes the original image to improve performance, then discards
+ transparent colors and white-ish colors, then calls the averaging
+ method twice to evaluate both primary and secondary colors.
+
+ :param logo: alternate logo to process
+ :param white_threshold: arbitrary value defining the maximum value a color can reach
+
+ :return colors: hex values of primary and secondary colors
+ """
+ self.ensure_one()
+ logo = logo or self.logo
+ if not logo:
+ return False, False
+
+ # The "===" gives different base64 encoding a correct padding
+ logo += b'===' if type(logo) == bytes else '==='
+ try:
+ # Catches exceptions caused by logo not being an image
+ image = tools.image_fix_orientation(tools.base64_to_image(logo))
+ except Exception:
+ return False, False
+
+ base_w, base_h = image.size
+ w = int(50 * base_w / base_h)
+ h = 50
+
+ # Converts to RGBA (if already RGBA, this is a noop)
+ image_converted = image.convert('RGBA')
+ image_resized = image_converted.resize((w, h), resample=Image.NEAREST)
+
+ colors = []
+ for color in image_resized.getcolors(w * h):
+ if not(color[1][0] > white_threshold and
+ color[1][1] > white_threshold and
+ color[1][2] > white_threshold) and color[1][3] > 0:
+ colors.append(color)
+
+ if not colors: # May happen when the whole image is white
+ return False, False
+ primary, remaining = tools.average_dominant_color(colors)
+ secondary = tools.average_dominant_color(
+ remaining)[0] if len(remaining) > 0 else primary
+
+ # Lightness and saturation are calculated here.
+ # - If both colors have a similar lightness, the most colorful becomes primary
+ # - When the difference in lightness is too great, the brightest color becomes primary
+ l_primary = tools.get_lightness(primary)
+ l_secondary = tools.get_lightness(secondary)
+ if (l_primary < 0.2 and l_secondary < 0.2) or (l_primary >= 0.2 and l_secondary >= 0.2):
+ s_primary = tools.get_saturation(primary)
+ s_secondary = tools.get_saturation(secondary)
+ if s_primary < s_secondary:
+ primary, secondary = secondary, primary
+ elif l_secondary > l_primary:
+ primary, secondary = secondary, primary
+
+ return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary)
+
+ @api.model
+ def action_open_base_document_layout(self, action_ref=None):
+ if not action_ref:
+ action_ref = 'web.action_base_document_layout_configurator'
+ res = self.env["ir.actions.actions"]._for_xml_id(action_ref)
+ self.env[res["res_model"]].check_access_rights('write')
+ return res
+
+ def document_layout_save(self):
+ # meant to be overridden
+ return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
+
+ def _get_asset_style(self):
+ """
+ Compile the style template. It is a qweb template expecting company ids to generate all the code in one batch.
+ We give a useless company_ids arg, but provide the PREVIEW_ID arg that will prepare the template for
+ '_get_css_for_preview' processing later.
+ :return:
+ """
+ template_style = self.env.ref('web.styles_company_report', raise_if_not_found=False)
+ if not template_style:
+ return b''
+
+ company_styles = template_style._render({
+ 'company_ids': self,
+ })
+
+ return company_styles
+
+ @api.model
+ def _get_css_for_preview(self, scss, new_id):
+ """
+ Compile the scss into css.
+ """
+ css_code = self._compile_scss(scss)
+ return css_code
+
+ @api.model
+ def _compile_scss(self, scss_source):
+ """
+ This code will compile valid scss into css.
+ Parameters are the same from odoo/addons/base/models/assetsbundle.py
+ Simply copied and adapted slightly
+ """
+
+ # No scss ? still valid, returns empty css
+ if not scss_source.strip():
+ return ""
+
+ precision = 8
+ output_style = 'expanded'
+ bootstrap_path = get_resource_path('web', 'static', 'lib', 'bootstrap', 'scss')
+
+ try:
+ return libsass.compile(
+ string=scss_source,
+ include_paths=[
+ bootstrap_path,
+ ],
+ output_style=output_style,
+ precision=precision,
+ )
+ except libsass.CompileError as e:
+ raise libsass.CompileError(e.args[0])