diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 1ac52912c81..48f7756c7e5 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1257,24 +1257,7 @@ class AccountInvoice(models.Model): @api.model def line_get_convert(self, line, part): - return { - 'date_maturity': line.get('date_maturity', False), - 'partner_id': part, - 'name': line['name'], - 'debit': line['price'] > 0 and line['price'], - 'credit': line['price'] < 0 and -line['price'], - 'account_id': line['account_id'], - 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), - 'currency_id': line.get('currency_id', False), - 'quantity': line.get('quantity', 1.00), - 'product_id': line.get('product_id', False), - 'product_uom_id': line.get('uom_id', False), - 'analytic_account_id': line.get('account_analytic_id', False), - 'invoice_id': line.get('invoice_id', False), - 'tax_ids': line.get('tax_ids', False), - 'tax_line_id': line.get('tax_line_id', False), - 'analytic_tag_ids': line.get('analytic_tag_ids', False), - } + return self.env['product.product']._convert_prepared_anglosaxon_line(line, part) @api.multi def action_cancel(self): diff --git a/addons/account/models/product.py b/addons/account/models/product.py index dd93a742056..b2b0026eea8 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -71,3 +71,28 @@ class ProductTemplate(models.Model): if not fiscal_pos: fiscal_pos = self.env['account.fiscal.position'] return fiscal_pos.map_accounts(accounts) + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.model + def _convert_prepared_anglosaxon_line(self, line, partner): + return { + 'date_maturity': line.get('date_maturity', False), + 'partner_id': partner, + 'name': line['name'], + 'debit': line['price'] > 0 and line['price'], + 'credit': line['price'] < 0 and -line['price'], + 'account_id': line['account_id'], + 'analytic_line_ids': line.get('analytic_line_ids', []), + 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency', False)) or -abs(line.get('amount_currency', False)), + 'currency_id': line.get('currency_id', False), + 'quantity': line.get('quantity', 1.00), + 'product_id': line.get('product_id', False), + 'product_uom_id': line.get('uom_id', False), + 'analytic_account_id': line.get('account_analytic_id', False), + 'invoice_id': line.get('invoice_id', False), + 'tax_ids': line.get('tax_ids', False), + 'tax_line_id': line.get('tax_line_id', False), + 'analytic_tag_ids': line.get('analytic_tag_ids', False), + } diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index ba8dbc1e54f..fcc04f25851 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -552,6 +552,8 @@ + + @@ -586,10 +588,6 @@ Not Draft - - - - diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 43437216da8..c652267ad5d 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -37,6 +37,9 @@ class AccountVoucher(models.Model): help="Effective date for accounting entries", copy=False, default=fields.Date.context_today) journal_id = fields.Many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=_default_journal) + payment_journal_id = fields.Many2one('account.journal', string='Payment Method', readonly=True, store=False, + states={'draft': [('readonly', False)]}, domain="[('type', 'in', ['cash', 'bank'])]", + compute='_compute_payment_journal_id', inverse='_inverse_payment_journal_id') account_id = fields.Many2one('account.account', 'Account', required=True, readonly=True, states={'draft': [('readonly', False)]}, domain="[('deprecated', '=', False), ('internal_type','=', (pay_now == 'pay_now' and 'liquidity' or voucher_type == 'purchase' and 'payable' or 'receivable'))]") @@ -109,6 +112,29 @@ class AccountVoucher(models.Model): def _get_journal_currency(self): self.currency_id = self.journal_id.currency_id.id or self.company_id.currency_id.id + @api.depends('company_id', 'pay_now', 'account_id') + def _compute_payment_journal_id(self): + for voucher in self: + if voucher.pay_now != 'pay_now': + continue + domain = [ + ('type', 'in', ('bank', 'cash')), + ('company_id', '=', voucher.company_id.id), + ] + if voucher.account_id and voucher.account_id.internal_type == 'liquidity': + field = 'default_debit_account_id' if voucher.voucher_type == 'sale' else 'default_credit_account_id' + domain.append((field, '=', voucher.account_id.id)) + voucher.payment_journal_id = self.env['account.journal'].search(domain, limit=1) + + def _inverse_payment_journal_id(self): + for voucher in self: + if voucher.pay_now != 'pay_now': + continue + if voucher.voucher_type == 'sale': + voucher.account_id = voucher.payment_journal_id.default_debit_account_id + else: + voucher.account_id = voucher.payment_journal_id.default_credit_account_id + @api.multi @api.depends('tax_correction', 'line_ids.price_subtotal') def _compute_total(self): @@ -122,22 +148,13 @@ class AccountVoucher(models.Model): voucher.amount = total + voucher.tax_correction voucher.tax_amount = tax_amount - @api.one - @api.depends('account_pay_now_id', 'account_pay_later_id', 'pay_now') - def _get_account(self): - self.account_id = self.account_pay_now_id if self.pay_now == 'pay_now' else self.account_pay_later_id - @api.onchange('date') def onchange_date(self): self.account_date = self.date @api.onchange('partner_id', 'pay_now') def onchange_partner_id(self): - if self.pay_now == 'pay_now': - liq_journal = self.env['account.journal'].search([('type', 'in', ('bank', 'cash'))], limit=1) - self.account_id = liq_journal.default_debit_account_id \ - if self.voucher_type == 'sale' else liq_journal.default_credit_account_id - else: + if self.pay_now != 'pay_now': if self.partner_id: self.account_id = self.partner_id.property_account_receivable_id \ if self.voucher_type == 'sale' else self.partner_id.property_account_payable_id @@ -187,7 +204,7 @@ class AccountVoucher(models.Model): 'account_id': self.account_id.id, 'move_id': move_id, 'journal_id': self.journal_id.id, - 'partner_id': self.partner_id.id, + 'partner_id': self.partner_id.commercial_partner_id.id, 'currency_id': company_currency != current_currency and current_currency or False, 'amount_currency': (sign * abs(self.amount) # amount < 0 for refunds if company_currency != current_currency else 0.0), @@ -234,19 +251,29 @@ class AccountVoucher(models.Model): @api.multi def voucher_pay_now_payment_create(self): - payment_methods = self.journal_id.outbound_payment_method_ids + if self.voucher_type == 'sale': + payment_methods = self.journal_id.inbound_payment_method_ids + payment_type = 'inbound' + partner_type = 'customer' + sequence_code = 'account.payment.customer.invoice' + else: + payment_methods = self.journal_id.outbound_payment_method_ids + payment_type = 'outbound' + partner_type = 'supplier' + sequence_code = 'account.payment.supplier.invoice' + name = self.env['ir.sequence'].with_context(ir_sequence_date=self.date).next_by_code(sequence_code) return { - 'payment_type': 'outbound', + 'name': name, + 'payment_type': payment_type, 'payment_method_id': payment_methods and payment_methods[0].id or False, - 'partner_type': 'supplier', - 'partner_id': self.partner_id.id, + 'partner_type': partner_type, + 'partner_id': self.partner_id.commercial_partner_id.id, 'amount': self.amount, 'currency_id': self.currency_id.id, 'payment_date': self.date, - 'journal_id': self.journal_id.id, + 'journal_id': self.payment_journal_id.id, 'company_id': self.company_id.id, 'communication': self.name, - 'name': self.name, 'state': 'reconciled', } @@ -278,7 +305,7 @@ class AccountVoucher(models.Model): 'name': line.name or '/', 'account_id': line.account_id.id, 'move_id': move_id, - 'partner_id': self.partner_id.id, + 'partner_id': self.partner_id.commercial_partner_id.id, 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False, 'analytic_tag_ids': [(6, 0, line.analytic_tag_ids.ids)], 'quantity': 1, diff --git a/addons/account_voucher/views/account_voucher_views.xml b/addons/account_voucher/views/account_voucher_views.xml index 1e1e0099590..af89a6f84e1 100644 --- a/addons/account_voucher/views/account_voucher_views.xml +++ b/addons/account_voucher/views/account_voucher_views.xml @@ -232,13 +232,18 @@ - - + + + - + @@ -341,26 +346,28 @@

- + + - - - + + + + + - + - - diff --git a/addons/hr_expense/__manifest__.py b/addons/hr_expense/__manifest__.py index ac6f28bc91e..46af9f9b4fa 100644 --- a/addons/hr_expense/__manifest__.py +++ b/addons/hr_expense/__manifest__.py @@ -40,6 +40,7 @@ This module also uses analytic accounting and is compatible with the invoice on 'views/hr_department_views.xml', 'views/assets.xml', 'views/res_config_settings_views.xml', + 'views/account_journal_dashboard.xml', ], 'demo': ['data/hr_expense_demo.xml'], 'installable': True, diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index a30175abbca..3cbf20badf5 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -220,3 +220,10 @@ class Employee(models.Model): ('date_to', '>=', today_start) ]) return [('id', 'in', holidays.mapped('employee_id').ids)] + + def write(self, values): + res = super(Employee, self).write(values) + if 'parent_id' in values: + holidays = self.env['hr.holidays'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)]) + holidays.write({'manager_id': values['parent_id']}) + return res diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 30ebc057333..69d578ff281 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -62,7 +62,7 @@ class HolidaysRequest(models.Model): domain="[('valid', '=', True), ('employee_applicability', 'in', ['leave', 'both'])]") employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, default=_default_employee, track_visibility='onchange') - manager_id = fields.Many2one('hr.employee', related='employee_id.parent_id', string='Manager', readonly=True, store=True) + manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True) notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}) number_of_days_temp = fields.Float( 'Allocation', copy=False, readonly=True, @@ -97,6 +97,10 @@ class HolidaysRequest(models.Model): ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), ] + @api.onchange('employee_id') + def _onchange_employee_id(self): + self.manager_id = self.employee_id and self.employee_id.parent_id + @api.multi @api.depends('number_of_days_temp') def _compute_number_of_days(self): @@ -242,6 +246,8 @@ class HolidaysRequest(models.Model): values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id}) holiday = super(HolidaysRequest, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) holiday.add_follower(employee_id) + if 'employee_id' in values: + holiday._onchange_employee_id() return holiday @api.multi @@ -249,6 +255,8 @@ class HolidaysRequest(models.Model): employee_id = values.get('employee_id', False) result = super(HolidaysRequest, self).write(values) self.add_follower(employee_id) + if 'employee_id' in values: + self._onchange_employee_id() return result @api.multi diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index e9d23227202..d652325d757 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -1,9 +1,9 @@ odoo.define('im_livechat.im_livechat', function (require) { "use strict"; +var bus = require('bus.bus').bus; var config = require('web.config'); var core = require('web.core'); -var ServiceProviderMixin = require('web.ServiceProviderMixin'); var session = require('web.session'); var time = require('web.time'); var utils = require('web.utils'); @@ -33,11 +33,7 @@ if (!_.contains(url_history, page)) { utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(url_history), 60*60*24); // 1 day cookie } -/** - * In order to handle services like ajax in the external lib, - * The parent of Livechat, LivechatButton, is set as a service provider - */ -var LivechatButton = Widget.extend(ServiceProviderMixin, { +var LivechatButton = Widget.extend({ className:"openerp o_livechat_button hidden-print", events: { @@ -56,7 +52,7 @@ var LivechatButton = Widget.extend(ServiceProviderMixin, { this.chat_window = null; this.messages = []; this.server_url = server_url; - this.busBus = this.call('bus_service', 'getBus'); + this.busBus = bus; }, willStart: function () { diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index ec25a417dca..3c13ab09fd8 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -142,7 +142,6 @@ - diff --git a/addons/link_tracker/i18n/link_tracker.pot b/addons/link_tracker/i18n/link_tracker.pot index efc1285b23a..d14c2e2ac58 100644 --- a/addons/link_tracker/i18n/link_tracker.pot +++ b/addons/link_tracker/i18n/link_tracker.pot @@ -162,11 +162,6 @@ msgstr "" msgid "Title and URL" msgstr "" -#. module: link_tracker -#: model:ir.ui.view,arch_db:link_tracker.view_link_tracker_filter -msgid "Tracked Link" -msgstr "" - #. module: link_tracker #: model:ir.model.fields,field_description:link_tracker.field_link_tracker_short_url msgid "Tracked URL" diff --git a/addons/mail/models/res_config_settings.py b/addons/mail/models/res_config_settings.py index 881f7c20184..6cae4f3b0d5 100644 --- a/addons/mail/models/res_config_settings.py +++ b/addons/mail/models/res_config_settings.py @@ -21,21 +21,12 @@ class ResConfigSettings(models.TransientModel): def get_values(self): res = super(ResConfigSettings, self).get_values() - alias_domain = res.get('alias_domain') - if not alias_domain: - domain = self.env["ir.config_parameter"].get_param("web.base.url") - try: - alias_domain = urls.url_parse(domain).host - except Exception: - pass - previous_date = datetime.datetime.now() - datetime.timedelta(days=30) res.update( fail_counter=self.env['mail.mail'].sudo().search_count([ ('date', '>=', previous_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)), ('state', '=', 'exception')]), - alias_domain=alias_domain or False, ) return res diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js index 8ebec45da2b..c7070e61a31 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js +++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js @@ -89,11 +89,11 @@ options.registry.sizing_x = options.Class.extend({ }); options.registry.table_item = options.Class.extend({ - onClone: function () { + onClone: function (options) { this._super.apply(this, arguments); // If we cloned a td or th element... - if (this.$target.is("td, th")) { + if (options.isCurrent && this.$target.is("td, th")) { // ... and that the td or th element was alone on its row ... if (this.$target.siblings().length === 1) { var $tr = this.$target.parent(); diff --git a/addons/mass_mailing/static/src/less/themes/theme_default.less b/addons/mass_mailing/static/src/less/themes/theme_default.less index 898174c8b37..aba67dbeb64 100644 --- a/addons/mass_mailing/static/src/less/themes/theme_default.less +++ b/addons/mass_mailing/static/src/less/themes/theme_default.less @@ -39,6 +39,11 @@ background-color: @o_mm_def_color_alpha; } +td { + // Default browser style but needed so that alignment works on some mail + // clients (see transcoder) + text-align: inherit; +} // ===== Layout ===== .o_layout { diff --git a/addons/mass_mailing/views/link_tracker_views.xml b/addons/mass_mailing/views/link_tracker_views.xml index 724f08cc8a4..f7ea013cefd 100644 --- a/addons/mass_mailing/views/link_tracker_views.xml +++ b/addons/mass_mailing/views/link_tracker_views.xml @@ -19,7 +19,7 @@ link.tracker - + diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 9a3f093a87d..6a6f154594c 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -345,13 +345,14 @@ class MrpWorkorder(models.Model): # If last work order, then post lots used # TODO: should be same as checking if for every workorder something has been done? if not self.next_work_order_id: - production_move = self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))) - if production_move.has_tracking != 'none': - move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id) - if move_line: - move_line.product_uom_qty += self.qty_producing - else: - move_line.create({'move_id': production_move.id, + production_moves = self.production_id.move_finished_ids.filtered(lambda x: (x.state not in ('done', 'cancel'))) + for production_move in production_moves: + if production_move.product_id.id == self.production_id.product_id.id and production_move.has_tracking != 'none': + move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id) + if move_line: + move_line.product_uom_qty += self.qty_producing + else: + move_line.create({'move_id': production_move.id, 'product_id': production_move.product_id.id, 'lot_id': self.final_lot_id.id, 'product_uom_qty': self.qty_producing, @@ -360,9 +361,12 @@ class MrpWorkorder(models.Model): 'workorder_id': self.id, 'location_id': production_move.location_id.id, 'location_dest_id': production_move.location_dest_id.id, - }) - else: - production_move.quantity_done += self.qty_producing + }) + elif production_move.unit_factor: + rounding = production_move.product_uom.rounding + production_move.quantity_done += float_round(self.qty_producing * production_move.unit_factor, precision_rounding=rounding) + else: + production_move.quantity_done += self.qty_producing if not self.next_work_order_id: for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))): diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index 951799917a4..e14b7d59ca6 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -63,7 +63,7 @@ class PaypalController(http.Controller): Once data is validated, process it. """ res = False - new_post = dict(post, cmd='_notify-validate', charset='utf-8') + new_post = dict(post, cmd='_notify-validate', charset='UTF-8') reference = post.get('item_number') tx = None if reference: diff --git a/addons/point_of_sale/__init__.py b/addons/point_of_sale/__init__.py index ee87c280f2c..0c674f48dfc 100644 --- a/addons/point_of_sale/__init__.py +++ b/addons/point_of_sale/__init__.py @@ -5,3 +5,4 @@ from . import models from . import controllers from . import report from . import wizard +from . import tests diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 5ef49361c51..cad950c576e 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -245,6 +245,41 @@ class PosOrder(models.Model): have_to_group_by = session and session.config_id.group_by or False rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method + def add_anglosaxon_lines(grouped_data): + Product = self.env['product.product'] + Analytic = self.env['account.analytic.account'] + for product_key in list(grouped_data.keys()): + if product_key[0] == "product": + line = grouped_data[product_key][0] + product = Product.browse(line['product_id']) + # In the SO part, the entries will be inverted by function compute_invoice_totals + price_unit = - product._get_anglo_saxon_price_unit() + account_analytic = Analytic.browse(line.get('analytic_account_id')) + res = Product._anglo_saxon_sale_move_lines( + line['name'], product, product.uom_id, line['quantity'], price_unit, + fiscal_position=order.fiscal_position_id, + account_analytic=account_analytic) + if res: + line1, line2 = res + line1 = Product._convert_prepared_anglosaxon_line(line1, order.partner_id) + insert_data('counter_part', { + 'name': line1['name'], + 'account_id': line1['account_id'], + 'credit': line1['credit'] or 0.0, + 'debit': line1['debit'] or 0.0, + 'partner_id': line1['partner_id'] + + }) + + line2 = Product._convert_prepared_anglosaxon_line(line2, order.partner_id) + insert_data('counter_part', { + 'name': line2['name'], + 'account_id': line2['account_id'], + 'credit': line2['credit'] or 0.0, + 'debit': line2['debit'] or 0.0, + 'partner_id': line2['partner_id'] + }) + for order in self.filtered(lambda o: not o.account_move or o.state == 'paid'): current_company = order.sale_journal.company_id account_def = IrProperty.get( @@ -360,6 +395,9 @@ class PosOrder(models.Model): order.write({'state': 'done', 'account_move': move.id}) + if self and order.company_id.anglo_saxon_accounting: + add_anglosaxon_lines(grouped_data) + all_lines = [] for group_key, group_data in grouped_data.items(): for value in group_data: diff --git a/addons/point_of_sale/tests/__init__.py b/addons/point_of_sale/tests/__init__.py index 8a93c4c4477..24294101606 100644 --- a/addons/point_of_sale/tests/__init__.py +++ b/addons/point_of_sale/tests/__init__.py @@ -4,3 +4,4 @@ from . import test_point_of_sale_flow from . import test_frontend from . import test_point_of_sale_ui +from . import test_anglo_saxon diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py new file mode 100644 index 00000000000..b6d27af9505 --- /dev/null +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +import openerp +import time +from openerp import fields + +from openerp.tests import common + + +class TestAngloSaxonCommon(common.TransactionCase): + + def setUp(self): + super(TestAngloSaxonCommon, self).setUp() + self.PosMakePayment = self.env['pos.make.payment'] + self.PosOrder = self.env['pos.order'] + self.Statement = self.env['account.bank.statement'] + self.company = self.env.ref('base.main_company') + self.product = self.env.ref('product.product_product_3') + self.partner = self.env.ref('base.res_partner_1') + self.category = self.env.ref('product.product_category_1') + self.category = self.category.copy({'name': 'New category','property_valuation': 'real_time'}) + account_type_rcv = self.env['account.account.type'].create({'name': 'RCV type', 'type': 'receivable'}) + account_type_oth = self.env['account.account.type'].create({'name': 'RCV type', 'type': 'other'}) + self.account = self.env['account.account'].create({'name': 'Receivable', 'code': 'RCV00' , 'user_type_id': account_type_rcv.id, 'reconcile': True}) + account_expense = self.env['account.account'].create({'name': 'Expense', 'code': 'EXP00' , 'user_type_id': account_type_oth.id, 'reconcile': True}) + account_output = self.env['account.account'].create({'name': 'Output', 'code': 'OUT00' , 'user_type_id': account_type_oth.id, 'reconcile': True}) + self.partner.property_account_receivable_id = self.account + self.category.property_account_income_categ_id = self.account + self.category.property_account_expense_categ_id = account_expense + self.category.property_stock_account_input_categ_id = self.account + self.category.property_stock_account_output_categ_id = account_output + self.category.property_stock_valuation_account_id = self.account + self.category.property_stock_journal = self.env['account.journal'].create({'name': 'Stock journal', 'type': 'sale', 'code': 'STK00'}) + self.pos_config = self.env.ref('point_of_sale.pos_config_main') + self.pos_config = self.pos_config.copy({'name': 'New POS config'}) + self.product = self.product.copy({'name': 'New product','standard_price': 100}) + self.company.anglo_saxon_accounting = True + self.product.categ_id = self.category + sale_journal = self.env['account.journal'].create({'name': 'POS journal', 'type': 'sale', 'code': 'POS00'}) + self.pos_config.journal_id = sale_journal + self.cash_journal = self.env['account.journal'].create({'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'}) + + +class TestAngloSaxonFlow(TestAngloSaxonCommon): + + def test_create_account_move_line(self): + # This test will check that the correct journal entries are created when a product in real time valuation + # is sold in a company using anglo-saxon + self.pos_config.open_session_cb() + self.pos_config.current_session_id.write({'journal_ids': [(6, 0, [self.cash_journal.id])]}) + self.cash_journal.loss_account_id = self.account + self.pos_statement = self.Statement.create({ + 'balance_start': 0.0, + 'balance_end_real': 0.0, + 'date': time.strftime('%Y-%m-%d'), + 'journal_id': self.cash_journal.id, + 'company_id': self.company.id, + 'name': 'pos session test', + }) + self.pos_config.current_session_id.write({'statement_ids': [(6, 0, [self.pos_statement.id])]}) + + # I create a PoS order with 1 unit of New product at 450 EUR + self.pos_order_pos0 = self.PosOrder.create({ + 'company_id': self.company.id, + 'partner_id': self.partner.id, + 'pricelist_id': self.company.partner_id.property_product_pricelist.id, + 'session_id': self.pos_config.current_session_id.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 1.0 + })] + }) + + # I make a payment to fully pay the order + context_make_payment = {"active_ids": [self.pos_order_pos0.id], "active_id": self.pos_order_pos0.id} + self.pos_make_payment_0 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 450.0, + 'journal_id': self.cash_journal.id, + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos0.id} + self.pos_make_payment_0.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual(self.pos_order_pos0.state, 'paid', 'Order should be in paid state.') + + # I close the current session to generate the journal entries + self.pos_config.current_session_id.action_pos_session_close() + + # I test that the generated journal entries are correct. + account_output = self.category.property_stock_account_output_categ_id + expense_account = self.category.property_account_expense_categ_id + aml = self.pos_order_pos0.account_move.line_ids + aml_output = aml.filtered(lambda l: l.account_id.id == account_output.id) + aml_expense = aml.filtered(lambda l: l.account_id.id == expense_account.id) + self.assertEqual(aml_output.credit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching") + self.assertEqual(aml_expense.debit, self.product.standard_price, "Cost of Good Sold entry missing or mismatching") \ No newline at end of file diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index c00265e711e..4f263d17ebf 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -416,7 +416,7 @@ class ProductTemplate(models.Model): for tmpl_id in self.with_context(active_test=False): # adding an attribute with only one value should not recreate product # write this attribute on every product to make sure we don't lose them - variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: len(line.value_ids) == 1).mapped('value_ids') + variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant and len(line.value_ids) == 1).mapped('value_ids') for value_id in variant_alone: updated_products = tmpl_id.product_variant_ids.filtered(lambda product: value_id.attribute_id not in product.mapped('attribute_value_ids.attribute_id')) updated_products.write({'attribute_value_ids': [(4, value_id.id)]}) diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py index 813f5f9e28d..1a621d689cb 100644 --- a/addons/product/tests/test_variants.py +++ b/addons/product/tests/test_variants.py @@ -4,10 +4,10 @@ from . import common from odoo.tests.common import TransactionCase -class TestVariants(TransactionCase): +class TestVariantsSearch(TransactionCase): def setUp(self): - res = super(TestVariants, self).setUp() + res = super(TestVariantsSearch, self).setUp() self.size_attr = self.env['product.attribute'].create({'name': 'Size'}) self.size_attr_value_s = self.env['product.attribute.value'].create({'name': 'S', 'attribute_id': self.size_attr.id}) self.size_attr_value_m = self.env['product.attribute.value'].create({'name': 'M', 'attribute_id': self.size_attr.id}) @@ -152,3 +152,181 @@ class TestVariants(common.TestProductCommon): 'value_ids': [(4, self.size_attr_value_l.id)], })] }) + + +class TestVariantsNoCreate(common.TestProductCommon): + + def setUp(self): + super(TestVariantsNoCreate, self).setUp() + self.size = self.env['product.attribute'].create({ + 'name': 'Size', + 'create_variant': False, + 'value_ids': [(0, 0, {'name': 'S'}), (0, 0, {'name': 'M'}), (0, 0, {'name': 'L'})], + }) + self.size_S = self.size.value_ids[0] + self.size_M = self.size.value_ids[1] + self.size_L = self.size.value_ids[2] + + def test_create_mono(self): + """ create a product with a 'nocreate' attribute with a single value """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.attribute_value_ids) + + def test_update_mono(self): + """ modify a product with a 'nocreate' attribute with a single value """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.attribute_value_ids) + + def test_create_multi(self): + """ create a product with a 'nocreate' attribute with several values """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.attribute_value_ids) + + def test_update_multi(self): + """ modify a product with a 'nocreate' attribute with several values """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [(0, 0, { + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + })], + }) + self.assertEqual(len(template.product_variant_ids), 1) + self.assertFalse(template.product_variant_ids.attribute_value_ids) + + def test_create_mixed_mono(self): + """ create a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.attribute_value_ids for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_update_mixed_mono(self): + """ modify a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(4, self.size_S.id)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.attribute_value_ids for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_create_mixed_multi(self): + """ create a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.attribute_value_ids for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) + + def test_update_mixed_multi(self): + """ modify a product with regular and 'nocreate' attributes """ + template = self.env['product.template'].create({ + 'name': 'Sofa', + 'uom_id': self.uom_unit.id, + 'uom_po_id': self.uom_unit.id, + }) + self.assertEqual(len(template.product_variant_ids), 1) + + template.write({ + 'attribute_line_ids': [ + (0, 0, { # no variants for this one + 'attribute_id': self.size.id, + 'value_ids': [(6, 0, self.size.value_ids.ids)], + }), + (0, 0, { # two variants for this one + 'attribute_id': self.prod_att_1.id, + 'value_ids': [(4, self.prod_attr1_v1.id), (4, self.prod_attr1_v2.id)], + }), + ], + }) + self.assertEqual(len(template.product_variant_ids), 2) + self.assertEqual( + {variant.attribute_value_ids for variant in template.product_variant_ids}, + {self.prod_attr1_v1, self.prod_attr1_v2}, + ) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 8de21782852..4b824ded373 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -318,7 +318,11 @@ class Project(models.Model): @api.multi def write(self, vals): - res = super(Project, self).write(vals) + # directly compute is_favorite to dodge allow write access right + if 'is_favorite' in vals: + vals.pop('is_favorite') + self._fields['is_favorite'].determine_inverse(self) + res = super(Project, self).write(vals) if vals else True if 'active' in vals: # archiving/unarchiving a project does it on its tasks, too self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']}) diff --git a/addons/project/tests/test_access_rights.py b/addons/project/tests/test_access_rights.py index 21278b287d4..0016a71f71f 100644 --- a/addons/project/tests/test_access_rights.py +++ b/addons/project/tests/test_access_rights.py @@ -53,6 +53,15 @@ class TestPortalProject(TestPortalProjectBase): tmp_task.sudo(self.user_projectuser).unlink() @mute_logger('odoo.addons.base.models.ir_model') + def test_favorite_project_access_rights(self): + pigs = self.project_pigs.sudo(self.user_projectuser) + + # we can't write on project name + self.assertRaises(AccessError, pigs.write, {'name': 'False Pigs'}) + # we can write on is_favorite + pigs.write({'is_favorite': True}) + + @mute_logger('odoo.addons.base.ir.ir_model') def test_followers_project_access_rights(self): pigs = self.project_pigs pigs.write({'privacy_visibility': 'followers'}) diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 37b2880bb59..20e16200a9e 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -341,7 +341,7 @@ - +
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index 2182e4f5adb..c5c010374c5 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -311,7 +311,7 @@ class ResourceCalendar(models.Model): domain += [('date_to', '>', fields.Datetime.to_string(start_datetime + timedelta(days=-1)))] if end_datetime: # domain += [('date_from', '<', fields.Datetime.to_string(to_naive_utc(end_datetime, self.env.user)))] - domain += [('date_from', '<', fields.Datetime.to_string(start_datetime + timedelta(days=1)))] + domain += [('date_from', '<', fields.Datetime.to_string(end_datetime + timedelta(days=1)))] leaves = self.env['resource.calendar.leaves'].search(domain + [('calendar_id', '=', self.id)]) filtered_leaves = self.env['resource.calendar.leaves'] diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index db13282be1e..9547c0625e7 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -28,54 +28,15 @@ class AccountInvoice(models.Model): """ inv = i_line.invoice_id company_currency = inv.company_id.currency_id + price_unit = i_line._get_anglo_saxon_price_unit() + if inv.currency_id != company_currency: + currency_id = inv.currency_id.id + amount_currency = i_line._get_price(company_currency, price_unit) + else: + currency_id = False + amount_currency = False - if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time': - fpos = i_line.invoice_id.fiscal_position_id - accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) - # debit account dacc will be the output account - dacc = accounts['stock_output'].id - # credit account cacc will be the expense account - cacc = accounts['expense'].id - if dacc and cacc: - price_unit = i_line._get_anglo_saxon_price_unit() - if inv.currency_id != company_currency: - currency_id = inv.currency_id.id - amount_currency = i_line._get_price(company_currency, price_unit) - else: - currency_id = False - amount_currency = False - return [ - { - 'type': 'src', - 'name': i_line.name[:64], - 'price_unit': price_unit, - 'quantity': i_line.quantity, - 'price': price_unit * i_line.quantity, - 'currency_id': currency_id, - 'amount_currency': amount_currency, - 'account_id':dacc, - 'product_id':i_line.product_id.id, - 'uom_id':i_line.uom_id.id, - 'account_analytic_id': i_line.account_analytic_id.id, - 'analytic_tag_ids': i_line.analytic_tag_ids.ids and [(6, 0, i_line.analytic_tag_ids.ids)] or False, - }, - - { - 'type': 'src', - 'name': i_line.name[:64], - 'price_unit': price_unit, - 'quantity': i_line.quantity, - 'price': -1 * price_unit * i_line.quantity, - 'currency_id': currency_id, - 'amount_currency': -1 * amount_currency, - 'account_id':cacc, - 'product_id':i_line.product_id.id, - 'uom_id':i_line.uom_id.id, - 'account_analytic_id': i_line.account_analytic_id.id, - 'analytic_tag_ids': i_line.analytic_tag_ids.ids and [(6, 0, i_line.analytic_tag_ids.ids)] or False, - }, - ] - return [] + return self.env['product.product']._anglo_saxon_sale_move_lines(i_line.name, i_line.product_id, i_line.uom_id, i_line.quantity, price_unit, currency=currency_id, amount_currency=amount_currency, fiscal_position=inv.fiscal_position_id, account_analytic=i_line.account_analytic_id, analytic_tags=i_line.analytic_tag_ids) class AccountInvoiceLine(models.Model): @@ -83,11 +44,7 @@ class AccountInvoiceLine(models.Model): def _get_anglo_saxon_price_unit(self): self.ensure_one() - price = self.product_id.standard_price - if not self.uom_id or self.product_id.uom_id == self.uom_id: - return price - else: - return self.product_id.uom_id._compute_price(price, self.uom_id) + return self.product_id._get_anglo_saxon_price_unit(uom=self.uom_id) def _get_price(self, company_currency, price_unit): if self.invoice_id.currency_id.id != company_currency.id: diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index d60f889b1af..93bc4c980ae 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -197,6 +197,70 @@ class ProductProduct(models.Model): } return action + @api.model + def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False, analytic_tags=False): + """Prepare dicts describing new journal COGS journal items for a product sale. + + Returns a dict that should be passed to `_convert_prepared_anglosaxon_line()` to + obtain the creation value for the new journal items. + + :param Model product: a product.product record of the product being sold + :param Model uom: a product.uom record of the UoM of the sale line + :param Integer qty: quantity of the product being sold + :param Integer price_unit: unit price of the product being sold + :param Model currency: a res.currency record from the order of the product being sold + :param Interger amount_currency: unit price in the currency from the order of the product being sold + :param Model fiscal_position: a account.fiscal.position record from the order of the product being sold + :param Model account_analytic: a account.account.analytic record from the line of the product being sold + """ + + if product.type == 'product' and product.valuation == 'real_time': + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fiscal_position) + # debit account dacc will be the output account + dacc = accounts['stock_output'].id + # credit account cacc will be the expense account + cacc = accounts['expense'].id + if dacc and cacc: + return [ + { + 'type': 'src', + 'name': name[:64], + 'price_unit': price_unit, + 'quantity': qty, + 'price': price_unit * qty, + 'currency_id': currency and currency.id, + 'amount_currency': amount_currency, + 'account_id': dacc, + 'product_id': product.id, + 'uom_id': uom.id, + 'account_analytic_id': account_analytic and account_analytic.id, + 'analytic_tag_ids': analytic_tags and analytic_tag.ids and [(6, 0, analytic_tag.ids)] or False, + }, + + { + 'type': 'src', + 'name': name[:64], + 'price_unit': price_unit, + 'quantity': qty, + 'price': -1 * price_unit * qty, + 'currency_id': currency and currency.id, + 'amount_currency': -1 * amount_currency, + 'account_id': cacc, + 'product_id': product.id, + 'uom_id': uom.id, + 'account_analytic_id': account_analytic and account_analytic.id, + 'analytic_tag_ids': analytic_tags and analytic_tags.ids and [(6, 0, analytic_tags.ids)] or False, + }, + ] + return [] + + @api.model + def _get_anglo_saxon_price_unit(self, uom=False): + price = self.standard_price + if not uom or self.uom_id.id == uom.id: + return price + return self.uom_id._compute_price(self.uom_id.id, price, to_uom_id=uom.id) + class ProductCategory(models.Model): _inherit = 'product.category' diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index cea75c56726..237ce1f1c96 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -172,20 +172,12 @@ var ActionManager = Widget.extend({ } return this.dp.add($.when(def)).then(function () { - action.jsID = _.uniqueId('action_'); - action.pushState = options.pushState; - // action.target 'main' is equivalent to 'current' except that it // also clears the breadcrumbs options.clear_breadcrumbs = action.target === 'main' || options.clear_breadcrumbs; - // ensure that the context and domain are evaluated - var context = new Context(self.userContext, options.additional_context, action.context); - action.context = pyeval.eval('context', context); - if (action.domain) { - action.domain = pyeval.eval('domain', action.domain, action.context); - } + self._preprocessAction(action, options); return self._handleAction(action, options); }); @@ -757,6 +749,24 @@ var ActionManager = Widget.extend({ }); return def; }, + /** + * Preprocesses the action before it is handled by the ActionManager + * (assigns a JS id, evaluates its context and domains, etc.). + * + * @param {Object} action + * @param {Object} options see @doAction options + */ + _preprocessAction: function (action, options) { + action.jsID = _.uniqueId('action_'); + action.pushState = options.pushState; + + // ensure that the context and domain are evaluated + var context = new Context(this.userContext, options.additional_context, action.context); + action.context = pyeval.eval('context', context); + if (action.domain) { + action.domain = pyeval.eval('domain', action.domain, action.context); + } + }, /** * Unlinks the given action and its controller from the internal structures * and destroys its controllers. diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js index 6fd57c0330b..1a77b3d19e7 100644 --- a/addons/web_editor/static/src/js/editor/transcoder.js +++ b/addons/web_editor/static/src/js/editor/transcoder.js @@ -18,18 +18,12 @@ function getMatchedCSSRules(a) { var sheets = document.styleSheets; for (i = sheets.length-1 ; i >= 0 ; i--) { var rules; - if (sheets[i].hasOwnProperty('rules')) { - rules = sheets[i].rules; - } else { - // try...catch because Firefox not able to enumerate - // document.styleSheets[].cssRules[] for cross-domain sheets. - try { - rules = sheets[i].cssRules; - } catch (e) { - console.warn("Can't read the css rules of: " + sheets[i].href, e); - continue; - } - rules = sheets[i].cssRules; + // try...catch because browser may not able to enumerate rules for cross-domain sheets + try { + rules = sheets[i].rules || sheets[i].cssRules; + } catch (e) { + console.warn("Can't read the css rules of: " + sheets[i].href, e); + continue; } if (rules) { for (r = rules.length-1; r >= 0; r--) { @@ -143,6 +137,20 @@ function getMatchedCSSRules(a) { delete style['text-decoration-style']; } + // text-align inheritance does not seem to get past elements on some + // mail clients + if (style['text-align'] === 'inherit') { + var $el = $(a).parent(); + do { + var align = $el.css('text-align'); + if (_.indexOf(['left', 'right', 'center', 'justify'], align) >= 0) { + style['text-align'] = align; + break; + } + $el = $el.parent(); + } while (!$el.is('html')); + } + return style; } diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index 8f31c1505e1..d8d3f03f802 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -21,7 +21,7 @@ class Event(models.Model): website_menu = fields.Boolean('Dedicated Menu', help="Creates menus Introduction, Location and Register on the page " - " of the event on the website.") + " of the event on the website.", copy=False) menu_id = fields.Many2one('website.menu', 'Event Menu') def _compute_is_participating(self): diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml index 3a994c4e4ac..3a0caa340ba 100644 --- a/addons/website_livechat/views/website_livechat.xml +++ b/addons/website_livechat/views/website_livechat.xml @@ -9,7 +9,6 @@