diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 156169896a4..237a6647790 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -940,7 +940,7 @@ class AccountInvoice(models.Model):
total_currency = 0
for line in invoice_move_lines:
if self.currency_id != company_currency:
- currency = self.currency_id.with_context(date=self.date or self.date_invoice or fields.Date.context_today(self))
+ currency = self.currency_id.with_context(date=self._get_currency_rate_date() or fields.Date.context_today(self))
if not (line.get('currency_id') and line.get('amount_currency')):
line['currency_id'] = currency.id
line['amount_currency'] = currency.round(line['price'])
@@ -1090,7 +1090,7 @@ class AccountInvoice(models.Model):
if inv.payment_term_id:
totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=company_currency.id).compute(total, inv.date_invoice)[0]
res_amount_currency = total_currency
- ctx['date'] = inv.date or inv.date_invoice
+ ctx['date'] = inv._get_currency_rate_date()
for i, t in enumerate(totlines):
if inv.currency_id != company_currency:
amount_currency = company_currency.with_context(ctx).compute(t[1], inv.currency_id)
@@ -1289,6 +1289,9 @@ class AccountInvoice(models.Model):
copy_fields = ['company_id', 'user_id', 'fiscal_position_id']
return self._get_refund_common_fields() + self._get_refund_prepare_fields() + copy_fields
+ def _get_currency_rate_date(self):
+ return self.date or self.date_invoice
+
@api.model
def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None):
""" Prepare the dict of values to create the new credit note from the invoice.
@@ -1449,7 +1452,7 @@ class AccountInvoiceLine(models.Model):
@api.one
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
- 'invoice_id.date_invoice')
+ 'invoice_id.date_invoice', 'invoice_id.date')
def _compute_price(self):
currency = self.invoice_id and self.invoice_id.currency_id or None
price = self.price_unit * (1 - (self.discount or 0.0) / 100.0)
@@ -1459,7 +1462,7 @@ class AccountInvoiceLine(models.Model):
self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price
self.price_total = taxes['total_included'] if taxes else self.price_subtotal
if self.invoice_id.currency_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id:
- price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id.date_invoice).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id)
+ price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id._get_currency_rate_date()).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id)
sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1
self.price_subtotal_signed = price_subtotal_signed * sign
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 0257c94eb3f..6e83a298097 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -171,6 +171,8 @@ class AccountMove(models.Model):
if not move.journal_id.update_posted:
raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.'))
if self.ids:
+ self.check_access_rights('write')
+ self.check_access_rule('write')
self._check_lock_date()
self._cr.execute('UPDATE account_move '\
'SET state=%s '\
diff --git a/addons/account/views/report_overdue.xml b/addons/account/views/report_overdue.xml
index bb421333cb6..002ce58ef9b 100644
--- a/addons/account/views/report_overdue.xml
+++ b/addons/account/views/report_overdue.xml
@@ -16,7 +16,7 @@
-
+
diff --git a/addons/account_budget/views/account_budget_views.xml b/addons/account_budget/views/account_budget_views.xml
index c0651cb06f5..6c7d8859b63 100644
--- a/addons/account_budget/views/account_budget_views.xml
+++ b/addons/account_budget/views/account_budget_views.xml
@@ -97,7 +97,7 @@
-
+
@@ -111,7 +111,7 @@
-
+
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 9699ff753aa..9d34fc6a07c 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -578,7 +578,7 @@ class Import(models.TransientModel):
# Check that currency exists
currency = self.env['res.currency'].search([('symbol', '=', split_value[currency_index].strip())])
if len(currency):
- return split_value[currency_index + 1 % 2] if not negative else '-' + split_value[currency_index + 1 % 2]
+ return split_value[(currency_index + 1) % 2] if not negative else '-' + split_value[(currency_index + 1) % 2]
# Otherwise it is not a float with a currency symbol
return False
diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py
index ffcf5985aac..9cfde466c39 100644
--- a/addons/fleet/models/fleet_vehicle.py
+++ b/addons/fleet/models/fleet_vehicle.py
@@ -21,7 +21,7 @@ class FleetVehicle(models.Model):
company_id = fields.Many2one('res.company', 'Company')
license_plate = fields.Char(required=True, help='License plate number of the vehicle (i = plate number for a car)')
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
- driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle')
+ driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle', copy=False)
model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle')
log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
diff --git a/addons/hr_expense/models/account_move_line.py b/addons/hr_expense/models/account_move_line.py
index 4cdc5bed0b5..8af2ef4c881 100644
--- a/addons/hr_expense/models/account_move_line.py
+++ b/addons/hr_expense/models/account_move_line.py
@@ -15,6 +15,8 @@ class AccountMoveLine(models.Model):
res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id)
account_move_ids = [l.move_id.id for l in self if float_compare(l.move_id.matched_percentage, 1, precision_digits=5) == 0]
if account_move_ids:
- expense_sheets = self.env['hr.expense.sheet'].search([('account_move_id', 'in', account_move_ids)])
+ expense_sheets = self.env['hr.expense.sheet'].search([
+ ('account_move_id', 'in', account_move_ids), ('state', '!=', 'done')
+ ])
expense_sheets.set_to_paid()
return res
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 7a19e73b819..67a00f606b6 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -433,18 +433,20 @@ class HrExpenseSheet(models.Model):
@api.multi
def check_consistency(self):
- if any(sheet.employee_id != self[0].employee_id for sheet in self):
- raise UserError(_("Expenses must belong to the same Employee."))
-
- expense_lines = self.mapped('expense_line_ids')
- if expense_lines and any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
- raise UserError(_("Expenses must have been paid by the same entity (Company or employee)"))
+ for rec in self:
+ expense_lines = rec.expense_line_ids
+ if not expense_lines:
+ continue
+ if any(expense.employee_id != rec.employee_id for expense in expense_lines):
+ raise UserError(_("Expenses must belong to the same Employee."))
+ if any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines):
+ raise UserError(_("Expenses must have been paid by the same entity (Company or employee)"))
@api.model
def create(self, vals):
self._create_set_followers(vals)
sheet = super(HrExpenseSheet, self).create(vals)
- self.check_consistency()
+ sheet.check_consistency()
return sheet
@api.multi
@@ -586,7 +588,7 @@ class HrExpenseSheet(models.Model):
return res
@api.one
- @api.constrains('expense_line_ids')
+ @api.constrains('expense_line_ids', 'employee_id')
def _check_employee(self):
employee_ids = self.expense_line_ids.mapped('employee_id')
if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != self.employee_id):
diff --git a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
index 28ed0b1dc14..4d3ca141fee 100644
--- a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
+++ b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml
@@ -373,7 +373,7 @@ elif salary <= 2461.27:
Red.Spouse.Net148python
- result = (employee.marital == 'married' and employee.spouse_net_revenue <= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)
+ result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue <= 221.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)coderesult = min(110.50, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))
@@ -384,7 +384,7 @@ elif salary <= 2461.27:
Red.Spouse.Oth.Net149python
- result = (employee.marital == 'married' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue <= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)
+ result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue <= 440.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)coderesult = min(220.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))
diff --git a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
index e6d11322c6e..1b9835b1624 100644
--- a/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
+++ b/addons/l10n_be_hr_payroll/models/l10n_be_hr_payroll.py
@@ -215,23 +215,23 @@ class HrEmployee(models.Model):
spouse_fiscal_status = fields.Selection([
('without income', 'Without Income'),
('with income', 'With Income')
- ], string='Tax status for spouse')
- disabled = fields.Boolean(string="Disabled", help="If the employee is declared disabled by law")
- disabled_spouse_bool = fields.Boolean(string='Disabled Spouse', help='if recipient spouse is declared disabled by law')
- disabled_children_bool = fields.Boolean(string='Disabled Children', help='if recipient children is/are declared disabled by law')
- resident_bool = fields.Boolean(string='Nonresident', help='if recipient lives in a foreign country')
- disabled_children_number = fields.Integer('Number of disabled children')
- dependent_children = fields.Integer(compute='_compute_dependent_children', string='Considered number of dependent children')
- other_dependent_people = fields.Boolean(string="Other Dependent People", help="If other people are dependent on the employee")
- other_senior_dependent = fields.Integer('# seniors (>=65)', help="Number of seniors dependent on the employee, including the disabled ones")
- other_disabled_senior_dependent = fields.Integer('# disabled seniors (>=65)')
- other_juniors_dependent = fields.Integer('# people (<65)', help="Number of juniors dependent on the employee, including the disabled ones")
- other_disabled_juniors_dependent = fields.Integer('# disabled people (<65)')
- dependent_seniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent seniors")
- dependent_juniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent juniors")
- spouse_net_revenue = fields.Float(string="Spouse Net Revenue", help="Own professional income, other than pensions, annuities or similar income")
+ ], string='Tax status for spouse', groups="hr.group_hr_user")
+ disabled = fields.Boolean(string="Disabled", help="If the employee is declared disabled by law", groups="hr.group_hr_user")
+ disabled_spouse_bool = fields.Boolean(string='Disabled Spouse', help='if recipient spouse is declared disabled by law', groups="hr.group_hr_user")
+ disabled_children_bool = fields.Boolean(string='Disabled Children', help='if recipient children is/are declared disabled by law', groups="hr.group_hr_user")
+ resident_bool = fields.Boolean(string='Nonresident', help='if recipient lives in a foreign country', groups="hr.group_hr_user")
+ disabled_children_number = fields.Integer('Number of disabled children', groups="hr.group_hr_user")
+ dependent_children = fields.Integer(compute='_compute_dependent_children', string='Considered number of dependent children', groups="hr.group_hr_user")
+ other_dependent_people = fields.Boolean(string="Other Dependent People", help="If other people are dependent on the employee", groups="hr.group_hr_user")
+ other_senior_dependent = fields.Integer('# seniors (>=65)', help="Number of seniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user")
+ other_disabled_senior_dependent = fields.Integer('# disabled seniors (>=65)', groups="hr.group_hr_user")
+ other_juniors_dependent = fields.Integer('# people (<65)', help="Number of juniors dependent on the employee, including the disabled ones", groups="hr.group_hr_user")
+ other_disabled_juniors_dependent = fields.Integer('# disabled people (<65)', groups="hr.group_hr_user")
+ dependent_seniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent seniors", groups="hr.group_hr_user")
+ dependent_juniors = fields.Integer(compute='_compute_dependent_people', string="Considered number of dependent juniors", groups="hr.group_hr_user")
+ spouse_net_revenue = fields.Float(string="Spouse Net Revenue", help="Own professional income, other than pensions, annuities or similar income", groups="hr.group_hr_user")
spouse_other_net_revenue = fields.Float(string="Spouse Other Net Revenue",
- help='Own professional income which is exclusively composed of pensions, annuities or similar income')
+ help='Own professional income which is exclusively composed of pensions, annuities or similar income', groups="hr.group_hr_user")
@api.constrains('spouse_fiscal_status', 'spouse_net_revenue', 'spouse_other_net_revenue')
def _check_spouse_revenue(self):
diff --git a/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml b/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml
index 4e7d429f102..37fa58ae2b7 100644
--- a/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml
+++ b/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml
@@ -141,7 +141,7 @@
-
+
diff --git a/addons/l10n_be_hr_payroll_fleet/models/fleet.py b/addons/l10n_be_hr_payroll_fleet/models/fleet.py
index b0ba4206ec4..2580fae49c1 100644
--- a/addons/l10n_be_hr_payroll_fleet/models/fleet.py
+++ b/addons/l10n_be_hr_payroll_fleet/models/fleet.py
@@ -57,11 +57,12 @@ class FleetVehicle(models.Model):
for car in self:
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
- @api.depends('model_id', 'license_plate', 'log_contracts', 'total_depreciated_cost', 'acquisition_date')
+ @api.depends('model_id', 'license_plate', 'log_contracts', 'acquisition_date',
+ 'co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated')
def _compute_vehicle_name(self):
super(FleetVehicle, self)._compute_vehicle_name()
for vehicle in self:
- acquisition_date = self._get_acquisition_date()
+ acquisition_date = vehicle._get_acquisition_date()
vehicle.name += u" \u2022 " + str(round(vehicle.total_depreciated_cost, 2)) + u" \u2022 " + acquisition_date
def _get_acquisition_date(self):
@@ -138,14 +139,14 @@ class FleetVehicleModel(models.Model):
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated
@api.multi
- @api.depends('name', 'brand_id')
+ @api.depends('name', 'brand_id', 'default_total_depreciated_cost')
def name_get(self):
res = super(FleetVehicleModel, self).name_get()
new_res = []
for res_item in res:
model = self.browse(res_item[0])
if model.default_total_depreciated_cost != 0.0:
- new_res.append((res_item[0], res_item[1] + u" \u2022 " + str(model.default_total_depreciated_cost)))
+ new_res.append((res_item[0], res_item[1] + u" \u2022 " + str(round(model.default_total_depreciated_cost, 2))))
else:
new_res.append(res_item)
return new_res
diff --git a/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py b/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py
index 6e20c4570d0..7f802498934 100644
--- a/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py
+++ b/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py
@@ -11,8 +11,8 @@ class HrContract(models.Model):
domain=lambda self: self._get_available_cars_domain(),
default=lambda self: self.env['fleet.vehicle'].search([('driver_id', '=', self.employee_id.address_home_id.id)], limit=1),
help="Employee's company car.")
- car_atn = fields.Float(compute='_compute_car_atn_and_costs', string='ATN Company Car')
- company_car_total_depreciated_cost = fields.Float(compute='_compute_car_atn_and_costs')
+ car_atn = fields.Float(compute='_compute_car_atn_and_costs', string='ATN Company Car', store=True)
+ company_car_total_depreciated_cost = fields.Float(compute='_compute_car_atn_and_costs', store=True)
available_cars_amount = fields.Integer(compute='_compute_available_cars_amount', string='Number of available cars')
new_car = fields.Boolean('Request a new car')
new_car_model_id = fields.Many2one('fleet.vehicle.model', string="Model", domain=lambda self: self._get_possible_model_domain())
diff --git a/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml b/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml
index 4075178b573..c6a7ad33abd 100644
--- a/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml
+++ b/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/addons/l10n_ch/models/account_invoice.py b/addons/l10n_ch/models/account_invoice.py
index bcb9935e0a9..4daf5ebef10 100644
--- a/addons/l10n_ch/models/account_invoice.py
+++ b/addons/l10n_ch/models/account_invoice.py
@@ -59,10 +59,11 @@ class AccountInvoice(models.Model):
""" The ISR reference number is 27 characters long. The first 12 of them
contain the postal account number of this ISR's issuer, removing the zeros
at the beginning and filling the empty places with zeros on the right if it is
- too short. The 15 other characters contain an internal reference identifying
+ too short. The next 14 characters contain an internal reference identifying
the invoice. For this, we use the invoice sequence number, removing each
of its non-digit characters, and pad the unused spaces on the left of
- this number with zeros.
+ this number with zeros. The last character of the ISR number is the result
+ of a recursive modulo 10 on its first 26 characters.
"""
def _space_isr_number(isr_number):
to_treat = isr_number
@@ -81,9 +82,9 @@ class AccountInvoice(models.Model):
invoice_ref = re.sub('[^\d]', '', record.number)
#We only keep the last digits of the sequence number if it is too long
invoice_ref = invoice_ref[-l10n_ch_ISR_NUMBER_ISSUER_LENGTH:]
- internal_ref = invoice_ref.zfill(l10n_ch_ISR_NUMBER_LENGTH - l10n_ch_ISR_NUMBER_ISSUER_LENGTH)
+ internal_ref = invoice_ref.zfill(l10n_ch_ISR_NUMBER_LENGTH - l10n_ch_ISR_NUMBER_ISSUER_LENGTH - 1) # -1 for mod10r check character
- record.l10n_ch_isr_number = invoice_issuer_ref + internal_ref
+ record.l10n_ch_isr_number = mod10r(invoice_issuer_ref + internal_ref)
record.l10n_ch_isr_number_spaced = _space_isr_number(record.l10n_ch_isr_number)
@api.depends('currency_id.name', 'amount_total', 'partner_bank_id.bank_id', 'number', 'partner_bank_id.l10n_ch_postal', 'partner_bank_id.bank_id.l10n_ch_postal_eur', 'partner_bank_id.bank_id.l10n_ch_postal_chf')
diff --git a/addons/l10n_fr_pos_cert/models/pos.py b/addons/l10n_fr_pos_cert/models/pos.py
index f88533f7964..f3a35a5a878 100644
--- a/addons/l10n_fr_pos_cert/models/pos.py
+++ b/addons/l10n_fr_pos_cert/models/pos.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from datetime import datetime
+from datetime import datetime, timedelta
from hashlib import sha256
from json import dumps
import pytz
@@ -43,7 +43,7 @@ class pos_session(models.Model):
self.ensure_one()
date_today = datetime.utcnow()
session_start = Datetime.from_string(self.start_at)
- if not date_today - datetime.timedelta(hours=24) <= session_start:
+ if not date_today - timedelta(hours=24) <= session_start:
raise UserError(_("This session has been opened another day. To comply with the French law, you should close sessions on a daily basis. Please close session %s and open a new one.") % self.name)
return True
diff --git a/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml b/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml
index 17ec10ad01d..948039f1545 100644
--- a/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml
+++ b/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml
@@ -160,7 +160,7 @@ or (contract.wage * 0.10))Arrearscode
- result = inputs.ARS.amount
+ result = inputs.ARS and inputs.ARS.amountGenerally arrears are fully taxable, but employee may claim exemption u/s 89(1).
@@ -185,7 +185,7 @@ Now difference of income tax between payment year and actual year would be allow
nonecode
- result = inputs.LE.amount
+ result = inputs.LE and inputs.LE.amountPayment by way of leave encashment received by Central and State Govt.employees at the time of retirement in respect of the period of earned leave at credit is fully exempt. In case of other employees, the exemption is to be limited to minimum of all below:
1.The actual amount received
@@ -199,7 +199,7 @@ Now difference of income tax between payment year and actual year would be allow
Performance Incentivecode
- result = inputs.PERF.amount
+ result = inputs.PERF and inputs.PERF.amountThis would be fully taxable based on incentive.
@@ -209,7 +209,7 @@ Now difference of income tax between payment year and actual year would be allow
Bonuscode
- result = inputs.BNS.amount
+ result = inputs.BNS and inputs.BNS.amountThis would be fully taxable based on Bonus.
@@ -229,7 +229,7 @@ Now difference of income tax between payment year and actual year would be allow
Medical Reimbursementcode
- result = inputs.MR.amount
+ result = inputs.MR and inputs.MR.amountThis component is on-taxable up to 15000 per year (or Rs 1250 per month) on producing medical bills.
@@ -250,7 +250,7 @@ Now difference of income tax between payment year and actual year would be allow
Periodical Journalscode
- result = inputs.PJ.amount
+ result = inputs.PJ and inputs.PJ.amountSome employers may provide component for buying magazines, journals and books as a part of knowledge enhancement for business growth.This part would become non taxable on providing original bills.
@@ -280,7 +280,7 @@ Now difference of income tax between payment year and actual year would be allow
Telephone Reimbursementcode
- result = inputs.TR.amount
+ result = inputs.TR and inputs.TR.amountIn some of the cases, companies may provide a component for telephone bills.Employees may provide actual phone usage bills to reimburse this component and make it non-taxable.
@@ -300,7 +300,7 @@ Now difference of income tax between payment year and actual year would be allow
Car Expenses Reimbursementcode
- result = inputs.CEI.amount
+ result = inputs.CEI and inputs.CEI.amountIn case company provides component for this and employee use self owned car for official and personal purposes, Rs 1800 per month would be non-taxable on showing bills for fuel or can maintenance. This amount would be Rs 2400 in case car is more capacity than 1600cc.
@@ -310,7 +310,7 @@ Now difference of income tax between payment year and actual year would be allow
Internet Expensecode
- result = inputs.IE.amount
+ result = inputs.IE and inputs.IE.amountEmployer may also provide reimbursement of internet expenses and thus this would become non taxable.
@@ -463,7 +463,7 @@ or - (contract.wage * contract.voluntary_provident_fund / 100))nonecode
- result = - (inputs.LAI.amount)
+ result = inputs.LAI and - (inputs.LAI.amount)
diff --git a/addons/l10n_lu/data/account.account.tag.csv b/addons/l10n_lu/data/account.account.tag.csv
index 93c9c280973..f25178a068e 100644
--- a/addons/l10n_lu/data/account.account.tag.csv
+++ b/addons/l10n_lu/data/account.account.tag.csv
@@ -179,20 +179,17 @@ tag_lu_177,I.B.5. Tabacs fabriqués,taxes
tag_lu_178,Base - Vente Biens Tabacs - Extracommunautaire,taxes
tag_lu_179,Base - Vente Biens Tabacs - Intracommunautaire,taxes
tag_lu_180,Base - Vente Biens Tabacs - Pays,taxes
-tag_lu_181,I.B.6.a) Livraisons subséquentes à des acquisitions intracommunautaires dans
-le cadre d'opérations triangulaires, lorsque le destinataire identifié à la TVA dans l'Etat membre de destination des biens y est le redevable de la taxe (4),taxes
+tag_lu_181,"I.B.6.a) Livraisons subséquentes à des acquisitions intracommunautaires dans le cadre d'opérations triangulaires, lorsque le destinataire identifié à la TVA dans l'Etat membre de destination des biens y est le redevable de la taxe (4)",taxes
tag_lu_182,Base - Vente Biens – Triangulaire Intracommunautaire,taxes
tag_lu_183,I.B.6.d) Autres opérations réalisées (imposables) à l'étranger,taxes
tag_lu_184,Base - Vente Prestations - Extracommunautaire,taxes
tag_lu_185,I.B.6.c) Opérations réalisées dans le cadre du régime particulier de l'article 56sexies,taxes
tag_lu_186,I.B.7. Opérations à l'intérieur du pays pour lesquelles le preneur est le redevable,taxes
-tag_lu_187,I.B.6.b)1) Prestations de services à des identifiés à la TVA dans un autre
-Etat membre non exonérées dans l'Etat membre du preneur redevable (art.17/1/b) (5),taxes
+tag_lu_187,I.B.6.b)1) Prestations de services à des identifiés à la TVA dans un autre Etat membre non exonérées dans l'Etat membre du preneur redevable (art.17/1/b) (5),taxes
tag_lu_188,Base - Vente Prestations - Intracommunautaire,taxes
-tag_lu_189,I.B.6.b)2) Prestations de services à des identifiés à la TVA dans un autre
-Etat membre exonérées dans l'Etat membre du preneur (art.17/1/b),taxes
+tag_lu_189,I.B.6.b)2) Prestations de services à des identifiés à la TVA dans un autre Etat membre exonérées dans l'Etat membre du preneur (art.17/1/b),taxes
tag_lu_190,Base - Vente Prestations - Intracommunautaire exonérées dans l'état membre,taxes
-tag_lu_191,I.B.1. Livraisons intracommunautaires de biens à des personnes identifiées à la TVA dans unautre Etat membre (art.43/1/d e et f) (3),taxes
+tag_lu_191,I.B.1. Livraisons intracommunautaires de biens à des personnes identifiées à la TVA dans un autre Etat membre (art.43/1/d e et f) (3),taxes
tag_lu_192,Base - Vente Biens - Intracommunautaire,taxes
tag_lu_193,I.C. Chiffre d'affaires imposable,taxes
tag_lu_194,II.A. Ventilation du chiffre d'affaires imposable – base,taxes
@@ -598,4 +595,4 @@ tag_lu_594,Taxe - Investissement Prestations 17% - Intracommunautaire (-),taxes
tag_lu_595,Taxe - Investissement Prestations 3% - Intracommunautaire (-),taxes
tag_lu_596,Taxe - Investissement Prestations 6% - Intracommunautaire (-),taxes
tag_lu_597,Taxe - Investissement Prestations 8% - Intracommunautaire (-),taxes
-tag_lu_598,Base - Sans Taxe,taxes
\ No newline at end of file
+tag_lu_598,Base - Sans Taxe,taxes
diff --git a/addons/mail/models/ir_model.py b/addons/mail/models/ir_model.py
index 5ab47dc0fc0..3a312cfe843 100644
--- a/addons/mail/models/ir_model.py
+++ b/addons/mail/models/ir_model.py
@@ -15,6 +15,12 @@ class IrModel(models.Model):
help="Whether this model supports messages and notifications.",
)
+ def unlink(self):
+ # Delete followers for models that will be unlinked.
+ query = "DELETE FROM mail_followers WHERE res_model IN %s"
+ self.env.cr.execute(query, [tuple(self.mapped('model'))])
+ return super(IrModel, self).unlink()
+
@api.multi
def write(self, vals):
if self and 'is_mail_thread' in vals:
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 86168bf6049..f173caff565 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -846,6 +846,9 @@ class Message(models.Model):
('channel_ids', 'in', email_channels.ids),
('email', '!=', self_sudo.author_id.email or self_sudo.email_from),
])._notify(self, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature)
+
+ notif_users.mapped('partner_id')._notify_by_chat(self)
+
channels_sudo._notify(self)
# Discard cache, because child / parent allow reading and therefore
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 679f1726e75..263cfb84f08 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -2150,7 +2150,7 @@ class MailThread(models.AbstractModel):
new_channels.setdefault(header_follower.channel_id.id, set()).add(new_subtype.id)
# add followers coming from res.users relational fields that are tracked
- to_add_users = self.env['res.users'].sudo().browse([values[name] for name in user_field_lst if values.get(name)])
+ to_add_users = self.env['res.users'].sudo().browse([values[name] for name in user_field_lst if values.get(name)]).filtered(lambda u: u.partner_id.active)
for partner in to_add_users.mapped('partner_id'):
new_partners.setdefault(partner.id, None)
diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js
index 84dcfa3ef0a..eaf0f89d546 100644
--- a/addons/mail/static/src/js/many2many_tags_email.js
+++ b/addons/mail/static/src/js/many2many_tags_email.js
@@ -127,6 +127,21 @@ var FieldMany2ManyTagsEmail = M2MTags.extend({
return _super.apply(self, arguments);
});
},
+
+ /**
+ * Override for res.partner
+ * name_get is dynamic (based on context) while display_name is static
+ * (stored)
+ */
+ get_render_data: function(ids){
+ this.dataset.cancel_read();
+ return this.dataset.name_get(ids).then(function (names) {
+ return _.map(names, function(name) {
+ return {id: name[0], display_name: name[1]};
+ });
+ });
+ },
+
});
field_registry.add('many2many_tags_email', FieldMany2ManyTagsEmail);
diff --git a/addons/mrp/views/mrp_workcenter_views.xml b/addons/mrp/views/mrp_workcenter_views.xml
index 26fc7ce405c..886c3ba3d76 100644
--- a/addons/mrp/views/mrp_workcenter_views.xml
+++ b/addons/mrp/views/mrp_workcenter_views.xml
@@ -125,9 +125,11 @@
Work Orders Performanceir.actions.act_windowmrp.workorder
- [('state','=','done')]
+ []
+ {'search_default_done': True}formgraph,pivot,tree,form,gantt
+
diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml
index a3522f85999..9e9f3b7bd2d 100644
--- a/addons/mrp/views/mrp_workorder_views.xml
+++ b/addons/mrp/views/mrp_workorder_views.xml
@@ -200,7 +200,7 @@
-
+
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 90c6eafe09d..8452bccabda 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -206,7 +206,7 @@ exports.PosModel = Backbone.Model.extend({
},{
model: 'account.tax',
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'],
- domain: null,
+ domain: function(self) {return [['company_id', '=', self.company && self.company.id || false]]},
loaded: function(self, taxes){
self.taxes = taxes;
self.taxes_by_id = {};
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 088f62fbeeb..61e72258997 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -2030,6 +2030,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
}
order.initialize_validation_date();
+ order.finalized = true;
if (order.is_to_invoice()) {
var invoiced = this.pos.push_and_invoice_order(order);
diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py
index ce9aec95e43..3df96254141 100644
--- a/addons/product/models/product_pricelist.py
+++ b/addons/product/models/product_pricelist.py
@@ -108,7 +108,7 @@ class Pricelist(models.Model):
"""
self.ensure_one()
if not date:
- date = self._context.get('date') or fields.Date.today()
+ date = self._context.get('date') or fields.Date.context_today(self)
if not uom_id and self._context.get('uom'):
uom_id = self._context['uom']
if uom_id:
diff --git a/addons/purchase/controllers/portal.py b/addons/purchase/controllers/portal.py
index 4a169daba44..53a517845e6 100644
--- a/addons/purchase/controllers/portal.py
+++ b/addons/purchase/controllers/portal.py
@@ -15,7 +15,7 @@ class CustomerPortal(CustomerPortal):
def _prepare_portal_layout_values(self):
values = super(CustomerPortal, self)._prepare_portal_layout_values()
partner = request.env.user.partner_id
- values['purchase_count'] = request.env['purchase.order'].search_count([
+ values['purchase_count'] = request.env['purchase.order'].sudo().search_count([
'|',
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('partner_id', 'child_of', [partner.commercial_partner_id.id]),
diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py
index fe789333d03..adbcebd4f18 100644
--- a/addons/sale_mrp/models/sale_mrp.py
+++ b/addons/sale_mrp/models/sale_mrp.py
@@ -76,7 +76,7 @@ class AccountInvoiceLine(models.Model):
prod_moves = [m for m in moves if m.product_id.id == product_id]
prod_qty_done = factor * qty_done
prod_quantity = factor * quantity
- average_price_unit += self._compute_average_price(prod_qty_done, prod_quantity, prod_moves)
+ average_price_unit += factor * self._compute_average_price(prod_qty_done, prod_quantity, prod_moves)
price_unit = average_price_unit or price_unit
price_unit = self.product_id.uom_id._compute_price(price_unit, self.uom_id)
return price_unit
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 438270ecfe6..a8a32347882 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -608,7 +608,7 @@ class WebClient(http.Controller):
def version_info(self):
return odoo.service.common.exp_version()
- @http.route('/web/tests', type='http', auth="none")
+ @http.route('/web/tests', type='http', auth="user")
def test_suite(self, mod=None, **kwargs):
return request.render('web.qunit_suite')
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index c9eed9b2e64..e72db2c3a0c 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -941,7 +941,7 @@ var ActionManager = Widget.extend({
});
});
},
- ir_actions_act_url: function (action) {
+ ir_actions_act_url: function (action, options) {
var url = action.url;
if (session.debug && url && url.length && url[0] === '/') {
url = $.param.querystring(url, {debug: session.debug});
@@ -952,6 +952,7 @@ var ActionManager = Widget.extend({
return $.Deferred(); // The action is finished only when the redirection is done
} else {
window.open(url, '_blank');
+ options.on_close();
}
return $.when();
},
diff --git a/addons/web/static/src/less/search_view.less b/addons/web/static/src/less/search_view.less
index a9d6d28e9e4..0261806631a 100644
--- a/addons/web/static/src/less/search_view.less
+++ b/addons/web/static/src/less/search_view.less
@@ -139,6 +139,9 @@
// Filters
.o_filters_menu {
.o-selected-li;
+
+ max-height: ~"calc(100vh - 125px)";
+ overflow: auto;
.o_filter_condition {
.o-search-options-dropdown-custom-li;
@@ -175,6 +178,9 @@
// Group By
.o_group_by_menu {
.o-selected-li;
+
+ max-height: ~"calc(100vh - 125px)";
+ overflow: auto;
.divider {display: none;} // hide the divider by default (no group)
.o_add_group { // add button
@@ -188,6 +194,9 @@
max-height: 75vh;
overflow-y: auto;
.o-selected-li;
+
+ max-height: ~"calc(100vh - 125px)";
+ overflow: auto;
.divider {display: none;} // hide the divider by default (no favorite)
.o-searchview-custom-private, .o-searchview-custom-public {
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js
index 95afaa90073..cbdb6cbb88b 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/fields.js
@@ -221,7 +221,8 @@ var FieldTextHtml = AbstractField.extend({
start: function () {
var self = this;
- this.loaded = false;
+ this.editorLoadedDeferred = $.Deferred();
+ this.contentLoadedDeferred = $.Deferred();
this.callback = _.uniqueId('FieldTextHtml_');
window.odoo[this.callback+"_editor"] = function (EditorBar) {
setTimeout(function () {
@@ -312,8 +313,6 @@ var FieldTextHtml = AbstractField.extend({
old_initialize_content: function () {
this.$el.closest('.modal-body').css('max-height', 'none');
this.$iframe = this.$el.find('iframe');
- // deactivate any button to avoid saving a not ready iframe
- $('.o_cp_buttons, .o_statusbar_buttons').find('button').addClass('o_disabled').attr('disabled', true);
this.document = null;
this.$body = $();
this.$content = $();
@@ -328,9 +327,7 @@ var FieldTextHtml = AbstractField.extend({
this.$content = this.$body.find("#editable_area");
this.render();
this.add_button();
- this.loaded = true;
- // reactivate all the buttons when the field's content (the iframe) is loaded
- $('.o_cp_buttons, .o_statusbar_buttons').find('button').removeClass('o_disabled').attr('disabled', false);
+ this.contentLoadedDeferred.resolve();
setTimeout(self.resize, 0);
},
on_editor_loaded: function (EditorBar) {
@@ -339,6 +336,7 @@ var FieldTextHtml = AbstractField.extend({
if (this.value && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) {
this.render();
}
+ this.editorLoadedDeferred.resolve();
setTimeout(function () {
setTimeout(self.resize,0);
}, 0);
@@ -395,20 +393,25 @@ var FieldTextHtml = AbstractField.extend({
//--------------------------------------------------------------------------
/**
+ * Set the value when the widget is fully loaded (content + editor).
+ *
* @override
*/
commitChanges: function () {
- if (!this.loaded || this.mode === 'readonly') {
+ var self = this;
+ var result = this._super.bind(this, arguments);
+ if (this.mode === 'readonly') {
return;
}
- // switch to WYSIWYG mode if currently in code mode to get all changes
- if (config.debug && this.mode === 'edit' && this.editor.rte) {
- var layoutInfo = this.editor.rte.editable().data('layoutInfo');
- $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
- }
- this.editor.snippetsMenu && this.editor.snippetsMenu.cleanForSave();
- this._setValue(this.$content.html());
- return this._super.apply(this, arguments);
+ return $.when(this.contentLoadedDeferred, this.editorLoadedDeferred, result).then(function () {
+ // switch to WYSIWYG mode if currently in code mode to get all changes
+ if (config.debug && self.editor.rte) {
+ var layoutInfo = self.editor.rte.editable().data('layoutInfo');
+ $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
+ }
+ self.editor.snippetsMenu && self.editor.snippetsMenu.cleanForSave();
+ self._setValue(self.$content.html());
+ });
},
});
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index 33bf1f38046..d1ebb21938f 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -17,7 +17,60 @@ var eventHandler = $.summernote.eventHandler;
var renderer = $.summernote.renderer;
var options = $.summernote.options;
+// Browser-unify execCommand
+var oldJustify = {};
+_.each(['Left', 'Right', 'Full', 'Center'], function (align) {
+ oldJustify[align] = editor['justify' + align];
+ editor['justify' + align] = function ($editable, value) {
+ // Before calling the standard function, check all elements which have
+ // an 'align' attribute and mark them with their value
+ var $align = $editable.find('[align]');
+ _.each($align, function (el) {
+ var $el = $(el);
+ $el.data('__align', $el.attr('align'));
+ });
+
+ // Call the standard function
+ oldJustify[align].apply(this, arguments);
+
+ // Then:
+
+ // Remove the text-align of elements which lost the 'align' attribute
+ var $newAlign = $editable.find('[align]');
+ $align.not($newAlign).css('text-align', '');
+
+ // Transform the 'align' attribute into the 'text-align' css
+ // property for elements which received the 'align' attribute or whose
+ // 'align' attribute changed
+ _.each($newAlign, function (el) {
+ var $el = $(el);
+
+ var oldAlignValue = $align.data('__align');
+ var alignValue = $el.attr('align');
+ if (oldAlignValue === alignValue) {
+ // If the element already had an 'align' attribute and that it
+ // did not changed, do nothing (compatibility)
+ return;
+ }
+
+ $el.removeAttr('align');
+ $el.css('text-align', alignValue);
+
+ // Note the first step (removing the text-align of elemnts which
+ // lost the 'align' attribute) is kinda the same as this one, but
+ // this one handles the elements which have been edited with chrome
+ // or with this new system
+ $el.find('*').css('text-align', '');
+ });
+
+ // Unmark the elements
+ $align.removeData('__align');
+ };
+});
+
+
// Add methods to summernote
+
dom.hasContentAfter = function (node) {
var next;
if (dom.isEditable(node)) return;
@@ -2289,26 +2342,6 @@ eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
}
};
-// override summernote clipboard functionality
-eventHandler.modules.clipboard.attach = function (layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.on('paste', function (e) {
- var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData);
- // Change nothing if pasting html (copy from text editor / web / ...) or
- // if clipboardData is not available (IE / ...)
- if (clipboardData && clipboardData.types && clipboardData.types.length === 1 && clipboardData.types[0] === "text/plain") {
- e.preventDefault();
- $editable.data('NoteHistory').recordUndo($editable); // FIXME
- var pastedText = clipboardData.getData("text/plain");
- // Try removing linebreaks which are not really linebreaks (in a PDF,
- // when a sentence goes over the next line, copying it considers it
- // a linebreak for example).
- var formattedText = pastedText.replace(/([\w-])\r?\n([\w-])/g, "$1 $2").trim();
- document.execCommand("insertText", false, formattedText);
- }
- });
-};
-
var fn_attach = eventHandler.attach;
eventHandler.attach = function (oLayoutInfo, options) {
var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js
index 41ada113cba..3f46edde64c 100644
--- a/addons/web_editor/static/tests/web_editor_tests.js
+++ b/addons/web_editor/static/tests/web_editor_tests.js
@@ -4,6 +4,7 @@ odoo.define('web_editor.web_editor_tests', function (require) {
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var core = require('web.core');
+var web_editor = require('web_editor.editor');
var _t = core._t;
@@ -191,7 +192,7 @@ QUnit.test('field htmlsimple does not crash when commitChanges is called in mode
QUnit.test('html_frame does not crash when saving in readonly', function (assert) {
// The 'Save' action may be triggered even in readonly (e.g. when clicking
// on a button in the form view)
- assert.expect(0);
+ assert.expect(2);
var form = testUtils.createView({
View: FormView,
@@ -203,10 +204,13 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
'' +
'',
res_id: 1,
- mockRPC: function (route) {
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
if (_.str.startsWith(route, '/test')) {
// manually call the callback to simulate that the iframe has
- // been correctly loaded
+ // been loaded (note: just the content, not the editor)
window.odoo[$.deparam(route).callback + '_content'].call();
return $.when();
}
@@ -215,7 +219,109 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
});
form.saveRecord(); // before the fix done in this commit, it crashed here
+
+ assert.verifySteps(['read']);
+
form.destroy();
});
+QUnit.test('html_frame does not crash when saving in edit mode (editor not loaded)', function (assert) {
+ // The 'Save' action may be triggered when saving in edit mode very fast
+ // so that the editor may be not loaded, even though the content is!
+ assert.expect(2);
+
+ var form = testUtils.createView({
+ View: FormView,
+ model: 'mass.mailing',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ if (_.str.startsWith(route, '/test')) {
+ // manually call the callback to simulate that the iframe has
+ // been partially loaded (just the content, not the editor)
+ window.odoo[$.deparam(route).callback + '_content']();
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').val('trululu').trigger('input');
+ form.$buttons.find('.o_form_button_save').click(); // crash without editor fully loaded
+
+ assert.verifySteps(['read']);
+
+ form.destroy();
+});
+
+QUnit.test('html_frame saving in edit mode (editor and content fully loaded)', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var editorBar = new web_editor.Class();
+ var loadDeferred = $.Deferred();
+ var writeDeferred = $.Deferred();
+
+ var form = testUtils.createView({
+ View: FormView,
+ model: 'mass.mailing',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ if (args.method === 'write') {
+ writeDeferred.resolve();
+ }
+ }
+ if (_.str.startsWith(route, '/test')) {
+ // manually call the callback to simulate that the iframe has
+ // been fully loaded (content + editor)
+ var callback = $.deparam(route).callback;
+ return loadDeferred.then(function () {
+ var contentCallback = window.odoo[callback + '_content'];
+ var editorCallback = window.odoo[callback + '_editor'];
+ if (editorCallback && contentCallback) {
+ contentCallback();
+ editorCallback(editorBar);
+ }
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').val('trululu').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ loadDeferred.resolve(); // simulate late loading of html frame
+
+ assert.strictEqual(form.$('.o_field_char').val(), 'trululu',
+ "should have saved the char field text");
+
+ writeDeferred.then( function () { // html_frame is async with write
+ assert.verifySteps(['read', 'write']);
+ form.destroy();
+ done();
+ });
+
+});
+
});
diff --git a/addons/website_event_sale/views/event_templates.xml b/addons/website_event_sale/views/event_templates.xml
index e336df0e7f2..3af11508112 100644
--- a/addons/website_event_sale/views/event_templates.xml
+++ b/addons/website_event_sale/views/event_templates.xml
@@ -79,7 +79,7 @@
- event.event_ticket_ids
+ event.event_ticket_ids and not all([ticket.is_expired for ticket in event.event_ticket_ids])
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 29f3652a185..88bbb95648c 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -168,7 +168,7 @@ odoo.define('website_sale.website_sale', function (require) {
}
else {
$q.parents('li:first').addClass("hidden");
- $('a[href^="/shop/checkout"]').addClass("hidden");
+ $('a[href*="/shop/checkout"]').addClass("hidden");
}
$q.html(data.cart_quantity).hide().fadeIn(600);
diff --git a/doc/cla/corporate/teclib.md b/doc/cla/corporate/teclib.md
index ff9db6dd21c..275a2b41300 100644
--- a/doc/cla/corporate/teclib.md
+++ b/doc/cla/corporate/teclib.md
@@ -12,3 +12,4 @@ List of contributors:
Laurent Destailleur ldestailleur@teclib.com https://github.com/eldy
David Halgand dhalgand@teclib.com https://github.com/halgandd
+Guillaume Masson gmasson@teclib.com https://github.com/metaminux
diff --git a/odoo/addons/base/ir/ir_sequence.py b/odoo/addons/base/ir/ir_sequence.py
index 70d70b536ef..7fff19057e9 100644
--- a/odoo/addons/base/ir/ir_sequence.py
+++ b/odoo/addons/base/ir/ir_sequence.py
@@ -96,7 +96,7 @@ class IrSequence(models.Model):
def _set_number_next_actual(self):
for seq in self:
- seq.write({'number_next': seq.number_next_actual or 0})
+ seq.write({'number_next': seq.number_next_actual or 1})
@api.model
def _get_current_sequence(self):
@@ -327,7 +327,7 @@ class IrSequenceDateRange(models.Model):
def _set_number_next_actual(self):
for seq in self:
- seq.write({'number_next': seq.number_next_actual or 0})
+ seq.write({'number_next': seq.number_next_actual or 1})
@api.model
def default_get(self, fields):
diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py
index 7300e580f8d..16c308e7fb1 100644
--- a/odoo/addons/base/res/res_partner.py
+++ b/odoo/addons/base/res/res_partner.py
@@ -509,7 +509,7 @@ class Partner(models.Model):
result = True
# To write in SUPERUSER on field is_company and avoid access rights problems.
if 'is_company' in vals and self.user_has_groups('base.group_partner_manager') and not self.env.uid == SUPERUSER_ID:
- result = super(Partner, self).sudo().write({'is_company': vals.get('is_company')})
+ result = super(Partner, self.sudo()).write({'is_company': vals.get('is_company')})
del vals['is_company']
result = result and super(Partner, self).write(vals)
for partner in self:
diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py
index 5f136959014..53a3438b21a 100644
--- a/odoo/addons/base/res/res_users.py
+++ b/odoo/addons/base/res/res_users.py
@@ -282,6 +282,13 @@ class Users(models.Model):
if any(user.company_ids and user.company_id not in user.company_ids for user in self):
raise ValidationError(_('The chosen company is not in the allowed companies for this user'))
+ @api.multi
+ @api.constrains('action_id')
+ def _check_action_id(self):
+ action_open_website = self.env.ref('base.action_open_website', raise_if_not_found=False)
+ if action_open_website and any(user.action_id.id == action_open_website.id for user in self):
+ raise ValidationError(_('The "App Switcher" action cannot be selected as home action.'))
+
@api.multi
def read(self, fields=None, load='_classic_read'):
if fields and self == self.env.user: