diff --git a/addons/account/models/company.py b/addons/account/models/company.py index ed1a8fdacbc..d5bcc3d6fa0 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -459,13 +459,6 @@ Best Regards,''')) """ Mark the dashboard onboarding panel as closed. """ self.env.company.account_dashboard_onboarding_state = 'closed' - @api.model - def action_open_account_onboarding_invoice_layout(self): - """ Onboarding step for the invoice layout. """ - action = self.env.ref('account.action_open_account_onboarding_invoice_layout').read()[0] - action['res_id'] = self.env.company.id - return action - @api.model def action_open_account_onboarding_sale_tax(self): """ Onboarding step for the invoice layout. """ diff --git a/addons/account/static/tests/account_dashboard_setup_bar_tests.js b/addons/account/static/tests/account_dashboard_setup_bar_tests.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js new file mode 100644 index 00000000000..4043254aae9 --- /dev/null +++ b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js @@ -0,0 +1,49 @@ +odoo.define('account.dashboard.setup.tour', function (require) { + "use strict"; + + var core = require('web.core'); + var tour = require('web_tour.tour'); + + var _t = core._t; + + tour.register('account_render_report', { + test: true, + url: '/web', + }, [tour.STEPS.SHOW_APPS_MENU_ITEM, + { + trigger: '.o_app[data-menu-xmlid="account.menu_finance"]', + position: 'bottom', + edition: 'community' + }, { + trigger: '.o_app[data-menu-xmlid="account_accountant.menu_accounting"]', + position: 'bottom', + edition: 'enterprise' + }, { + trigger: 'a:contains(' + _t("Customer Invoices") + ')', + edition: 'enterprise' + }, { + trigger: '.o_data_row:first', + extra_trigger: '.breadcrumb', + }, { + trigger: '.o_control_panel button:contains("' + _t('Print') + '")', + }, { + trigger: '.o_control_panel div.o_dropdown_menu a:contains("' + _t('Invoices without Payment') + '")', + }, { + trigger: 'iframe .o_report_layout_standard h2', + content: 'Primary color is correct', + run: function () { + if (this.$anchor.css('color') !== "rgb(18, 52, 86)") { + console.error('The primary color should be the one set on the company.'); + } + }, + }, { + trigger: 'iframe .o_report_layout_standard #informations div strong', + content: 'Secondary color is correct', + run: function () { + if (this.$anchor.css('color') !== "rgb(120, 145, 1)") { + console.error('The secondary color should be the one set on the company.'); + } + }, + } + ]); +}); diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index c2612d50551..5c822cca737 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -19,3 +19,4 @@ from . import test_templates_consistency from . import test_account_fiscal_year from . import test_account_all_l10n from . import test_reconciliation_matching_rules +from . import test_account_onboarding \ No newline at end of file diff --git a/addons/account/tests/account_test_no_chart.py b/addons/account/tests/account_test_no_chart.py index ca1c971d0a6..ba145b02d48 100644 --- a/addons/account/tests/account_test_no_chart.py +++ b/addons/account/tests/account_test_no_chart.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests.common import SavepointCase +from odoo.tests.common import SavepointCase, HttpCase, tagged class TestAccountNoChartCommon(SavepointCase): diff --git a/addons/account/tests/test_account_onboarding.py b/addons/account/tests/test_account_onboarding.py new file mode 100644 index 00000000000..c4839a35a32 --- /dev/null +++ b/addons/account/tests/test_account_onboarding.py @@ -0,0 +1,18 @@ +from odoo.tests.common import tagged, HttpCase + + +@tagged('-at_install', 'post_install', 'document_layout') +class TestAccountDocumentLayout(HttpCase): + + def test_render_account_document_layout(self): + report_layout = self.env.ref('web.report_layout_standard') + main_company = self.env.ref('base.main_company') + main_company.write({ + 'primary_color': '#123456', + 'secondary_color': '#789101', + 'external_report_layout_id': report_layout.view_id.id, + }) + self.env.ref('account.account_invoices_without_payment').write({ + 'report_type': 'qweb-html', + }) + self.start_tour("/web", 'account_render_report', login='admin', timeout=200) diff --git a/addons/account/views/account.xml b/addons/account/views/account.xml index 448363ddec2..b06b122a6fc 100644 --- a/addons/account/views/account.xml +++ b/addons/account/views/account.xml @@ -27,7 +27,6 @@ - + diff --git a/addons/account/views/account_onboarding_templates.xml b/addons/account/views/account_onboarding_templates.xml index 8393efd7d79..1f2af4843fb 100644 --- a/addons/account/views/account_onboarding_templates.xml +++ b/addons/account/views/account_onboarding_templates.xml @@ -8,8 +8,8 @@ Customize Looks great! - - + + @@ -94,27 +94,6 @@ - - - account.invoice.layout.onboarding.form - res.company - - primary - 1000 - - - +
+
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index d537698432f..a67d4e19cbc 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -42,6 +42,7 @@ QUnit.module('basic_fields', { selection: {string: "Selection", type: "selection", searchable:true, selection: [['normal', 'Normal'],['blocked', 'Blocked'],['done', 'Done']]}, document: {string: "Binary", type: "binary"}, + hex_color: {string: "hexadecimal color", type: "char"}, }, records: [{ id: 1, @@ -57,6 +58,7 @@ QUnit.module('basic_fields', { trululu: 4, selection: 'blocked', document: 'coucou==\n', + hex_color: '#ff0000', }, { id: 2, display_name: "second record", @@ -2940,7 +2942,7 @@ QUnit.module('basic_fields', { await testUtils.form.clickEdit(form); await testUtils.dom.openDatepicker(form.$('.o_datepicker')); - + assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget', "datepicker should be opened"); form.el.dispatchEvent(new Event('scroll')); @@ -5814,6 +5816,68 @@ QUnit.module('basic_fields', { form.destroy(); }); + + QUnit.module('FieldColor'); + + QUnit.test('Field Color: default widget state', async function (assert) { + assert.expect(3); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + await testUtils.dom.click(form.$('.o_field_color')); + await testUtils.nextTick(); + assert.containsOnce($, '.modal'); + assert.containsNone($('.modal'), '.o_opacity_slider', + "Opacity slider should not be present"); + assert.containsNone($('.modal'), '.o_opacity_input', + "Opacity input should not be present"); + + await testUtils.dom.click($('.modal .btn:contains("Discard")')); + + form.destroy(); + }); + + QUnit.test('Field Color: pick and reset colors', async function (assert) { + assert.expect(2); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual($('.o_field_color').css('backgroundColor'), 'rgb(255, 0, 0)', + "Background of the color field should be initially red"); + + await testUtils.dom.click(form.$('.o_field_color')); + await testUtils.fields.editAndTrigger($('.modal .o_hex_input'), '#00ff00', ['change']); + await testUtils.dom.click($('.modal .btn:contains("Choose")')); + + assert.strictEqual($('.o_field_color').css('backgroundColor'), 'rgb(0, 255, 0)', + "Background of the color field should be updated to green"); + + form.destroy(); + }); }); }); }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index b5e098d230c..229d020bfbd 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2131,6 +2131,42 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.module('FieldSelectionFont'); + + QUnit.test('FieldSelectionFont displays the correct fonts on options', async function (assert) { + assert.expect(4); + + this.data.partner.fields.fonts = { + type: "selection", + selection: [['Lato', "Lato"], ['Oswald', "Oswald"]], + default: 'Lato', + string: "Fonts", + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '', + }); + var options = form.$('.o_field_widget[name="fonts"] > option'); + + assert.strictEqual(form.$('.o_field_widget[name="fonts"]').css('fontFamily'), 'Lato', + "Widget font should be default (Lato)"); + assert.strictEqual($(options[0]).css('fontFamily'), 'Lato', + "Option 0 should have the correct font (Lato)"); + assert.strictEqual($(options[1]).css('fontFamily'), 'Oswald', + "Option 1 should have the correct font (Oswald)"); + + await testUtils.fields.editSelect(form.$('.o_field_widget[name="fonts"]'), '"Oswald"'); + assert.strictEqual(form.$('.o_field_widget[name="fonts"]').css('fontFamily'), 'Oswald', + "Widget font should be updated (Oswald)"); + + form.destroy(); + }); + QUnit.module('FieldMany2ManyCheckBoxes'); QUnit.test('widget many2many_checkboxes', async function (assert) { diff --git a/addons/web/views/report_templates.xml b/addons/web/views/report_templates.xml index 4865160be0e..0ba32cda223 100644 --- a/addons/web/views/report_templates.xml +++ b/addons/web/views/report_templates.xml @@ -18,6 +18,9 @@ + + + @@ -330,6 +333,8 @@
+ + @@ -351,6 +356,8 @@ + +
@@ -403,6 +410,7 @@ + + diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py index 1a3658ceaef..aed63a8501d 100644 --- a/odoo/addons/base/__manifest__.py +++ b/odoo/addons/base/__manifest__.py @@ -58,6 +58,7 @@ The kernel of Odoo, needed for all installation. 'wizard/base_export_language_views.xml', 'wizard/base_update_translations_views.xml', 'wizard/base_partner_merge_views.xml', + 'wizard/base_document_layout_views.xml', 'data/ir_actions_data.xml', 'data/ir_demo_failure_data.xml', 'views/res_company_views.xml', diff --git a/odoo/addons/base/data/res_company_data.xml b/odoo/addons/base/data/res_company_data.xml index 50c19767c40..36c3eda67b3 100644 --- a/odoo/addons/base/data/res_company_data.xml +++ b/odoo/addons/base/data/res_company_data.xml @@ -8,8 +8,18 @@ + + + + JG8tY29tcGFueS1mb250OiAnTGF0byc7JG8tY29tcGFueS1wcmltYXJ5LWNvbG9yOiByZ2IoMCwgMCwgMCk7JG8tY29tcGFueS1zZWNvbmRhcnktY29sb3I6IHJnYigwLCAwLCAwKTs= + text/scss + res.company.scss + binary + /web/static/src/scss/report_styles.scss + + - + diff --git a/odoo/addons/base/models/__init__.py b/odoo/addons/base/models/__init__.py index d74b68c77d8..bdc3d0b1a0e 100644 --- a/odoo/addons/base/models/__init__.py +++ b/odoo/addons/base/models/__init__.py @@ -29,7 +29,7 @@ from . import ir_property from . import ir_module from . import ir_demo from . import ir_demo_failure - +from . import report_layout from . import report_paperformat from . import image_mixin diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py index 1e76c8c1cf8..820d852e6b5 100644 --- a/odoo/addons/base/models/ir_actions_report.py +++ b/odoo/addons/base/models/ir_actions_report.py @@ -791,18 +791,7 @@ class IrActionsReport(models.Model): """ discard_logo_check = self.env.context.get('discard_logo_check') if self.env.is_admin() and ((not self.env.company.external_report_layout_id) or (not discard_logo_check and not self.env.company.logo)) and config: - template = self.env.ref('base.view_company_report_form_with_print') if self.env.context.get('from_transient_model', False) else self.env.ref('base.view_company_report_form') - return { - 'name': _('Choose Your Document Layout'), - 'type': 'ir.actions.act_window', - 'context': {'default_report_name': self.report_name, 'discard_logo_check': True}, - 'view_mode': 'form', - 'res_id': self.env.company.id, - 'res_model': 'res.company', - 'views': [(template.id, 'form')], - 'view_id': template.id, - 'target': 'new', - } + return self.env.ref('base.action_base_document_layout_configurator').read()[0] context = self.env.context if docids: diff --git a/addons/web/models/report_layout.py b/odoo/addons/base/models/report_layout.py similarity index 82% rename from addons/web/models/report_layout.py rename to odoo/addons/base/models/report_layout.py index a38f25c2033..e80fbb47c96 100644 --- a/addons/web/models/report_layout.py +++ b/odoo/addons/base/models/report_layout.py @@ -1,18 +1,15 @@ # -*- coding: ascii -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import logging - from odoo import fields, models -_logger = logging.getLogger(__name__) - - -class View(models.Model): +class ReportLayout(models.Model): _name = "report.layout" _description = 'Report Layout' view_id = fields.Many2one('ir.ui.view', 'Document Template', required=True) image = fields.Char(string="Preview image src") pdf = fields.Char(string="Preview pdf src") + + name = fields.Char() diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index 1c4a001096e..0d76629f661 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -94,8 +94,9 @@ class Company(models.Model): base_onboarding_company_state = fields.Selection([ ('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")], string="State of the onboarding company step", default='not_done') favicon = fields.Binary(string="Company Favicon", help="This field holds the image used to display a favicon for a given company.", default=_get_default_favicon) - - + font = fields.Selection([("Lato", "Lato"), ("Roboto", "Roboto"), ("Open Sans", "Open Sans"), ("Montserrat", "Montserrat"), ("Oswald", "Oswald"), ("Raleway", "Raleway")], default="Lato") + primary_color = fields.Char() + secondary_color = fields.Char() _sql_constraints = [ ('name_uniq', 'unique (name)', 'The company name must be unique !') ] @@ -309,3 +310,24 @@ class Company(models.Model): main_company = self.env['res.company'].sudo().search([], limit=1, order="id") return main_company + + def update_scss(self): + """ update the company scss stylesheet """ + scss_properties = [] + if self.primary_color: + scss_properties.append('$o-company-primary-color:%s;' % self.primary_color) + if self.secondary_color: + scss_properties.append('$o-company-secondary-color:%s;' % self.secondary_color) + if self.font: + scss_properties.append('$o-company-font:%s;' % self.font) + scss_string = '\n'.join(scss_properties) + + if not len(scss_string): + scss_string = "" + + scss_data = base64.b64encode((scss_string).encode('utf-8')) + + attachment = self.env['ir.attachment'].search([('name', '=', 'res.company.scss')]) + attachment.write({'datas': scss_data}) + + return '' diff --git a/odoo/addons/base/security/ir.model.access.csv b/odoo/addons/base/security/ir.model.access.csv index d25dddad322..706f68e6f7d 100644 --- a/odoo/addons/base/security/ir.model.access.csv +++ b/odoo/addons/base/security/ir.model.access.csv @@ -96,3 +96,4 @@ "access_ir_logging","ir_logging admin","model_ir_logging","group_erp_manager",1,1,1,1 "paperformat_access_portal","ir_actions_report_paperformat group_portal","model_report_paperformat",,1,0,0,0 "paperformat_access_employee","ir_actions_report_paperformat group_system","model_report_paperformat",group_system,1,1,1,1 +"access_report_layout","access_report_layout","model_report_layout",,1,1,1,1 diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py index bba0d5b6742..db7b07f81af 100644 --- a/odoo/addons/base/tests/__init__.py +++ b/odoo/addons/base/tests/__init__.py @@ -38,3 +38,4 @@ from . import test_res_partner_bank from . import test_reports from . import test_tests_tags from . import test_non_regression +from . import test_base_document_layout diff --git a/odoo/addons/base/tests/fire_small.jpeg b/odoo/addons/base/tests/fire_small.jpeg new file mode 100644 index 00000000000..1dd0909e399 Binary files /dev/null and b/odoo/addons/base/tests/fire_small.jpeg differ diff --git a/odoo/addons/base/tests/odoo.jpg b/odoo/addons/base/tests/odoo.jpg new file mode 100644 index 00000000000..312451a2ae1 Binary files /dev/null and b/odoo/addons/base/tests/odoo.jpg differ diff --git a/odoo/addons/base/tests/overwatch.jpg b/odoo/addons/base/tests/overwatch.jpg new file mode 100644 index 00000000000..38fdaeffc51 Binary files /dev/null and b/odoo/addons/base/tests/overwatch.jpg differ diff --git a/odoo/addons/base/tests/test_base_document_layout.py b/odoo/addons/base/tests/test_base_document_layout.py new file mode 100644 index 00000000000..ec785ea6216 --- /dev/null +++ b/odoo/addons/base/tests/test_base_document_layout.py @@ -0,0 +1,206 @@ +import os +from PIL import Image +from functools import partial + +from odoo.tests import TransactionCase, tagged, Form +from odoo.tools import frozendict, image_to_base64, hex_to_rgb + + +dir_path = os.path.dirname(os.path.realpath(__file__)) +_file_cache = {} + + +class TestBaseDocumentLayoutHelpers(TransactionCase): + # + # Public + # + def setUp(self): + super(TestBaseDocumentLayoutHelpers, self).setUp() + self.color_fields = ['primary_color', 'secondary_color'] + self.company = self.env.company + self.css_color_error = 0 + self._set_templates_and_layouts() + self._set_images() + + def assertColors(self, checked_obj, expected): + _expected_getter = expected.get if isinstance(expected, dict) else partial(getattr, expected) + for fname in self.color_fields: + color1 = getattr(checked_obj, fname) + color2 = _expected_getter(fname) + if self.css_color_error: + self._compare_colors_rgb(color1, color2) + else: + self.assertEqual(color1, color2) + + # + # Private + # + def _compare_colors_rgb(self, color1, color2): + self.assertEqual(bool(color1), bool(color2)) + if not color1: + return + color1 = hex_to_rgb(color1) + color2 = hex_to_rgb(color2) + self.assertEqual(len(color1), len(color2)) + for i in range(len(color1)): + self.assertAlmostEqual(color1[i], color2[i], delta=self.css_color_error) + + def _get_images_for_test(self): + return ['overwatch.jpg', 'odoo.jpg'] + + def _set_images(self): + for fname in self._get_images_for_test(): + fname_split = fname.split('.') + if not fname_split[0] in _file_cache: + with Image.open(os.path.join(dir_path, fname), 'r') as img: + base64_img = image_to_base64(img, 'JPEG') + primary, secondary = self.env['base.document.layout'].create( + {})._parse_logo_colors(base64_img) + _img = frozendict({ + 'img': base64_img, + 'colors': { + 'primary_color': primary, + 'secondary_color': secondary, + }, + }) + _file_cache[fname_split[0]] = _img + self.company_imgs = frozendict(_file_cache) + + def _set_templates_and_layouts(self): + self.layout_template1 = self.env['ir.ui.view'].create({ + 'name': 'layout_template1', + 'key': 'layout_template1', + 'type': 'qweb', + 'arch': '''
''', + }) + self.env['ir.model.data'].create({ + 'name': self.layout_template1.name, + 'model': 'ir.ui.view', + 'module': 'base', + 'res_id': self.layout_template1.id, + }) + self.default_colors = { + 'primary_color': '#000000', + 'secondary_color': '#000000', + } + self.report_layout1 = self.env['report.layout'].create({ + 'view_id': self.layout_template1.id, + 'name': 'report_%s' % self.layout_template1.name, + }) + self.layout_template2 = self.env['ir.ui.view'].create({ + 'name': 'layout_template2', + 'key': 'layout_template2', + 'type': 'qweb', + 'arch': '''
''', + }) + self.env['ir.model.data'].create({ + 'name': self.layout_template2.name, + 'model': 'ir.ui.view', + 'module': 'base', + 'res_id': self.layout_template2.id, + }) + self.report_layout2 = self.env['report.layout'].create({ + 'view_id': self.layout_template2.id, + 'name': 'report_%s' % self.layout_template2.name, + }) + + +@tagged('document_layout') +class TestBaseDocumentLayout(TestBaseDocumentLayoutHelpers): + # Logo change Tests + def test_company_no_color_change_logo(self): + """When neither a logo nor the colors are set + The wizard displays the colors of the report layout + Changing logo means the colors on the wizard change too + Emptying the logo works and doesn't change the colors""" + self.company.write({ + 'primary_color': False, + 'secondary_color': False, + 'logo': False, + 'external_report_layout_id': self.env.ref('base.layout_template1').id, + }) + default_colors = self.default_colors + with Form(self.env['base.document.layout']) as doc_layout: + self.assertColors(doc_layout, default_colors) + self.assertEqual(doc_layout.company_id, self.company) + doc_layout.logo = self.company_imgs['overwatch']['img'] + + self.assertColors(doc_layout, self.company_imgs['overwatch']['colors']) + + doc_layout.logo = '' + self.assertColors(doc_layout, self.company_imgs['overwatch']['colors']) + self.assertEqual(doc_layout.logo, '') + + def test_company_no_color_but_logo_change_logo(self): + """When company colors are not set, but a logo is, + the wizard displays the computed colors from the logo""" + self.company.write({ + 'primary_color': '#ff0080', + 'secondary_color': '#00ff00', + 'logo': self.company_imgs['overwatch']['img'], + }) + + with Form(self.env['base.document.layout']) as doc_layout: + self.assertColors(doc_layout, self.company) + doc_layout.logo = self.company_imgs['odoo']['img'] + self.assertColors(doc_layout, self.company_imgs['odoo']['colors']) + + def test_company_colors_change_logo(self): + """changes of the logo implies displaying the new computed colors""" + self.company.write({ + 'primary_color': '#ff0080', + 'secondary_color': '#00ff00', + 'logo': False, + }) + + with Form(self.env['base.document.layout']) as doc_layout: + self.assertColors(doc_layout, self.company) + doc_layout.logo = self.company_imgs['odoo']['img'] + self.assertColors(doc_layout, self.company_imgs['odoo']['colors']) + + def test_company_colors_and_logo_change_logo(self): + """The colors of the company may differ from the one the logo computes + Opening the wizard in these condition displays the company's colors + When the logo changes, colors must change according to the logo""" + self.company.write({ + 'primary_color': '#ff0080', + 'secondary_color': '#00ff00', + 'logo': self.company_imgs['overwatch']['img'], + }) + + with Form(self.env['base.document.layout']) as doc_layout: + self.assertColors(doc_layout, self.company) + doc_layout.logo = self.company_imgs['odoo']['img'] + self.assertColors(doc_layout, self.company_imgs['odoo']['colors']) + + # Layout change tests + def test_company_colors_reset_colors(self): + """Reset the colors when they differ from the ones originally + computed from the company logo""" + self.company.write({ + 'primary_color': '#ff0080', + 'secondary_color': '#00ff00', + 'logo': self.company_imgs['overwatch']['img'], + }) + + with Form(self.env['base.document.layout']) as doc_layout: + self.assertColors(doc_layout, self.company) + doc_layout.custom_colors = False + self.assertColors(doc_layout, self.company_imgs['overwatch']['colors']) + + # /!\ This case is NOT supported, and probably not supportable + # res.partner resizes manu-militari the image it is given + # so res.company._get_logo differs from res.partner.[default image] + # def test_company_no_colors_default_logo_and_layout_change_layout(self): + # """When the default YourCompany logo is set, and no colors are set on company: + # change wizard's color according to template""" + # self.company.write({ + # 'primary_color': False, + # 'secondary_color': False, + # 'external_report_layout_id': self.layout_template1.id, + # }) + # default_colors = self.default_colors + # with Form(self.env['base.document.layout']) as doc_layout: + # self.assertColors(doc_layout, default_colors) + # doc_layout.report_layout_id = self.report_layout2 + # self.assertColors(doc_layout, self.report_layout2) diff --git a/odoo/addons/base/tests/tommy_small.jpeg b/odoo/addons/base/tests/tommy_small.jpeg new file mode 100644 index 00000000000..70cd4fb3eb4 Binary files /dev/null and b/odoo/addons/base/tests/tommy_small.jpeg differ diff --git a/odoo/addons/base/views/res_company_views.xml b/odoo/addons/base/views/res_company_views.xml index 29051f5a8a9..e549946ff04 100644 --- a/odoo/addons/base/views/res_company_views.xml +++ b/odoo/addons/base/views/res_company_views.xml @@ -115,86 +115,5 @@
- - - res.company.report.form - res.company - - primary - 1024 - - -
-
- Update your company details and upload your logo to get a beautiful document. -
-
-
- -

- -

-
- - -
-
- -
-
-
-
-
-
- - - res.company.report.form.with.print - res.company - primary - - 1024 - - - +
+ + + + + +
+
+ +
+
+ +
+ + + + ir.actions.act_window + Configure your document layout + form + new + base.document.layout + + + + + + + + diff --git a/odoo/tools/image.py b/odoo/tools/image.py index 7917cc7fd0e..ee7d109c10b 100644 --- a/odoo/tools/image.py +++ b/odoo/tools/image.py @@ -276,6 +276,65 @@ def image_process(base64_source, size=(0, 0), verify_resolution=False, quality=0 # Misc image tools # --------------------------------------- +def average_dominant_color(colors, mitigate=175, max_margin=140): + """This function is used to calculate the dominant colors when given a list of colors + + There are 5 steps : + 1) Select dominant colors (highest count), isolate its values and remove + it from the current color set. + 2) Set margins according to the prevalence of the dominant color. + 3) Evaluate the colors. Similar colors are grouped in the dominant set + while others are put in the "remaining" list. + 4) Calculate the average color for the dominant set. This is done by + averaging each band and joining them into a tuple. + 5) Mitigate final average and convert it to hex + + :param colors: list of tuples having: + [0] color count in the image + [1] actual color: tuple(R, G, B, A) + -> these can be extracted from a PIL image using image.getcolors() + :param mitigate: maximum value a band can reach + :param max_margin: maximum difference from one of the dominant values + :returns: a tuple with two items: + [0] the average color of the dominant set as: tuple(R, G, B) + [1] list of remaining colors, used to evaluate subsequent dominant colors + """ + dominant_color = max(colors) + dominant_rgb = dominant_color[1][:3] + dominant_set = [dominant_color] + remaining = [] + + margins = [max_margin * (1 - dominant_color[0] / + sum([col[0] for col in colors]))] * 3 + + colors.remove(dominant_color) + + for color in colors: + rgb = color[1] + if (rgb[0] < dominant_rgb[0] + margins[0] and rgb[0] > dominant_rgb[0] - margins[0] and + rgb[1] < dominant_rgb[1] + margins[1] and rgb[1] > dominant_rgb[1] - margins[1] and + rgb[2] < dominant_rgb[2] + margins[2] and rgb[2] > dominant_rgb[2] - margins[2]): + dominant_set.append(color) + else: + remaining.append(color) + + dominant_avg = [] + for band in range(3): + avg = total = 0 + for color in dominant_set: + avg += color[0] * color[1][band] + total += color[0] + dominant_avg.append(int(avg / total)) + + final_dominant = [] + brightest = max(dominant_avg) + for color in range(3): + value = dominant_avg[color] / (brightest / mitigate) if brightest > mitigate else dominant_avg[color] + final_dominant.append(int(value)) + + return tuple(final_dominant), remaining + + def base64_to_image(base64_source): """Return a PIL image from the given `base64_source`. @@ -407,6 +466,37 @@ def image_data_uri(base64_source): ) +def get_saturation(rgb): + """Returns the saturation (hsl format) of a given rgb color + + :param rgb: rgb tuple or list + :return: saturation + """ + c_max = max(rgb) / 255 + c_min = min(rgb) / 255 + d = c_max - c_min + return 0 if d == 0 else d / (1 - abs(c_max + c_min - 1)) + + +def get_lightness(rgb): + """Returns the lightness (hsl format) of a given rgb color + + :param rgb: rgb tuple or list + :return: lightness + """ + return (max(rgb) + min(rgb)) / 2 / 255 + + +def hex_to_rgb(hx): + """Converts an hexadecimal string (starting with '#') to a RGB tuple""" + return tuple([int(hx[i:i+2], 16) for i in range(1, 6, 2)]) + + +def rgb_to_hex(rgb): + """Converts a RGB tuple or list to an hexadecimal string""" + return '#' + ''.join([(hex(c).split('x')[-1].zfill(2)) for c in rgb]) + + if __name__=="__main__": import sys