summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-03-21 11:04:48 +0100
committerChristophe Simonis <chs@odoo.com>2018-03-21 11:04:48 +0100
commitb51f2d38c247dce9afc04c29774a77d773e3f393 (patch)
tree1133e0dd4c082543d1f361fb7ec160ac0b2cccc6
parentc7dff653d56910fd46e4dcf745c2c6088ce9fc58 (diff)
parent500e3f497057c5d00ae5cc34da7012c6f31f81d4 (diff)
[MERGE] forward port branch saas-11.2 up to 500e3f49705
-rw-r--r--addons/account/models/account.py19
-rw-r--r--addons/account/models/account_bank_statement.py18
-rw-r--r--addons/account/models/account_invoice.py3
-rw-r--r--addons/account/models/account_move.py60
-rw-r--r--addons/account/models/account_payment.py34
-rw-r--r--addons/account/models/chart_template.py3
-rw-r--r--addons/account/models/res_config_settings.py6
-rw-r--r--addons/account/report/account_aged_partner_balance.py6
-rw-r--r--addons/account/report/account_overdue_report.py2
-rw-r--r--addons/account/report/account_partner_ledger.py6
-rw-r--r--addons/account/tests/test_payment.py100
-rw-r--r--addons/account/tests/test_reconciliation.py96
-rw-r--r--addons/account/views/account_invoice_view.xml8
-rw-r--r--addons/account/views/res_config_settings_views.xml6
-rw-r--r--addons/account_voucher/models/account_voucher.py7
-rw-r--r--addons/auth_crypt/models/res_users.py3
-rw-r--r--addons/auth_oauth/models/res_users.py3
-rw-r--r--addons/base_address_city/models/res_partner.py9
-rw-r--r--addons/base_setup/models/res_config_settings.py2
-rw-r--r--addons/crm/report/crm_opportunity_report_views.xml6
-rw-r--r--addons/crm/views/crm_lead_views.xml16
-rw-r--r--addons/event_sale/wizard/event_edit_registration.xml2
-rw-r--r--addons/hr/models/hr.py3
-rw-r--r--addons/hr_org_chart/static/src/js/hr_org_chart.js1
-rw-r--r--addons/hr_payroll/models/hr_salary_rule.py4
-rw-r--r--addons/hr_timesheet/models/hr_timesheet.py7
-rw-r--r--addons/hw_scanner/controllers/main.py4
-rw-r--r--addons/im_livechat/models/mail_channel.py6
-rw-r--r--addons/im_livechat/views/im_livechat_channel_views.xml9
-rw-r--r--addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml12
-rw-r--r--addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml16
-rw-r--r--addons/l10n_be_hr_payroll_fleet/models/hr_contract.py2
-rw-r--r--addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml4
-rw-r--r--addons/l10n_ch/models/res_company.py20
-rw-r--r--addons/l10n_ch/models/res_config_settings.py8
-rw-r--r--addons/l10n_ch/report/isr_report.xml16
-rw-r--r--addons/l10n_ch/static/src/less/report_isr.less79
-rw-r--r--addons/l10n_ch/views/res_config_settings_views.xml31
-rw-r--r--addons/l10n_fr_fec/wizard/account_fr_fec.py4
-rw-r--r--addons/l10n_multilang/models/l10n_multilang.py4
-rw-r--r--addons/mail/static/src/js/services/chat_manager.js8
-rw-r--r--addons/mail/static/src/js/thread.js2
-rw-r--r--addons/mail/static/src/xml/thread.xml2
-rw-r--r--addons/maintenance/models/maintenance.py14
-rw-r--r--addons/membership/models/partner.py6
-rw-r--r--addons/mrp/models/product.py13
-rw-r--r--addons/payment/models/payment_acquirer.py16
-rw-r--r--addons/payment/static/src/js/payment_form.js1
-rw-r--r--addons/payment/tests/common.py1
-rw-r--r--addons/payment/views/payment_templates.xml2
-rw-r--r--addons/payment_adyen/models/payment.py4
-rw-r--r--addons/payment_authorize/models/payment.py14
-rw-r--r--addons/payment_authorize/tests/test_authorize.py1
-rw-r--r--addons/payment_authorize/views/payment_authorize_templates.xml1
-rw-r--r--addons/payment_ogone/models/payment.py17
-rw-r--r--addons/point_of_sale/static/src/js/models.js7
-rw-r--r--addons/portal/controllers/mail.py4
-rw-r--r--addons/portal/views/portal_templates.xml2
-rw-r--r--addons/pos_restaurant/static/src/xml/printbill.xml2
-rw-r--r--addons/product/models/product_template.py8
-rw-r--r--addons/product/views/product_views.xml8
-rw-r--r--addons/product_extended/models/product.py4
-rw-r--r--addons/product_extended/views/product_views.xml20
-rw-r--r--addons/project/static/src/js/tour.js2
-rw-r--r--addons/project_timesheet_holidays/tests/test_timesheet_holidays.py3
-rw-r--r--addons/purchase/data/mail_template_data.xml4
-rw-r--r--addons/purchase/models/stock.py11
-rw-r--r--addons/sale/__init__.py2
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale/views/sale_portal_templates.xml4
-rw-r--r--addons/sale_margin/__init__.py15
-rw-r--r--addons/sale_margin/__manifest__.py1
-rw-r--r--addons/sale_stock/models/stock.py13
-rw-r--r--addons/stock/data/stock_data.xml1
-rw-r--r--addons/stock/models/stock_move.py14
-rw-r--r--addons/stock/models/stock_picking.py42
-rw-r--r--addons/stock/models/stock_warehouse.py12
-rw-r--r--addons/stock/report/report_deliveryslip.xml19
-rw-r--r--addons/stock/report/report_stockpicking_operations.xml12
-rw-r--r--addons/stock/views/stock_move_views.xml1
-rw-r--r--addons/stock/wizard/stock_change_product_qty.py2
-rw-r--r--addons/stock_account/models/stock.py7
-rw-r--r--addons/stock_landed_costs/models/stock_landed_cost.py8
-rw-r--r--addons/survey/models/survey.py3
-rw-r--r--addons/web/static/lib/ace/ace.odoo-custom.js9
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js28
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js48
-rw-r--r--addons/web/static/src/js/chrome/control_panel.js3
-rw-r--r--addons/web/static/src/js/chrome/menu.js17
-rw-r--r--addons/web/static/src/js/chrome/web_client.js4
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js5
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js29
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js19
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js4
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js1
-rw-r--r--addons/web/static/src/less/kanban_view.less4
-rw-r--r--addons/web/static/src/less/kanban_view_mobile.less3
-rw-r--r--addons/web/static/src/xml/base.xml28
-rw-r--r--addons/web/static/tests/chrome/action_manager_tests.js268
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js76
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js411
-rw-r--r--addons/web/static/tests/views/form_tests.js40
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js122
-rw-r--r--addons/web_editor/controllers/main.py5
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js2
-rw-r--r--addons/web_editor/static/src/less/web_editor.backend.less3
-rw-r--r--addons/web_editor/static/src/less/web_editor.variables.less6
-rw-r--r--addons/website/static/src/js/backend/dashboard.js1
-rw-r--r--addons/website/static/src/js/content/snippets.animation.js2
-rw-r--r--addons/website/static/src/js/editor/snippets.options.js3
-rw-r--r--addons/website/static/src/js/menu/content.js36
-rw-r--r--addons/website_event/README.md2
-rw-r--r--addons/website_event_sale/views/event_templates.xml3
-rw-r--r--addons/website_sale/controllers/main.py13
-rw-r--r--addons/website_sale/data/demo.xml1
-rw-r--r--addons/website_sale/tests/test_website_sale_pricelist.py12
-rw-r--r--addons/website_sale/views/sale_report_views.xml2
-rw-r--r--addons/website_sale/views/templates.xml4
-rw-r--r--addons/website_sale_comparison/static/src/js/website_sale_comparison.js14
-rw-r--r--addons/website_sale_comparison/views/website_sale_comparison_template.xml2
-rw-r--r--addons/website_sale_options/static/src/js/website_sale.test.js33
-rw-r--r--addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js31
-rw-r--r--doc/_extensions/odoo_ext/layout.html14
-rw-r--r--doc/cla/corporate/dajmi5.md15
-rw-r--r--doc/cla/corporate/kazacube.md16
-rw-r--r--doc/cla/corporate/odoo_house.md15
-rw-r--r--doc/cla/individual/SimoRubi.md11
-rw-r--r--doc/cla/individual/agente2012.md12
-rw-r--r--doc/cla/individual/ernesto-telematel.md11
-rw-r--r--doc/cla/individual/kurkop.md1
-rw-r--r--doc/cla/individual/mginshe.md9
-rw-r--r--doc/cla/individual/mpanarin.md10
-rw-r--r--doc/howtos/backend.rst13
-rw-r--r--doc/reference/guidelines.rst187
-rw-r--r--doc/reference/mobile.rst2
-rw-r--r--doc/reference/orm.rst8
-rw-r--r--odoo/addons/base/data/res_country_data.xml1
-rw-r--r--odoo/addons/base/data/res_currency_data.xml4
-rw-r--r--odoo/addons/base/models/ir_ui_view.py49
-rw-r--r--odoo/addons/base/models/res_config.py5
-rw-r--r--odoo/addons/base/models/res_users.py33
-rw-r--r--odoo/addons/base/tests/test_views.py177
-rw-r--r--odoo/addons/test_main_flows/static/src/js/tour.js28
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py8
-rw-r--r--odoo/api.py6
-rw-r--r--odoo/fields.py6
-rw-r--r--odoo/http.py9
-rw-r--r--odoo/models.py18
-rw-r--r--odoo/modules/loading.py7
-rw-r--r--odoo/modules/registry.py8
-rw-r--r--odoo/service/security.py13
-rw-r--r--odoo/tests/common.py4
-rw-r--r--odoo/tools/translate.py2
154 files changed, 2438 insertions, 540 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 4f6685f7fc0..e292ed69818 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -521,11 +521,16 @@ class AccountJournal(models.Model):
@api.multi
def write(self, vals):
for journal in self:
+ company = journal.company_id
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its company.'))
- if self.bank_account_id:
- self.bank_account_id.company_id = vals['company_id']
+ company = self.env['res.company'].browse(vals['company_id'])
+ if self.bank_account_id.company_id and self.bank_account_id.company_id != company:
+ self.bank_account_id.write({
+ 'company_id': company.id,
+ 'partner_id': company.partner_id.id,
+ })
if ('code' in vals and journal.code != vals['code']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
@@ -541,8 +546,14 @@ class AccountJournal(models.Model):
self.default_credit_account_id.currency_id = vals['currency_id']
if self.bank_account_id:
self.bank_account_id.currency_id = vals['currency_id']
- if 'bank_account_id' in vals and not vals.get('bank_account_id'):
- raise UserError(_('You cannot remove the bank account from the journal once set.'))
+ if 'bank_account_id' in vals:
+ if not vals.get('bank_account_id'):
+ raise UserError(_('You cannot remove the bank account from the journal once set.'))
+ else:
+ bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id'])
+ if bank_account.partner_id != company.partner_id:
+ raise UserError(_("The partners of the journal's company and the related bank account mismatch."))
+
result = super(AccountJournal, self).write(vals)
# Create the bank_account_id if necessary
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index dcd4159bce7..9296124eec2 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -690,23 +690,8 @@ class AccountBankStatementLine(models.Model):
aml_dict['currency_id'] = statement_currency.id
# Create write-offs
- # When we register a payment on an invoice, the write-off line contains the amount
- # currency if all related invoices have the same currency. We apply the same logic in
- # the manual reconciliation.
- counterpart_aml = self.env['account.move.line']
- for aml_dict in counterpart_aml_dicts:
- counterpart_aml |= aml_dict.get('move_line', self.env['account.move.line'])
- new_aml_currency = False
- if counterpart_aml\
- and len(counterpart_aml.mapped('currency_id')) == 1\
- and counterpart_aml[0].currency_id\
- and counterpart_aml[0].currency_id != company_currency:
- new_aml_currency = counterpart_aml[0].currency_id
for aml_dict in new_aml_dicts:
aml_dict['payment_id'] = payment and payment.id or False
- if new_aml_currency and not aml_dict.get('currency_id'):
- aml_dict['currency_id'] = new_aml_currency.id
- aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], new_aml_currency)
aml_obj.with_context(check_move_validity=False, apply_taxes=True).create(aml_dict)
# Create counterpart move lines and reconcile them
@@ -717,9 +702,6 @@ class AccountBankStatementLine(models.Model):
aml_dict['payment_id'] = payment and payment.id or False
counterpart_move_line = aml_dict.pop('move_line')
- if counterpart_move_line.currency_id and counterpart_move_line.currency_id != company_currency and not aml_dict.get('currency_id'):
- aml_dict['currency_id'] = counterpart_move_line.currency_id.id
- aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], counterpart_move_line.currency_id)
new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict)
(new_aml | counterpart_move_line).reconcile()
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 47d7bcaf446..3f78e546e36 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -224,7 +224,7 @@ class AccountInvoice(models.Model):
@api.depends('move_id.line_ids.amount_residual')
def _compute_payments(self):
payment_lines = set()
- for line in self.move_id.line_ids:
+ for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id):
payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id'))
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
@@ -267,7 +267,6 @@ class AccountInvoice(models.Model):
], string='Status', index=True, readonly=True, default='draft',
track_visibility='onchange', copy=False,
help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
- " * The 'Pro-forma' status is used when the invoice does not have an invoice number.\n"
" * The 'Open' status is used when user creates invoice, an invoice number is generated. It stays in the open status till the user pays the invoice.\n"
" * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
" * The 'Cancelled' status is used when user cancel invoice.")
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index f5196eb02d6..7902ec8ceca 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -472,7 +472,7 @@ class AccountMoveLine(models.Model):
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict')
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
- company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True)
+ company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True, readonly=True)
counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.")
# TODO: put the invoice link and partner_id on the account_move
@@ -525,7 +525,7 @@ class AccountMoveLine(models.Model):
raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field."))
@api.multi
- @api.constrains('amount_currency')
+ @api.constrains('amount_currency', 'debit', 'credit')
def _check_currency_amount(self):
for line in self:
if line.amount_currency:
@@ -558,7 +558,9 @@ class AccountMoveLine(models.Model):
def _get_pair_to_reconcile(self):
#field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set)
- field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
+ company_currency_id = self[0].account_id.company_id.currency_id
+ account_curreny_id = self[0].account_id.currency_id
+ field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
#reconciliation on bank accounts are special cases as we don't want to set them as reconcilable
#but we still want to reconcile entries that are reversed together in order to clear those lines
#in the bank reconciliation report.
@@ -596,8 +598,9 @@ class AccountMoveLine(models.Model):
#there is no more pair to reconcile so return what move_line are left
if not sm_credit_move or not sm_debit_move:
return self
-
- field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
+ company_currency_id = self[0].account_id.company_id.currency_id
+ account_curreny_id = self[0].account_id.currency_id
+ field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
if not sm_debit_move.debit and not sm_debit_move.credit:
#both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry
field = 'amount_residual_currency'
@@ -619,9 +622,20 @@ class AccountMoveLine(models.Model):
#Check for the currency and amount_currency we can set
currency = False
amount_reconcile_currency = 0
- if sm_debit_move.currency_id == sm_credit_move.currency_id and sm_debit_move.currency_id.id:
- currency = sm_credit_move.currency_id.id
- amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency)
+ if sm_debit_move.currency_id == sm_credit_move.currency_id:
+ if sm_debit_move.currency_id.id:
+ currency = sm_credit_move.currency_id.id
+ amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency)
+ else:
+ if not sm_debit_move.currency_id or not sm_credit_move.currency_id:
+ # If only one of debit_move or credit_move has a secondary currency, also record the converted amount
+ # in that secondary currency in the partial reconciliation. That allows the exchange difference entry
+ # to be created, in case it is needed.
+ company_currency = sm_debit_move.company_id.currency_id
+ currency = sm_debit_move.currency_id or sm_credit_move.currency_id
+ currency_date = sm_debit_move.currency_id and sm_credit_move.date or sm_debit_move.date
+ amount_reconcile_currency = company_currency.with_context(date=currency_date).compute(amount_reconcile, currency)
+ currency = currency.id
amount_reconcile = min(sm_debit_move.amount_residual, -sm_credit_move.amount_residual)
@@ -1061,7 +1075,7 @@ class AccountMoveLine(models.Model):
return matched_percentage_per_move
@api.model
- def compute_amount_fields(self, amount, src_currency, company_currency, invoice_currency=False):
+ def _compute_amount_fields(self, amount, src_currency, company_currency):
""" Helper function to compute value for fields debit/credit/amount_currency based on an amount and the currencies given in parameter"""
amount_currency = False
currency_id = False
@@ -1071,9 +1085,6 @@ class AccountMoveLine(models.Model):
currency_id = src_currency.id
debit = amount > 0 and amount or 0.0
credit = amount < 0 and -amount or 0.0
- if invoice_currency and invoice_currency != company_currency and not amount_currency:
- amount_currency = src_currency.with_context(self._context).compute(amount, invoice_currency)
- currency_id = invoice_currency.id
return debit, credit, amount_currency, currency_id
def _get_analytic_tag_ids(self):
@@ -1436,7 +1447,8 @@ class AccountPartialReconcile(models.Model):
total_amount_currency = 0
#make sure that all partial reconciliations share the same secondary currency otherwise it's not
#possible to compute the exchange difference entry and it has to be done manually.
- currency = self[0].currency_id
+ currency = self[0].debit_move_id.currency_id or self[0].credit_move_id.currency_id or None
+ more_than_1_currency = False
maxdate = '0000-00-00'
seen = set()
@@ -1444,9 +1456,14 @@ class AccountPartialReconcile(models.Model):
while todo:
partial_rec = todo.pop()
seen.add(partial_rec)
+ if partial_rec.debit_move_id.currency_id != currency or partial_rec.credit_move_id.currency_id != currency:
+ #There's more than 1 secondary currency involved, which means that we can use the total_debit
+ # and total_credit comparison (otherwise we cannot, as we have to rely only on the amount in
+ # secondary currency to deal with the case stated in test_partial_reconcile_currencies_02)
+ more_than_1_currency = True
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
- currency = None
+ currency = False
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
if aml.amount_residual or aml.amount_residual_currency:
@@ -1459,9 +1476,9 @@ class AccountPartialReconcile(models.Model):
total_amount_currency += aml.amount_currency
elif partial_rec.currency_id and partial_rec.currency_id == currency:
#if the aml has no secondary currency but is reconciled with other journal item(s) in secondary currency, the amount
- #currency is recorded on the partial rec and in order to check if the reconciliation is total, we need to convert the
- #aml.balance in that foreign currency
- total_amount_currency += aml.company_id.currency_id.with_context(date=aml.date).compute(aml.balance, partial_rec.currency_id)
+ #in secondary currency is recorded on the partial rec. That allows us to consider it, in order to check if the
+ # reconciliation is total
+ total_amount_currency += aml.balance > 0 and partial_rec.amount_currency or - partial_rec.amount_currency
for x in aml.matched_debit_ids | aml.matched_credit_ids:
if x not in seen:
@@ -1469,15 +1486,16 @@ class AccountPartialReconcile(models.Model):
partial_rec_ids = [x.id for x in seen]
aml_ids = aml_set.ids
- #then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
+ #if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
- if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
+ if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or \
+ ((currency is None or more_than_1_currency) and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0):
exchange_move_id = False
- if currency and aml_to_balance:
+ if aml_to_balance:
exchange_move = self.env['account.move'].create(
self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id))
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
- rate_diff_amls, rate_diff_partial_rec = self.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
+ rate_diff_amls, rate_diff_partial_rec = self.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, aml_to_balance.currency_id, exchange_move)
aml_ids += rate_diff_amls.ids
partial_rec_ids += rate_diff_partial_rec.ids
exchange_move.post()
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index ca1e13511d4..09e50feb664 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -2,6 +2,7 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
+from odoo.tools import float_compare
from itertools import groupby
@@ -544,11 +545,7 @@ class account_payment(models.Model):
Return the journal entry.
"""
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
- invoice_currency = False
- if self.invoice_ids and all([x.currency_id == self.invoice_ids[0].currency_id for x in self.invoice_ids]):
- #if all the invoices selected share the same currency, record the payment in that currency too
- invoice_currency = self.invoice_ids[0].currency_id
- debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id, invoice_currency)
+ debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
move = self.env['account.move'].create(self._get_move_vals())
@@ -561,26 +558,7 @@ class account_payment(models.Model):
#Reconcile with the invoices
if self.payment_difference_handling == 'reconcile' and self.payment_difference:
writeoff_line = self._get_shared_move_line_vals(0, 0, 0, move.id, False)
- amount_currency_wo, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(self.payment_difference, self.currency_id, self.company_id.currency_id, invoice_currency)[2:]
- # the writeoff debit and credit must be computed from the invoice residual in company currency
- # minus the payment amount in company currency, and not from the payment difference in the payment currency
- # to avoid loss of precision during the currency rate computations. See revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 for a detailed example.
- total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids)
- total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id)
- if self.invoice_ids[0].type in ['in_invoice', 'out_refund']:
- amount_wo = total_payment_company_signed - total_residual_company_signed
- else:
- amount_wo = total_residual_company_signed - total_payment_company_signed
- # Align the sign of the secondary currency writeoff amount with the sign of the writeoff
- # amount in the company currency
- if amount_wo > 0:
- debit_wo = amount_wo
- credit_wo = 0.0
- amount_currency_wo = abs(amount_currency_wo)
- else:
- debit_wo = 0.0
- credit_wo = -amount_wo
- amount_currency_wo = -abs(amount_currency_wo)
+ debit_wo, credit_wo, amount_currency_wo, currency_id = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(self.payment_difference, self.currency_id, self.company_id.currency_id)
writeoff_line['name'] = self.writeoff_label
writeoff_line['account_id'] = self.writeoff_account_id.id
writeoff_line['debit'] = debit_wo
@@ -614,7 +592,7 @@ class account_payment(models.Model):
""" Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconcilable move line
"""
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
- debit, credit, amount_currency, dummy = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
+ debit, credit, amount_currency, dummy = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
amount_currency = self.destination_journal_id.currency_id and self.currency_id.with_context(date=self.payment_date).compute(amount, self.destination_journal_id.currency_id) or 0
dst_move = self.env['account.move'].create(self._get_move_vals(self.destination_journal_id))
@@ -669,6 +647,7 @@ class account_payment(models.Model):
'credit': credit,
'amount_currency': amount_currency or False,
'payment_id': self.id,
+ 'journal_id': self.journal_id.id,
}
def _get_counterpart_move_line_vals(self, invoice=False):
@@ -695,7 +674,6 @@ class account_payment(models.Model):
return {
'name': name,
'account_id': self.destination_account_id.id,
- 'journal_id': self.journal_id.id,
'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False,
}
@@ -713,7 +691,7 @@ class account_payment(models.Model):
# If the journal has a currency specified, the journal item need to be expressed in this currency
if self.journal_id.currency_id and self.currency_id != self.journal_id.currency_id:
amount = self.currency_id.with_context(date=self.payment_date).compute(amount, self.journal_id.currency_id)
- debit, credit, amount_currency, dummy = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id)
+ debit, credit, amount_currency, dummy = self.env['account.move.line'].with_context(date=self.payment_date)._compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id)
vals.update({
'amount_currency': amount_currency,
'currency_id': self.journal_id.currency_id.id,
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 93f17153fa8..d035fe594dc 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -738,6 +738,9 @@ class WizardMultiChartsAccounts(models.TransientModel):
res.setdefault('domain', {})
res['domain']['sale_tax_id'] = repr(sale_tax_domain)
res['domain']['purchase_tax_id'] = repr(purchase_tax_domain)
+ else:
+ self.sale_tax_id = False
+ self.purchase_tax_id = False
if self.chart_template_id.transfer_account_id:
self.transfer_account_id = self.chart_template_id.transfer_account_id.id
if self.chart_template_id.code_digits:
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index 68d8ef3e83b..d4593130d2c 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -36,12 +36,12 @@ class ResConfigSettings(models.TransientModel):
module_account_budget = fields.Boolean(string='Budget Management')
module_account_payment = fields.Boolean(string='Online Payment')
module_account_reports = fields.Boolean("Dynamic Reports")
- module_account_reports_followup = fields.Boolean("Enable payment followup management")
+ module_account_reports_followup = fields.Boolean("Follow-up Levels")
module_account_check_printing = fields.Boolean("Allow check printing and deposits")
module_account_batch_deposit = fields.Boolean(string='Use batch deposit',
help='This allows you to group received checks before you deposit them to the bank.\n'
'-This installs the module account_batch_deposit.')
- module_account_sepa = fields.Boolean(string='Use SEPA payments')
+ module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)')
module_account_sepa_direct_debit = fields.Boolean(string='Use SEPA Direct Debit')
module_account_plaid = fields.Boolean(string="Plaid Connector")
module_account_yodlee = fields.Boolean("Bank Interface - Sync your bank feeds automatically")
@@ -49,7 +49,7 @@ class ResConfigSettings(models.TransientModel):
module_account_bank_statement_import_ofx = fields.Boolean("Import in .ofx format")
module_account_bank_statement_import_csv = fields.Boolean("Import in .csv format")
module_account_bank_statement_import_camt = fields.Boolean("Import in CAMT.053 format")
- module_currency_rate_live = fields.Boolean(string="Allow Currency Rate Live")
+ module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates")
module_print_docsaway = fields.Boolean(string="Docsaway")
module_product_margin = fields.Boolean(string="Allow Product Margin")
module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 0140d489817..504a78e05e0 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -13,6 +13,10 @@ class ReportAgedPartnerBalance(models.AbstractModel):
_name = 'report.account.report_agedpartnerbalance'
def _get_partner_move_lines(self, account_type, date_from, target_move, period_length):
+ # This method can receive the context key 'include_nullified_amount' {Boolean}
+ # Do an invoice and a payment and unreconcile. The amount will be nullified
+ # By default, the partner wouldn't appear in this report.
+ # The context key allow it to appear
periods = {}
start = datetime.strptime(date_from, "%Y-%m-%d")
for i in range(5)[::-1]:
@@ -190,7 +194,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
values['name'] = _('Unknown Partner')
values['trust'] = False
- if at_least_one_amount:
+ if at_least_one_amount or self._context.get('include_nullified_amount'):
res.append(values)
return res, total, lines
diff --git a/addons/account/report/account_overdue_report.py b/addons/account/report/account_overdue_report.py
index 4d7e4b87ede..99463c482bb 100644
--- a/addons/account/report/account_overdue_report.py
+++ b/addons/account/report/account_overdue_report.py
@@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel):
"FROM account_move_line l "
"JOIN account_account_type at ON (l.user_type_id = at.id) "
"JOIN account_move m ON (l.move_id = m.id) "
- "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
+ "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
for row in self.env.cr.dictfetchall():
res[row.pop('partner_id')].append(row)
return res
diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py
index e28ab7f0169..68fe77f8f40 100644
--- a/addons/account/report/account_partner_ledger.py
+++ b/addons/account/report/account_partner_ledger.py
@@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel):
full_account = []
currency = self.env['res.currency']
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
- reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
+ reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
query = """
SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code
@@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel):
return
result = 0.0
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
- reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
+ reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
query = """SELECT sum(""" + field + """)
@@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel):
AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),))
data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()]
params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
- reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
+ reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
query = """
SELECT DISTINCT "account_move_line".partner_id
FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am
diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py
index 3d490eedc49..af8a66df8ef 100644
--- a/addons/account/tests/test_payment.py
+++ b/addons/account/tests/test_payment.py
@@ -98,9 +98,9 @@ class TestPayment(AccountingTestCase):
self.assertEqual(len(aml_rec), 1, "Expected a move line with values : %s" % str(aml_dict))
if aml_dict.get('currency_diff'):
if aml_rec.credit:
- currency_diff_move = aml_rec.matched_debit_ids.full_reconcile_id.exchange_move_id
+ currency_diff_move = aml_rec.matched_debit_ids[0].full_reconcile_id.exchange_move_id
else:
- currency_diff_move = aml_rec.matched_credit_ids.full_reconcile_id.exchange_move_id
+ currency_diff_move = aml_rec.matched_credit_ids[0].full_reconcile_id.exchange_move_id
for currency_diff_line in currency_diff_move.line_ids:
if aml_dict.get('currency_diff') > 0:
if currency_diff_line.account_id.id == aml_rec.account_id.id:
@@ -139,6 +139,7 @@ class TestPayment(AccountingTestCase):
{'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
{'account_id': inv_1.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 00, 'currency_id': False},
])
+ self.assertTrue(payment.move_line_ids.filtered(lambda l: l.account_id == inv_1.account_id)[0].full_reconcile_id)
liquidity_aml = payment.move_line_ids.filtered(lambda r: r.account_id == self.account_eur)
bank_statement = self.reconcile(liquidity_aml, 200, 0, False)
@@ -364,7 +365,7 @@ class TestPayment(AccountingTestCase):
{'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False},
])
- def test_payment_and_writeoff_in_other_currency(self):
+ def test_payment_and_writeoff_in_other_currency_1(self):
# Use case:
# Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD.
# Mark invoice as fully paid with a write_off
@@ -413,3 +414,96 @@ class TestPayment(AccountingTestCase):
{'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id},
])
+
+ def test_payment_and_writeoff_in_other_currency_2(self):
+ # Use case:
+ # Company is in EUR, create a supplier bill of 5325.6 USD and register payment of 5325 USD, at a different rate
+ # Mark invoice as fully paid with a write_off
+ # Check that all the aml are correctly created.
+
+ # Set exchange rates 0.895@2017-11-01 and 0.88@2017-12-01
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 0.895,
+ 'name': time.strftime('%Y') + '-06-26'})
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 0.88,
+ 'name': time.strftime('%Y') + '-07-15'})
+
+ invoice = self.create_invoice(amount=5325.6, type='in_invoice', currency_id=self.currency_usd_id, partner=self.partner_agrolait.id)
+ self.check_journal_items(invoice.move_id.line_ids, [
+ {'account_id': self.account_revenue.id, 'debit': 5950.39, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 5950.39, 'amount_currency': -5325.6, 'currency_id': self.currency_usd_id},
+ ])
+ # register payment on invoice
+ payment = self.payment_model.create({'payment_type': 'outbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'supplier',
+ 'partner_id': self.partner_agrolait.id,
+ 'amount': 5325,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.account_revenue.id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'invoice_ids': [(4, invoice.id, None)]
+ })
+ payment.post()
+ self.check_journal_items(payment.move_line_ids, [
+ {'account_id': self.account_eur.id, 'debit': 0, 'credit': 6051.14, 'amount_currency': -5325.0, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 0.68, 'amount_currency': -0.6, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 6051.82, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id, 'currency_diff': -101.43},
+ ])
+
+ #check the invoice status
+ self.assertEqual(invoice.state, 'paid')
+
+ def test_payment_and_writeoff_in_other_currency_3(self):
+ # Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
+ # Invoice made in secondary currency for which the rate to the company currency
+ # is higher than the foreign currency decimal precision.
+ # E.g: Company currency is EUR, create a customer invoice of 247590.40 EUR and
+ # register payment of 267 USD (1 USD = 948 EUR)
+ # Mark invoice as fully paid with a write_off
+ # Check that all the aml are correctly created and that the invoice is paid
+
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 1,
+ 'name': time.strftime('%Y') + '-06-26'})
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_eur_id,
+ 'rate': 948,
+ 'name': time.strftime('%Y') + '-06-26'})
+
+ invoice = self.create_invoice(amount=247590.4, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ self.check_journal_items(invoice.move_id.line_ids, [
+ {'account_id': self.account_receivable.id, 'debit': 247590.4, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
+ {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 247590.4, 'amount_currency': 0.0, 'currency_id': False},
+ ])
+ # register payment on invoice
+ payment = self.payment_model.create({'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait.id,
+ 'amount': 267,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-06-26',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.account_revenue.id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'invoice_ids': [(4, invoice.id, None)],
+ 'name': 'test_payment_and_writeoff_in_other_currency_3',
+ })
+ payment.post()
+ self.check_journal_items(payment.move_line_ids, [
+ {'account_id': self.account_eur.id, 'debit': 253116.0, 'credit': 0.0, 'amount_currency': 267.0, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 5526.84, 'amount_currency': -5.83, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 247589.16, 'amount_currency': -261.17, 'currency_id': self.currency_usd_id},
+ ])
+
+ # Check the invoice status and the full reconciliation: the difference on the receivable account
+ # should have been completed by an exchange rate difference entry
+ self.assertEqual(invoice.state, 'paid')
+ self.assertTrue(invoice.move_id.line_ids.filtered(lambda l: l.account_id == self.account_receivable)[0].full_reconcile_id)
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index 1180df172c9..4840e40f41a 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -103,6 +103,7 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(move_line.currency_id.id, aml_dict[move_line.account_id.id]['currency_id'])
if 'currency_diff' in aml_dict[move_line.account_id.id]:
currency_diff_move = move_line.full_reconcile_id.exchange_move_id
+ self.assertTrue(currency_diff_move, 'The full reconciliation should have created an exchange rate journal entry')
for currency_diff_line in currency_diff_move.line_ids:
if aml_dict[move_line.account_id.id].get('currency_diff') == 0:
if currency_diff_line.account_id.id == move_line.account_id.id:
@@ -196,11 +197,11 @@ class TestReconciliation(AccountingTestCase):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 0.0, False)
self.check_results(customer_move_lines, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
- self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -61.16, 'currency_id': self.currency_usd_id},
+ self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False},
})
self.check_results(supplier_move_lines, {
self.account_euro.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False},
- self.account_rcv.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 61.16, 'currency_id': self.currency_usd_id},
+ self.account_rcv.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
})
def test_statement_euro_invoice_usd_transaction_chf(self):
@@ -238,7 +239,7 @@ class TestReconciliation(AccountingTestCase):
'move_line': line_id,
'debit': 0.0,
'credit': 32.7,
- 'name': line_id.name,
+ 'name': 'test_statement_euro_invoice_usd_transaction_euro_full',
}], new_aml_dicts=[{
'debit': 0.0,
'credit': 7.3,
@@ -247,8 +248,8 @@ class TestReconciliation(AccountingTestCase):
}])
self.check_results(bank_stmt.move_line_ids, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
- self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': -41.97, 'currency_id': self.currency_usd_id, 'currency_diff': 0, 'amount_currency_diff': -8.03},
- self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': -9.37, 'currency_id': self.currency_usd_id},
+ self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': 0, 'currency_id': False, 'currency_diff': 0, 'amount_currency_diff': -8.03},
+ self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': 0, 'currency_id': False},
})
# The invoice should be paid, as the payments totally cover its total
@@ -480,7 +481,7 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
- def test_partial_reconcile_currencies(self):
+ def test_partial_reconcile_currencies_01(self):
# client Account (payable, rsa)
# Debit Credit
# --------------------------------------------------------
@@ -623,3 +624,86 @@ class TestReconciliation(AccountingTestCase):
# Checking if the direction of the move is correct
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
self.assertEqual(full_rec_payable.balance, 18.75)
+
+ def test_partial_reconcile_currencies_02(self):
+ ####
+ # Day 1: Invoice Cust/001 to customer (expressed in USD)
+ # Market value of USD (day 1): 1 USD = 0.5 EUR
+ # * Dr. 100 USD / 50 EUR - Accounts receivable
+ # * Cr. 100 USD / 50 EUR - Revenue
+ ####
+ account_revenue = self.env['account.account'].search(
+ [('user_type_id', '=', self.env.ref(
+ 'account.data_account_type_revenue').id)], limit=1)
+ dest_journal_id = self.env['account.journal'].search(
+ [('type', '=', 'purchase'),
+ ('company_id', '=', self.env.ref('base.main_company').id)],
+ limit=1)
+
+ # Delete any old rate - to make sure that we use the ones we need.
+ old_rates = self.env['res.currency.rate'].search(
+ [('currency_id', '=', self.currency_usd_id)])
+ old_rates.unlink()
+
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'name': time.strftime('%Y') + '-01-01',
+ 'rate': 2,
+ })
+
+ invoice_cust_1 = self.account_invoice_model.create({
+ 'partner_id': self.partner_agrolait_id,
+ 'account_id': self.account_rcv.id,
+ 'type': 'out_invoice',
+ 'currency_id': self.currency_usd_id,
+ 'date_invoice': time.strftime('%Y') + '-01-01',
+ })
+ self.account_invoice_line_model.create({
+ 'quantity': 1.0,
+ 'price_unit': 100.0,
+ 'invoice_id': invoice_cust_1.id,
+ 'name': 'product that cost 100',
+ 'account_id': account_revenue.id,
+ })
+ invoice_cust_1.action_invoice_open()
+ self.assertEqual(invoice_cust_1.residual_company_signed, 50.0)
+ aml = invoice_cust_1.move_id.mapped('line_ids').filtered(
+ lambda x: x.account_id == account_revenue)
+ self.assertEqual(aml.credit, 50.0)
+ #####
+ # Day 2: Receive payment for half invoice Cust/1 (in USD)
+ # -------------------------------------------------------
+ # Market value of USD (day 2): 1 USD = 1 EUR
+
+ # Payment transaction:
+ # * Dr. 50 USD / 50 EUR - EUR Bank (valued at market price
+ # at the time of receiving the money)
+ # * Cr. 50 USD / 50 EUR - Accounts Receivable
+ #####
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'name': time.strftime('%Y') + '-01-02',
+ 'rate': 1,
+ })
+ # register payment on invoice
+ payment = self.env['account.payment'].create(
+ {'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref(
+ 'account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 50,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-01-02',
+ 'journal_id': dest_journal_id.id,
+ })
+ payment.post()
+ payment_move_line = False
+ for l in payment.move_line_ids:
+ if l.account_id == invoice_cust_1.account_id:
+ payment_move_line = l
+ invoice_cust_1.register_payment(payment_move_line)
+ # We expect at this point that the invoice should still be open,
+ # because they owe us still 50 CC.
+ self.assertEqual(invoice_cust_1.state, 'open',
+ 'Invoice is in status %s' % invoice_cust_1.state)
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 30b9382e607..c3af025c5e6 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -256,7 +256,7 @@
<group>
<field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/>
<field name="date_invoice" string="Bill Date"/>
- <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
+ <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
<field name="move_name" invisible="1"/>
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
<field name="company_currency_id" invisible="1"/>
@@ -303,7 +303,7 @@
<field name="amount"/>
<field name="amount_rounding" invisible="1"/>
<field name="amount_total" invisible="1"/>
- <field name="currency_id" invisible="1"/>
+ <field name="currency_id" invisible="1" force_save="1"/>
</tree>
</field>
</div>
@@ -401,7 +401,7 @@
</group>
<group>
<field name="date_invoice"/>
- <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
+ <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/>
<field name="move_name" invisible="1"/>
<field name="user_id" groups="base.group_user"/>
<label for="currency_id" groups="base.group_multi_currency"/>
@@ -504,7 +504,7 @@
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="amount_rounding" invisible="1"/>
<field name="amount_total"/>
- <field name="currency_id" invisible="1"/>
+ <field name="currency_id" invisible="1" force_save="1"/>
</tree>
</field>
</page>
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index 179699fa726..e02640721fe 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -176,7 +176,7 @@
<field name="module_currency_rate_live" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
- <label string="Automatic Currency Rates"/>
+ <label for="module_currency_rate_live"/>
<div class="text-muted" id="update_currency_live">
Update exchange rates automatically
</div>
@@ -190,7 +190,7 @@
<field name="module_account_reports_followup" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
- <label string="Follow-up Levels"/>
+ <label for="module_account_reports_followup"/>
<div class="text-muted">
Use follow-up levels and schedule actions
</div>
@@ -256,7 +256,7 @@
<field name="module_account_sepa" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane" name="sepa_right_pane">
- <label string="SEPA Credit Transfer (SCT)" for="module_account_sepa"/>
+ <label for="module_account_sepa"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Pay your bills in one-click using Euro SEPA service
diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py
index 36636e05731..73cbff6aa52 100644
--- a/addons/account_voucher/models/account_voucher.py
+++ b/addons/account_voucher/models/account_voucher.py
@@ -139,9 +139,16 @@ class AccountVoucher(models.Model):
@api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self):
+ pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
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
+ else:
+ if self.voucher_type == 'purchase':
+ pay_journal_domain.append(('outbound_payment_method_ids', '!=', False))
+ else:
+ pay_journal_domain.append(('inbound_payment_method_ids', '!=', False))
+ return {'domain': {'payment_journal_id': pay_journal_domain}}
@api.multi
def proforma_voucher(self):
diff --git a/addons/auth_crypt/models/res_users.py b/addons/auth_crypt/models/res_users.py
index ad66d0c7025..ff7e983bc64 100644
--- a/addons/auth_crypt/models/res_users.py
+++ b/addons/auth_crypt/models/res_users.py
@@ -96,3 +96,6 @@ class ResUsers(models.Model):
internally
"""
return default_crypt_context
+
+ def _get_session_token_fields(self):
+ return super(ResUsers, self)._get_session_token_fields() | {'password_crypt'}
diff --git a/addons/auth_oauth/models/res_users.py b/addons/auth_oauth/models/res_users.py
index fa052e41a1b..3657885c3ac 100644
--- a/addons/auth_oauth/models/res_users.py
+++ b/addons/auth_oauth/models/res_users.py
@@ -117,3 +117,6 @@ class ResUsers(models.Model):
res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)])
if not res:
raise
+
+ def _get_session_token_fields(self):
+ return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'}
diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py
index dff5de888c7..c35430d1f92 100644
--- a/addons/base_address_city/models/res_partner.py
+++ b/addons/base_address_city/models/res_partner.py
@@ -4,6 +4,8 @@
from lxml import etree
from odoo import api, models, fields
+from odoo.tools.translate import _
+
class Partner(models.Model):
_inherit = 'res.partner'
@@ -24,14 +26,15 @@ class Partner(models.Model):
doc = etree.fromstring(arch)
if doc.xpath("//field[@name='city_id']"):
return arch
+ label = _('City')
for city_node in doc.xpath("//field[@name='city']"):
replacement_xml = """
<div>
<field name="country_enforce_cities" invisible="1"/>
- <field name='city' attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
- <field name='city_id' attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]" string="City"/>
+ <field name='city' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/>
+ <field name='city_id' placeholder="%s" string="%s" attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/>
</div>
- """
+ """ % (label, label, label)
city_id_node = etree.fromstring(replacement_xml)
city_node.getparent().replace(city_node, city_id_node)
diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py
index 063ebf6a829..a781a314686 100644
--- a/addons/base_setup/models/res_config_settings.py
+++ b/addons/base_setup/models/res_config_settings.py
@@ -85,6 +85,8 @@ class ResConfigSettings(models.TransientModel):
@api.multi
def edit_external_header(self):
+ if not self.external_report_layout:
+ return False
return self._prepare_report_view_action('web.external_layout_' + self.external_report_layout)
@api.multi
diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml
index 24625320191..e798d5fd1eb 100644
--- a/addons/crm/report/crm_opportunity_report_views.xml
+++ b/addons/crm/report/crm_opportunity_report_views.xml
@@ -55,14 +55,14 @@
<field name="model_id">crm.opportunity.report</field>
<field name="domain">[('probability', '=', 100)]</field>
<field name="user_id" eval="False"/>
- <field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count']}</field>
+ <field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count__']}</field>
</record>
<record id="filter_opportunity_opportunities_won_per_team" model="ir.filters">
<field name="name">Won by Channel</field>
<field name="model_id">crm.opportunity.report</field>
<field name="domain">[('probability', '=', 100)]</field>
<field name="user_id" eval="False"/>
- <field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected revenue']}</field>
+ <field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected_revenue']}</field>
</record>
<record id="filter_opportunity_country" model="ir.filters">
<field name="name">Won by Country</field>
@@ -75,7 +75,7 @@
<field name="name">Expected Revenue by Channel</field>
<field name="model_id">crm.opportunity.report</field>
<field name="user_id" eval="False"/>
- <field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count']}</field>
+ <field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count__']}</field>
</record>
<!-- Opportunities by user and team Search View -->
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index 9a64a61fe68..ddaa57c01f6 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -251,8 +251,6 @@
<group>
<field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/>
<field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
- <field name="email_from" invisible="1"/>
- <field name="phone" invisible="1"/>
<field name="partner_name" invisible="1"/>
<field name="street" invisible="1"/>
<field name="street2" invisible="1"/>
@@ -274,6 +272,20 @@
<button string="Create &amp; Edit" name="edit_dialog" type="object" class="btn-primary"/>
<button string="Discard" class="btn-default" special="cancel"/>
</footer>
+ <field name="partner_name" invisible="1"/>
+ <field name="contact_name" invisible="1"/>
+ <field name="title" invisible="1"/>
+ <field name="street" invisible="1"/>
+ <field name="street2" invisible="1"/>
+ <field name="city" invisible="1"/>
+ <field name="state_id" invisible="1"/>
+ <field name="country_id" invisible="1"/>
+ <field name="email_from" invisible="1"/>
+ <field name="phone" invisible="1"/>
+ <field name="mobile" invisible="1"/>
+ <field name="zip" invisible="1"/>
+ <field name="function" invisible="1"/>
+ <field name="website" invisible="1"/>
</sheet>
</form>
</field>
diff --git a/addons/event_sale/wizard/event_edit_registration.xml b/addons/event_sale/wizard/event_edit_registration.xml
index 47bb964b0f2..5adc5d50bae 100644
--- a/addons/event_sale/wizard/event_edit_registration.xml
+++ b/addons/event_sale/wizard/event_edit_registration.xml
@@ -11,7 +11,7 @@
<field name="event_registration_ids">
<tree string="Registration" editable="top" create="false" delete="false">
<field name="event_id" readonly='1' force_save="1"/>
- <field name="registration_id" readonly='1'/>
+ <field name="registration_id" readonly='1' force_save="1"/>
<field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/>
<field name="name"/>
<field name="email"/>
diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py
index 20370a4a32d..60fa38e7b14 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr.py
@@ -127,7 +127,8 @@ class Employee(models.Model):
], groups="hr.group_hr_user", default="male")
marital = fields.Selection([
('single', 'Single'),
- ('married', 'Married (or similar)'),
+ ('married', 'Married'),
+ ('cohabitant', 'Legal Cohabitant'),
('widower', 'Widower'),
('divorced', 'Divorced')
], string='Marital Status', groups="hr.group_hr_user", default='single')
diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js
index c2c425d2612..69599fa39e3 100644
--- a/addons/hr_org_chart/static/src/js/hr_org_chart.js
+++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js
@@ -13,6 +13,7 @@ var FieldOrgChart = AbstractField.extend({
events: {
"click .o_employee_redirect": "_onEmployeeRedirect",
+ "click .o_employee_sub_redirect": "_onEmployeeSubRedirect",
},
/**
* @constructor
diff --git a/addons/hr_payroll/models/hr_salary_rule.py b/addons/hr_payroll/models/hr_salary_rule.py
index 95c06cbcb56..74052e7b290 100644
--- a/addons/hr_payroll/models/hr_salary_rule.py
+++ b/addons/hr_payroll/models/hr_salary_rule.py
@@ -77,7 +77,7 @@ class HrSalaryRuleCategory(models.Model):
_name = 'hr.salary.rule.category'
_description = 'Salary Rule Category'
- name = fields.Char(required=True)
+ name = fields.Char(required=True, translate=True)
code = fields.Char(required=True)
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
help="Linking a salary category to its parent is used only for the reporting purpose.")
@@ -90,7 +90,7 @@ class HrSalaryRuleCategory(models.Model):
class HrSalaryRule(models.Model):
_name = 'hr.salary.rule'
- name = fields.Char(required=True)
+ name = fields.Char(required=True, translate=True)
code = fields.Char(required=True,
help="The code of salary rules can be used as reference in computation of other rules. "
"In that case, it is case sensitive.")
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index ddfd68e0d56..e37f3139e57 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -18,7 +18,7 @@ class AccountAnalyticLine(models.Model):
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
- department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True)
+ department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True)
@api.onchange('project_id')
def onchange_project_id(self):
@@ -28,6 +28,11 @@ class AccountAnalyticLine(models.Model):
def _onchange_employee_id(self):
self.user_id = self.employee_id.user_id
+ @api.depends('employee_id')
+ def _compute_department_id(self):
+ for line in self:
+ line.department_id = line.employee_id.department_id
+
@api.model
def create(self, vals):
vals = self._timesheet_preprocess(vals)
diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py
index 9cd8776eaf7..86a01497b46 100644
--- a/addons/hw_scanner/controllers/main.py
+++ b/addons/hw_scanner/controllers/main.py
@@ -4,7 +4,7 @@
import logging
import time
from os import listdir
-from os.path import join
+from os.path import join, isdir
try:
from queue import Queue, Empty
except ImportError:
@@ -124,7 +124,7 @@ class Scanner(Thread):
if not evdev:
return []
- if not os.path.isdir(self.input_dir):
+ if not isdir(self.input_dir):
return []
new_devices = [device for device in listdir(self.input_dir)
diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py
index 0e140f9de7f..a4200047315 100644
--- a/addons/im_livechat/models/mail_channel.py
+++ b/addons/im_livechat/models/mail_channel.py
@@ -101,12 +101,6 @@ class MailChannel(models.Model):
empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()]
self.browse(empty_channel_ids).unlink()
- @api.model
- def get_empty_list_help(self, help):
- if help:
- help
- return super(MailChannel, self).get_empty_list_help(help)
-
def _define_command_history(self):
return {
'channel_types': ['livechat'],
diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml
index 0c8d8ec72bb..cc4133b46ab 100644
--- a/addons/im_livechat/views/im_livechat_channel_views.xml
+++ b/addons/im_livechat/views/im_livechat_channel_views.xml
@@ -106,10 +106,11 @@
<templates>
<t t-name="kanban-box">
<div>
- <a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a>
- <div class="oe_group_details" style="min-height: 40px">
- <img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)"
- class="oe_avatar oe_kanban_avatar_smallbox" style="float:left; margin-right: 10px;"/>
+ <a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right"/>
+ <div class="o_kanban_image">
+ <img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)"/>
+ </div>
+ <div class="o_kanban_details">
<h4 class="o_kanban_record_title"><field name="name"/></h4>
</div>
</div>
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 0f6bb86479f..6920f154d9d 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
@@ -160,7 +160,7 @@ elif salary &lt;= 2510.47:
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
- <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital=='married' and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field>
+ <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
wage = categories.BASIC
@@ -184,7 +184,7 @@ else:
<field name="sequence">165</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
- <field name="condition_python">result = (((employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
+ <field name="condition_python">result = (((employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">
wage = categories.BASIC
@@ -208,7 +208,7 @@ else:
<field name="sequence">120</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
- <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
+ <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field>
</record>
<record id="hr_payroll_rules_baremeII" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
@@ -218,7 +218,7 @@ else:
<field name="sequence">120</field>
<field name="condition_select">python</field>
<field name="appears_on_payslip" eval="False"/>
- <field name="condition_python">result = ((employee.marital=='married' and employee.spouse_fiscal_status=='without income') and (employee.resident_bool!=True))</field>
+ <field name="condition_python">result = ((employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='without income') and (employee.resident_bool!=True))</field>
</record>
<record id="hr_payroll_rules_baremeIII" model="hr.salary.rule">
<field name="category_id" ref="hr_payroll_head_pp"/>
@@ -361,7 +361,7 @@ elif salary &lt;= 2461.27:
<field name="code">Red.Spouse.Net</field>
<field name="sequence">148</field>
<field name="condition_select">python</field>
- <field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue &lt;= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
+ <field name="condition_python">result = (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue &lt;= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(110.50, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
@@ -372,7 +372,7 @@ elif salary &lt;= 2461.27:
<field name="code">Red.Spouse.Oth.Net</field>
<field name="sequence">149</field>
<field name="condition_select">python</field>
- <field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue &lt;= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
+ <field name="condition_python">result = (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue &lt;= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA &lt; 0.0)</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = min(220.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field>
</record>
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 37fa58ae2b7..4111ab06878 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
@@ -61,8 +61,8 @@
<field name="transport_mode" widget="radio"/>
<field name="car_atn" attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}" invisible="1"/>
<field name="transport_employer_cost" invisible="1"/>
- <label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}"/>
- <div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}">
+ <label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}" name="public_transport_label"/>
+ <div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}" name="public_transport_div">
<div>
<label for="public_transport_employee_amount" class="oe_inline"/>
<field name="public_transport_employee_amount" class="oe_inline"/>
@@ -72,8 +72,8 @@
<field name="public_transport_reimbursed_amount" class="oe_inline"/>
</div>
</div>
- <label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}"/>
- <div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row">
+ <label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}" name="others_label"/>
+ <div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row" name="others_div">
<field name="others_reimbursed_amount" class="oe_inline" nolabel="1"/>
<span>/ month</span>
</div>
@@ -131,8 +131,8 @@
<field name="resident_bool" eval="False"/>
</xpath>
<xpath expr="//field[@name='marital']" position="after">
- <field name="spouse_fiscal_status" attrs="{'invisible':[('marital','!=','married')],'required':[('marital','=','married')]}" colspan="1" help="if spouse has professionnel income or not"/>
- <field name="disabled_spouse_bool" attrs="{'invisible':[('marital','!=','married')]}" colspan="1"/>
+ <field name="spouse_fiscal_status" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])], 'required': [('marital', 'in', ['married', 'cohabitant'])]}" colspan="1" help="if spouse has professionnel income or not"/>
+ <field name="disabled_spouse_bool" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" colspan="1"/>
</xpath>
<xpath expr="//field[@name='gender']" position="after">
<field name="disabled"/>
@@ -148,8 +148,8 @@
<field name="other_disabled_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/>
</xpath>
<xpath expr="//field[@name='spouse_fiscal_status']" position="after">
- <field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/>
- <field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/>
+ <field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/>
+ <field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/>
</xpath>
</field>
</record>
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 f774eaa0436..a6a3c10ac13 100644
--- a/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py
+++ b/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py
@@ -21,7 +21,7 @@ class HrContract(models.Model):
@api.depends('car_id', 'new_car', 'new_car_model_id')
def _compute_car_atn_and_costs(self):
for contract in self:
- if contract.car_id:
+ if not contract.new_car and contract.car_id:
contract.car_atn = contract.car_id.atn
contract.company_car_total_depreciated_cost = contract.car_id.total_depreciated_cost
elif contract.new_car and contract.new_car_model_id:
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 c6a7ad33abd..184772fc1e5 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
@@ -7,8 +7,8 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='transport_mode']" position="after">
<field name="new_car" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('available_cars_amount', '&gt;', 3)]}"/>
- <label for="car_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}"/>
- <div attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}">
+ <label for="car_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}" name="car_id_label"/>
+ <div attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}" name="car_id_div">
<field name="car_id" nolabel="1" attrs="{'invisible': [('new_car', '=', True)]}"/>
<field name="available_cars_amount" invisible="1"/>
</div>
diff --git a/addons/l10n_ch/models/res_company.py b/addons/l10n_ch/models/res_company.py
index 861dc514ce7..b30e8e13135 100644
--- a/addons/l10n_ch/models/res_company.py
+++ b/addons/l10n_ch/models/res_company.py
@@ -6,4 +6,24 @@ from odoo import fields, models
class Company(models.Model):
_inherit = "res.company"
+ l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
+ l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
l10n_ch_isr_print_bank_location = fields.Boolean(string='Print bank location', default=False, help='Boolean option field indicating whether or not the alternate layout (the one printing bank name and address) must be used when generating an ISR.')
+ l10n_ch_isr_scan_line_left = fields.Float(string='Scan line horizontal offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
+ l10n_ch_isr_scan_line_top = fields.Float(string='Scan line vertical offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr')
+
+ def _compute_l10n_ch_isr(self):
+ get_param = self.env['ir.config_parameter'].sudo().get_param
+ for company in self:
+ company.l10n_ch_isr_preprinted_account = bool(get_param('l10n_ch.isr_preprinted_account', default=False))
+ company.l10n_ch_isr_preprinted_bank = bool(get_param('l10n_ch.isr_preprinted_bank', default=False))
+ company.l10n_ch_isr_scan_line_top = float(get_param('l10n_ch.isr_scan_line_top', default=0))
+ company.l10n_ch_isr_scan_line_left = float(get_param('l10n_ch.isr_scan_line_left', default=0))
+
+ def _set_l10n_ch_isr(self):
+ set_param = self.env['ir.config_parameter'].sudo().set_param
+ for company in self:
+ set_param("l10n_ch.isr_preprinted_account", company.l10n_ch_isr_preprinted_account)
+ set_param("l10n_ch.isr_preprinted_bank", company.l10n_ch_isr_preprinted_bank)
+ set_param("l10n_ch.isr_scan_line_top", company.l10n_ch_isr_scan_line_top)
+ set_param("l10n_ch.isr_scan_line_left", company.l10n_ch_isr_scan_line_left)
diff --git a/addons/l10n_ch/models/res_config_settings.py b/addons/l10n_ch/models/res_config_settings.py
index b1ff9537918..31bc5fc7bed 100644
--- a/addons/l10n_ch/models/res_config_settings.py
+++ b/addons/l10n_ch/models/res_config_settings.py
@@ -7,6 +7,14 @@ from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
+ l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account',
+ related="company_id.l10n_ch_isr_preprinted_account")
+ l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank',
+ related="company_id.l10n_ch_isr_preprinted_bank")
l10n_ch_isr_print_bank_location = fields.Boolean(string="Print bank on ISR",
related="company_id.l10n_ch_isr_print_bank_location",
required=True)
+ l10n_ch_isr_scan_line_left = fields.Float(string='Horizontal offset',
+ related="company_id.l10n_ch_isr_scan_line_left")
+ l10n_ch_isr_scan_line_top = fields.Float(string='Vertical offset',
+ related="company_id.l10n_ch_isr_scan_line_top")
diff --git a/addons/l10n_ch/report/isr_report.xml b/addons/l10n_ch/report/isr_report.xml
index b6966836e8b..4ffa10363c6 100644
--- a/addons/l10n_ch/report/isr_report.xml
+++ b/addons/l10n_ch/report/isr_report.xml
@@ -61,7 +61,7 @@
and location of the bank.-->
<t t-if="print_bank">
<div id="voucher-for-bank">
- <p>
+ <p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank">
<t t-esc="invoice.partner_bank_id.bank_id.name"/><br />
<t t-esc="invoice.partner_bank_id.bank_id.zip"/>
<t t-esc="invoice.partner_bank_id.bank_id.city"/>
@@ -79,12 +79,11 @@
</p>
</div>
- <div id="voucher-bank">
+ <div id="voucher-bank" t-if="not print_bank or not invoice.company_id.l10n_ch_isr_preprinted_account">
<!--Konto/Compte/Conto-->
<p id="voucher-bank_ref" t-field="invoice.l10n_ch_isr_postal_formatted"/>
</div>
- <p id="voucher-currency" t-field="invoice.currency_id.name"/>
<p id="voucher-amount_units" t-esc="split_total_amount[0]"/>
<p id="voucher-amount_cents" t-esc="split_total_amount[1]"/>
@@ -103,7 +102,7 @@
and location of the bank.-->
<t t-if="print_bank">
<div id="slip-for-bank">
- <p>
+ <p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank">
<t t-esc="invoice.partner_bank_id.bank_id.name"/><br />
<t t-esc="invoice.partner_bank_id.bank_id.zip"/>
<t t-esc="invoice.partner_bank_id.bank_id.city"/>
@@ -122,12 +121,11 @@
</p>
</div>
- <div id="slip-bank">
+ <div id="slip-bank" t-if="not print_bank or not invoice.company_id.l10n_ch_isr_preprinted_account">
<!--Konto/Compte/Conto-->
<p id="slip-bank_ref" t-field="invoice.l10n_ch_isr_postal_formatted"/>
</div>
- <p id="slip-currency" t-field="invoice.currency_id.name"/>
<p id="slip-amount_units" t-esc="split_total_amount[0]"/>
<p id="slip-amount_cents" t-esc="split_total_amount[1]"/>
@@ -141,9 +139,11 @@
<address id="slip-by_customer_address" t-field="invoice.partner_id" t-options='{"widget": "contact", "fields": ["address","name"], "no_marker": True}' />
</div>
- <div id="slip-optical">
+ <div id="slip-optical-line">
<!--Optical reference-->
- <span id="slip-optical_ref_line" t-field="invoice.l10n_ch_isr_optical_line"/>
+ <div t-attf-style="top: {{ invoice.company_id.l10n_ch_isr_scan_line_top }}mm; left: {{ invoice.company_id.l10n_ch_isr_scan_line_left }}mm;">
+ <div t-foreach="invoice.l10n_ch_isr_optical_line" t-as="char" t-esc="char" t-attf-style="right: {{ round((char_size - char_index - 1) * 0.1, 1) }}in"/>
+ </div>
</div>
</div>
</div>
diff --git a/addons/l10n_ch/static/src/less/report_isr.less b/addons/l10n_ch/static/src/less/report_isr.less
index be366e54ddd..913404727d2 100644
--- a/addons/l10n_ch/static/src/less/report_isr.less
+++ b/addons/l10n_ch/static/src/less/report_isr.less
@@ -38,6 +38,7 @@ body.l10n_ch_isr #isr {
overflow: hidden;
font-family: "Work Sans", Verdana, Geneva, sans-serif;
+ line-height: 1;
font-size: 13px;
p, address {
@@ -58,25 +59,26 @@ body.l10n_ch_isr #isr {
#voucher > * {
position: absolute;
/* default left margin for fields content */
- left: 1/10in;
+ left: 3/20in;
&#voucher-for-contact {
- top: 3/10in;
+ top: 0.25in;
}
&#voucher-for-bank {
- top: 3/20in;
+ top: 0.18in;
}
&#voucher-for-bank + #voucher-for-contact {
- top: 4/6in;
+ top: 0.68in;
}
&#voucher-bank {
- top: 9/6in;
+ top: 1.51in;
left: 11/10in;
}
+ /* TODO: remove in master */
&#voucher-currency {
top: 9/6 + 3/20in;
@@ -86,24 +88,22 @@ body.l10n_ch_isr #isr {
&#voucher-amount_units {
width: 15/10in;
- top: 11/6in;
+ top: 1.84in;
text-align: right;
- line-height: 1/6in;
- font-weight: 14px;
+ font-size: 14px;
}
&#voucher-amount_cents {
width: 4/10in;
- top: 11/6in;
- left: 19/10in;
+ top: 1.84in;
+ left: 20/10in;
- line-height: 1/6in;
- font-weight: 14px;
+ font-size: 14px;
}
&#voucher-by {
- top: 26/12in;
+ top: 2.18in;
}
}
@@ -121,25 +121,26 @@ body.l10n_ch_isr #isr {
#slip > * {
position: absolute;
/* default left margin for fields content */
- left: 1/10in;
+ left: 3/20in;
&#slip-for-contact {
- top: 3/10in;
+ top: 0.25in;
}
&#slip-for-bank {
- top: 3/20in;
+ top: 0.18in;
}
&#slip-for-bank + #slip-for-contact {
- top: 4/6in;
+ top: 0.68in;
}
&#slip-bank {
- top: 9/6in;
+ top: 1.51in;
left: 11/10in;
}
+ /* TODO: remove in master */
&#slip-currency {
top: 9/6 + 3/20in;
@@ -149,34 +150,52 @@ body.l10n_ch_isr #isr {
&#slip-amount_units {
width: 15/10in;
- top: 11/6in;
+ top: 1.84in;
text-align: right;
- line-height: 1/6in;
- font-weight: 14px;
+ font-size: 14px;
}
&#slip-amount_cents {
width: 4/10in;
- top: 11/6in;
- left: 19/10in;
+ top: 1.84in;
+ left: 20/10in;
- line-height: 1/6in;
- font-weight: 14px;
+ font-size: 14px;
}
&#slip-reference {
- width: 3 + 3/10in;
- top: 7/6in;
- left: 2 + 5/10in;
+ width: 3.3in;
+ top: 1.17in;
+ left: 2.5in;
text-align: center;
}
&#slip-by {
- top: 11/6in;
- left: 2 + 5/10in;
+ top: 1.85in;
+ left: 2.55in;
}
+ &#slip-optical-line {
+ right: 3/10in;
+ bottom: 4/6in;
+
+ /* Positioning is set for OCRB of Tsukurimashou Font Family */
+ font-family: OCRB, Courier, monospace;
+ font-size: 10pt;
+ line-height: 0.1in;
+
+ > div {
+ position: relative;
+
+ > div {
+ position: absolute;
+ bottom: 0;
+ }
+ }
+ }
+
+ /* TODO: remove in master */
&#slip-optical {
right: 3/10in;
bottom: 4/6in;
diff --git a/addons/l10n_ch/views/res_config_settings_views.xml b/addons/l10n_ch/views/res_config_settings_views.xml
index 02847355c7a..2fa7a54386c 100644
--- a/addons/l10n_ch/views/res_config_settings_views.xml
+++ b/addons/l10n_ch/views/res_config_settings_views.xml
@@ -17,10 +17,39 @@
Print the coordinates of your bank under the &apos;Payment for&apos; title of the ISR.
Your address will be moved to the &apos;in favour of&apos; section.
</div>
+ <div class="content-group" attrs="{'invisible': [('l10n_ch_isr_print_bank_location', '=', False)]}">
+ <div class="row mt16">
+ <label for="l10n_ch_isr_preprinted_bank" class="col-md-4 o_light_label"/>
+ <field name="l10n_ch_isr_preprinted_bank"/>
+ </div>
+ <div class="row">
+ <label for="l10n_ch_isr_preprinted_account" class="col-md-4 o_light_label"/>
+ <field name="l10n_ch_isr_preprinted_account"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-xs-12 col-md-6 o_setting_box" id="l10n_ch-isr_print_scanline_offset">
+ <div class="o_setting_left_pane"/>
+ <div class="o_setting_right_pane">
+ <label string="ISR scan line offset"/>
+ <div class="text-muted">
+ Offset to move the the scan line in mm
+ </div>
+ <div class="content-group">
+ <div class="row mt16">
+ <label for="l10n_ch_isr_scan_line_top" class="col-md-4 o_light_label"/>
+ <field name="l10n_ch_isr_scan_line_top"/>
+ </div>
+ <div class="row">
+ <label for="l10n_ch_isr_scan_line_left" class="col-md-4 o_light_label"/>
+ <field name="l10n_ch_isr_scan_line_left"/>
+ </div>
+ </div>
</div>
</div>
</xpath>
</field>
</record>
</data>
-</odoo> \ No newline at end of file
+</odoo>
diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py
index ac778e6be8f..bd8e4cc44c0 100644
--- a/addons/l10n_fr_fec/wizard/account_fr_fec.py
+++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py
@@ -220,7 +220,7 @@ class AccountFrFec(models.TransientModel):
replace(MIN(aa.name), '|', '/') AS CompteLib,
CASE WHEN rp.ref IS null OR rp.ref = ''
THEN COALESCE('ID ' || rp.id, '')
- ELSE rp.ref
+ ELSE replace(rp.ref, '|', '/')
END
AS CompAuxNum,
COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib,
@@ -278,7 +278,7 @@ class AccountFrFec(models.TransientModel):
replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib,
CASE WHEN rp.ref IS null OR rp.ref = ''
THEN COALESCE('ID ' || rp.id, '')
- ELSE rp.ref
+ ELSE replace(rp.ref, '|', '/')
END
AS CompAuxNum,
COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib,
diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py
index bee06394b7d..0c096c4cdf3 100644
--- a/addons/l10n_multilang/models/l10n_multilang.py
+++ b/addons/l10n_multilang/models/l10n_multilang.py
@@ -37,11 +37,11 @@ class AccountChartTemplate(models.Model):
'type': 'model',
'res_id': out_ids[counter].id,
'lang': lang,
- 'src': element.name,
+ 'src': element[in_field],
'value': value[element.id],
})
else:
- _logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element.name))
+ _logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element[in_field]))
counter += 1
return True
diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js
index a3997b14200..8ae45486e16 100644
--- a/addons/mail/static/src/js/services/chat_manager.js
+++ b/addons/mail/static/src/js/services/chat_manager.js
@@ -396,7 +396,7 @@ var ChatManager = AbstractService.extend({
* @param {integer|string} [options.channelID]
* @param {Array} [options.domain]
* @param {integer[]} [options.ids]
- * @param {boolean} [options.load_more]
+ * @param {boolean} [options.loadMore]
* @param {string} [options.model]
* @param {integer} [options.res_id]
* @return {$.Promise<Object[]>} list of messages
@@ -405,8 +405,8 @@ var ChatManager = AbstractService.extend({
var channel;
var self = this;
- if ('channelID' in options && options.load_more) {
- // get channel messages, force load_more
+ if ('channelID' in options && options.loadMore) {
+ // get channel messages, force load more
channel = this.getChannel(options.channelID);
return this._fetchFromChannel(channel, {domain: options.domain || {}, loadMore: true});
}
@@ -1027,7 +1027,7 @@ var ChatManager = AbstractService.extend({
* @param {integer|string} channel.id string for static channels, e.g. 'channel_inbox'
* @param {Object} [option={}]
* @param {Array} [options.domain] filter on the messages of the channel
- * @param {boolean} [options.load_more] Whether it should load more message
+ * @param {boolean} [options.loadMore] Whether it should load more message
* @return {$.Promise<Object[]>} resolved with list of messages
*/
_fetchFromChannel: function (channel, options) {
diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js
index 2a421f6fdcd..97b6bf4906f 100644
--- a/addons/mail/static/src/js/thread.js
+++ b/addons/mail/static/src/js/thread.js
@@ -353,7 +353,7 @@ var Thread = Widget.extend({
} else {
this.trigger('redirect', options.model, options.id);
}
- }, 200, true),
+ }, 500, true),
//--------------------------------------------------------------------------
// Handlers
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index 5568729b02d..e1c0b843482 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -179,7 +179,7 @@
- <small class="o_mail_timestamp" t-att-title="message.date.format(date_format)"><t t-esc="message.hour"/></small>
<t t-if="message.model and (message.model != 'mail.channel') and options.display_document_link">
- <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link">on <t t-esc="message.record_name"/></a>
+ on <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link"><t t-esc="message.record_name"/></a>
</t>
<t t-if="message.origin_id and (message.origin_id !== options.channel_id)">
(from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>)
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index f258a5aefa9..47bcf0a95b3 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -31,7 +31,7 @@ class MaintenanceEquipmentCategory(models.Model):
self.fold = False if self.equipment_count else True
name = fields.Char('Category Name', required=True, translate=True)
- company_id = fields.Many2one('res.company', string='Company', required=True,
+ company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user.company_id)
technician_user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid, oldname='user_id')
color = fields.Integer('Color Index')
@@ -122,7 +122,7 @@ class MaintenanceEquipment(models.Model):
return recs.name_get()
name = fields.Char('Equipment Name', required=True, translate=True)
- company_id = fields.Many2one('res.company', string='Company', required=True,
+ company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user.company_id)
active = fields.Boolean(default=True)
technician_user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange', oldname='user_id')
@@ -273,10 +273,14 @@ class MaintenanceRequest(models.Model):
return super(MaintenanceRequest, self)._track_subtype(init_values)
def _get_default_team_id(self):
- return self.env['maintenance.team'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).id
+ MT = self.env['maintenance.team']
+ team = MT.search([('company_id', '=', self.env.user.company_id.id)], limit=1)
+ if not team:
+ team = MT.search([], limit=1)
+ return team.id
name = fields.Char('Subjects', required=True)
- company_id = fields.Many2one('res.company', string='Company', required=True,
+ company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user.company_id)
description = fields.Text('Description')
request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today,
@@ -367,7 +371,7 @@ class MaintenanceTeam(models.Model):
name = fields.Char(required=True, translate=True)
active = fields.Boolean(default=True)
- company_id = fields.Many2one('res.company', string='Company', required=True,
+ company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user.company_id)
member_ids = fields.Many2many('res.users', 'maintenance_team_users_rel', string="Team Members")
color = fields.Integer("Color Index", default=0)
diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py
index 348d4d04dfa..6b66bfb07b5 100644
--- a/addons/membership/models/partner.py
+++ b/addons/membership/models/partner.py
@@ -51,7 +51,7 @@ class Partner(models.Model):
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
'member_lines.account_invoice_line.invoice_id.payment_ids',
'free_member',
- 'member_lines.date_to', 'member_lines.date_from',
+ 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel',
'membership_state',
'associate_member.membership_state')
def _compute_membership_start(self):
@@ -65,7 +65,7 @@ class Partner(models.Model):
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
'member_lines.account_invoice_line.invoice_id.payment_ids',
'free_member',
- 'member_lines.date_to', 'member_lines.date_from',
+ 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel',
'membership_state',
'associate_member.membership_state')
def _compute_membership_stop(self):
@@ -79,7 +79,7 @@ class Partner(models.Model):
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
'member_lines.account_invoice_line.invoice_id.payment_ids',
'free_member',
- 'member_lines.date_to', 'member_lines.date_from',
+ 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel',
'membership_state',
'associate_member.membership_state')
def _compute_membership_cancel(self):
diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py
index d9fa6fcf6f6..e08d759ff45 100644
--- a/addons/mrp/models/product.py
+++ b/addons/mrp/models/product.py
@@ -56,14 +56,17 @@ class ProductProduct(models.Model):
mo_count = fields.Integer('# Manufacturing Orders', compute='_compute_mo_count')
def _compute_bom_count(self):
+ # read_group_res: BOM where product_id is set
+ # read_group_res_tmpl: BOM where product_tmpl_id is set and product_id is not set
+ # The total count is the sum of both.
read_group_res = self.env['mrp.bom'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id'])
mapped_data = dict([(data['product_id'][0], data['product_id_count']) for data in read_group_res])
+ read_group_res_tmpl = self.env['mrp.bom'].read_group([
+ ('product_tmpl_id', 'in', self.mapped('product_tmpl_id.id')), ('product_id', '=', False)
+ ], ['product_tmpl_id'], ['product_tmpl_id'])
+ mapped_data_tmpl = dict([(data['product_tmpl_id'][0], data['product_tmpl_id_count']) for data in read_group_res_tmpl])
for product in self:
- if product.product_tmpl_id.product_variant_count == 1:
- bom_count = mapped_data.get(product.id, product.product_tmpl_id.bom_count)
- else:
- bom_count = mapped_data.get(product.id, 0)
- product.bom_count = bom_count
+ product.bom_count = mapped_data.get(product.id, 0) + mapped_data_tmpl.get(product.product_tmpl_id.id, 0)
@api.multi
def _compute_used_in_bom_count(self):
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index d68df6f9934..d9617a633a2 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -313,6 +313,7 @@ class PaymentAcquirer(models.Model):
'billing_partner': billing_partner,
'billing_partner_id': billing_partner_id,
'billing_partner_name': billing_partner.name,
+ 'billing_partner_commercial_company_name': billing_partner.commercial_company_name,
'billing_partner_lang': billing_partner.lang,
'billing_partner_email': billing_partner.email,
'billing_partner_zip': billing_partner.zip,
@@ -845,16 +846,13 @@ class PaymentToken(models.Model):
'partner_country_id': self.partner_id.country_id.id,
})
- try:
- kwargs.update({'3d_secure': True})
- tx.s2s_do_transaction(**kwargs)
- # if 3D secure is called, then we do not refund right now
- if tx.html_3ds:
- return tx
- except:
- _logger.error('Error while validating a payment method')
- finally:
+ kwargs.update({'3d_secure': True})
+ tx.s2s_do_transaction(**kwargs)
+
+ # if 3D secure is called, then we do not refund right now
+ if not tx.html_3ds:
tx.s2s_do_refund()
+
return tx
@api.multi
diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js
index 20216c4cdad..1e0d5e7a0c2 100644
--- a/addons/payment/static/src/js/payment_form.js
+++ b/addons/payment/static/src/js/payment_form.js
@@ -106,6 +106,7 @@ odoo.define('payment.payment_form', function (require) {
else {
checked_radio.value = data.id; // set the radio value to the new card id
form.submit();
+ return;
}
}
// if the server has returned false, we display an error
diff --git a/addons/payment/tests/common.py b/addons/payment/tests/common.py
index 4caa6206dc8..4cdad303018 100644
--- a/addons/payment/tests/common.py
+++ b/addons/payment/tests/common.py
@@ -25,6 +25,7 @@ class PaymentAcquirerCommon(common.TransactionCase):
'partner_country_id': self.country_belgium.id,
'partner_country_name': 'Belgium',
'billing_partner_name': 'Norbert Buyer',
+ 'billing_partner_commercial_company_name': 'Big Company',
'billing_partner_lang': 'en_US',
'billing_partner_email': 'norbert.buyer@example.com',
'billing_partner_address': 'Huge Street 2/543',
diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml
index 45ad4a9b0e6..6ffee52c975 100644
--- a/addons/payment/views/payment_templates.xml
+++ b/addons/payment/views/payment_templates.xml
@@ -84,7 +84,7 @@
<t t-call="{{acq.sudo().get_s2s_form_xml_id()}}">
<t t-set="id" t-value="acq.id"/>
<t t-set="partner_id" t-value="partner_id"/>
- <t t-set="return_url" t-value="''"/>
+ <t t-if="not return_url" t-set="return_url" t-value="''"/>
</t>
</div>
</div>
diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py
index dbc24ced0c0..3b15e51d44f 100644
--- a/addons/payment_adyen/models/payment.py
+++ b/addons/payment_adyen/models/payment.py
@@ -78,7 +78,7 @@ class AcquirerAdyen(models.Model):
]
hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii'))
- raw_values = {k: values.get(k.encode('ascii'), '') for k in keys if k in values}
+ raw_values = {k: values.get(k, '') for k in keys if k in values}
raw_values_ordered = OrderedDict(sorted(raw_values.items(), key=lambda t: t[0]))
return signParams(raw_values_ordered)
@@ -151,8 +151,8 @@ class AcquirerAdyen(models.Model):
'sessionValidity': tmp_date,
'resURL': urls.url_join(base_url, AdyenController._return_url),
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url') else False,
- 'merchantSig': self._adyen_generate_merchant_sig('in', values),
})
+ values['merchantSig'] = self._adyen_generate_merchant_sig('in', values)
return values
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index c354f2e6aa9..b10f2824733 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -127,8 +127,18 @@ class PaymentAcquirerAuthorize(models.Model):
for field_name in mandatory_fields:
if not data.get(field_name):
error[field_name] = 'missing'
- if data['cc_expiry'] and datetime.now().strftime('%y%m') > datetime.strptime(data['cc_expiry'], '%m / %y').strftime('%y%m'):
- return False
+ if data['cc_expiry']:
+ # FIX we split the date into their components and check if there is two components containing only digits
+ # this fixes multiples crashes, if there was no space between the '/' and the components the code was crashing
+ # the code was also crashing if the customer was proving non digits to the date.
+ cc_expiry = [i.strip() for i in data['cc_expiry'].split('/')]
+ if len(cc_expiry) != 2 or any(not i.isdigit() for i in cc_expiry):
+ return False
+ try:
+ if datetime.now().strftime('%y%m') > datetime.strptime('/'.join(cc_expiry), '%m/%y').strftime('%y%m'):
+ return False
+ except ValueError:
+ return False
return False if error else True
@api.multi
diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py
index f903f05b759..242fb20d265 100644
--- a/addons/payment_authorize/tests/test_authorize.py
+++ b/addons/payment_authorize/tests/test_authorize.py
@@ -63,6 +63,7 @@ class AuthorizeForm(AuthorizeCommon):
'x_invoice_num': 'SO004',
'x_first_name': 'Norbert',
'x_last_name': 'Buyer',
+ 'x_company': 'Big Company',
'x_address': 'Huge Street 2/543',
'x_city': 'Sin City',
'x_zip': '1000',
diff --git a/addons/payment_authorize/views/payment_authorize_templates.xml b/addons/payment_authorize/views/payment_authorize_templates.xml
index dcc88b9df37..8002b5aae57 100644
--- a/addons/payment_authorize/views/payment_authorize_templates.xml
+++ b/addons/payment_authorize/views/payment_authorize_templates.xml
@@ -23,6 +23,7 @@
<!-- Billing Information-->
<input type='hidden' name='x_first_name' t-att-value="billing_first_name"/>
<input type='hidden' name='x_last_name' t-att-value="billing_last_name"/>
+ <input type="hidden" name="x_company" t-att-value="billing_partner_commercial_company_name"/>
<input type='hidden' name='x_address' t-att-value="billing_address"/>
<input type='hidden' name='x_city' t-att-value="billing_city"/>
<input type='hidden' name='x_zip' t-att-value="billing_zip_code"/>
diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py
index 09df4b52bc4..8771addc151 100644
--- a/addons/payment_ogone/models/payment.py
+++ b/addons/payment_ogone/models/payment.py
@@ -214,8 +214,8 @@ class PaymentTxOgone(models.Model):
_inherit = 'payment.transaction'
# ogone status
_ogone_valid_tx_status = [5, 9, 8]
- _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99, 81, 82]
- _ogone_pending_tx_status = [46] # 3DS HTML response
+ _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
+ _ogone_pending_tx_status = [46, 81, 82] # 46 = 3DS HTML response
_ogone_cancel_tx_status = [1]
# --------------------------------------------------
@@ -283,7 +283,7 @@ class PaymentTxOgone(models.Model):
return invalid_parameters
def _ogone_form_validate(self, data):
- if self.state in ['done', 'refunded']:
+ if self.state in ['done', 'refunding', 'refunded']:
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference)
return True
@@ -466,6 +466,9 @@ class PaymentTxOgone(models.Model):
if self.payment_token_id:
self.payment_token_id.verified = True
self.execute_callback()
+ # if this transaction is a validation one, then we refund the money we just withdrawn
+ if self.type == 'validation':
+ self.s2s_do_refund()
return True
elif status in self._ogone_cancel_tx_status:
self.write({
@@ -474,11 +477,13 @@ class PaymentTxOgone(models.Model):
})
elif status in self._ogone_pending_tx_status:
new_state = 'refunding' if self.state == 'refunding' else 'pending'
- self.write({
+ vals = {
'state': new_state,
'acquirer_reference': tree.get('PAYID'),
- 'html_3ds': ustr(base64.b64decode(tree.HTML_ANSWER.text)),
- })
+ }
+ if status == 46: # HTML 3DS
+ vals['html_3ds'] = ustr(base64.b64decode(tree.HTML_ANSWER.text))
+ self.write(vals)
elif status in self._ogone_wait_tx_status and tries > 0:
time.sleep(0.5)
self.write({'acquirer_reference': tree.get('PAYID')})
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 591488c225c..f8a6c749929 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -194,7 +194,7 @@ exports.PosModel = Backbone.Model.extend({
},
},{
model: 'res.country',
- fields: ['name'],
+ fields: ['name', 'vat_label'],
loaded: function(self,countries){
self.countries = countries;
self.company.country = null;
@@ -2119,6 +2119,7 @@ exports.Order = Backbone.Model.extend({
company_registry: company.company_registry,
contact_address: company.partner_id[1],
vat: company.vat,
+ vat_label: company.country.vat_label,
name: company.name,
phone: company.phone,
logo: this.pos.company_logo_base64,
@@ -2263,7 +2264,9 @@ exports.Order = Backbone.Model.extend({
}
})
- unit_price = line.compute_all(mapped_included_taxes, unit_price, 1, this.pos.currency.rounding, true).total_excluded;
+ if (mapped_included_taxes.length > 0) {
+ unit_price = line.compute_all(mapped_included_taxes, unit_price, 1, this.pos.currency.rounding, true).total_excluded;
+ }
line.set_unit_price(unit_price);
}
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
index b8ff6f8f2fd..9fb2c6b94ac 100644
--- a/addons/portal/controllers/mail.py
+++ b/addons/portal/controllers/mail.py
@@ -6,7 +6,7 @@ from werkzeug.exceptions import NotFound, Forbidden
from odoo import http
from odoo.http import request
from odoo.osv import expression
-from odoo.tools import consteq
+from odoo.tools import consteq, plaintext2html
def _has_token_access(res_model, res_id, token=''):
@@ -59,6 +59,8 @@ class PortalChatter(http.Controller):
def portal_chatter_post(self, res_model, res_id, message, **kw):
url = request.httprequest.referrer
if message:
+ # message is received in plaintext and saved in html
+ message = plaintext2html(message)
_message_post_helper(res_model, int(res_id), message, **kw)
url = url + "#discussion"
return request.redirect(url)
diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml
index 1d5d3a79f3f..d93ca1f6d27 100644
--- a/addons/portal/views/portal_templates.xml
+++ b/addons/portal/views/portal_templates.xml
@@ -352,7 +352,7 @@
the message will be posted with the identity of the partner_id of the object
-->
<template id="message_thread">
- <div id="discussion" class="hidden-print o_portal_chatter"
+ <div id="discussion" class="hidden-print o_portal_chatter o_not_editable"
t-att-data-token="token" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
</div>
</template>
diff --git a/addons/pos_restaurant/static/src/xml/printbill.xml b/addons/pos_restaurant/static/src/xml/printbill.xml
index 2771409e5e1..0c3f5ae7530 100644
--- a/addons/pos_restaurant/static/src/xml/printbill.xml
+++ b/addons/pos_restaurant/static/src/xml/printbill.xml
@@ -54,7 +54,7 @@
<div>Tel:<t t-esc='receipt.company.phone' /></div>
</t>
<t t-if='receipt.company.vat'>
- <div><t t-esc="receipt.company.country_id.vat_label or 'TIN'"/>:<t t-esc='receipt.company.vat' /></div>
+ <div><t t-esc="receipt.company.vat_label or 'TIN'"/>:<t t-esc='receipt.company.vat' /></div>
</t>
<t t-if='receipt.company.email'>
<div><t t-esc='receipt.company.email' /></div>
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 166a0a44506..2df63cf1e66 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -119,6 +119,7 @@ class ProductTemplate(models.Model):
active = fields.Boolean('Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.")
color = fields.Integer('Color Index')
+ is_product_variant = fields.Boolean(string='Is a product variant', compute='_compute_is_product_variant')
attribute_line_ids = fields.One2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes')
product_variant_ids = fields.One2many('product.product', 'product_tmpl_id', 'Products', required=True)
# performance: product_variant_id provides prefetching on the first product variant only
@@ -236,6 +237,13 @@ class ProductTemplate(models.Model):
for template in (self - unique_variants):
template.weight = 0.0
+ def _compute_is_product_variant(self):
+ for template in self:
+ if template._name == 'product.template':
+ template.is_product_variant = False
+ else:
+ template.is_product_variant = True
+
@api.model
def _get_weight_uom_id_from_ir_config_parameter(self):
""" Get the unit of measure to interpret the `weight` field. By default, we considerer
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index d4ffa71f15f..84c383620af 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -16,6 +16,7 @@
</header>
<sheet>
<field name='product_variant_count' invisible='1'/>
+ <field name='is_product_variant' invisible='1'/>
<field name="id" invisible="True"/>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
@@ -48,8 +49,8 @@
</group>
<group name="group_standard_price">
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
- <label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1)]}"/>
- <div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1)]}">
+ <label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1), ('is_product_variant', '=', False)]}"/>
+ <div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1), ('is_product_variant', '=', False)]}">
<field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
</div>
<field name="company_id" groups="base.group_multi_company"
@@ -343,7 +344,8 @@
</field>
</field>
<xpath expr="//div[hasclass('oe_title')]" position="inside">
- <field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
+ <field name="attribute_value_ids" widget="many2many_tags" readonly="1"
+ groups="product.group_product_variant"/>
</xpath>
</field>
</record>
diff --git a/addons/product_extended/models/product.py b/addons/product_extended/models/product.py
index 0efd659dd79..19ef8e08492 100644
--- a/addons/product_extended/models/product.py
+++ b/addons/product_extended/models/product.py
@@ -37,9 +37,7 @@ class ProductProduct(models.Model):
price = 0.0
result, result2 = bom.explode(self, 1)
for sbom, sbom_data in result2:
- if not sbom.attribute_value_ids:
- # No attribute_value_ids means the bom line is not variant specific
- price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty']
+ price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty']
if bom.routing_id:
# FIXME master: remove me
if hasattr(self.env['mrp.workcenter'], 'costs_hour'):
diff --git a/addons/product_extended/views/product_views.xml b/addons/product_extended/views/product_views.xml
index 4e065e19604..2b7e4eca022 100644
--- a/addons/product_extended/views/product_views.xml
+++ b/addons/product_extended/views/product_views.xml
@@ -35,4 +35,24 @@
</div>
</field>
</record>
+
+ <record id="product_product_view_form_easy_inherit_extended" model="ir.ui.view">
+ <field name="name">product.product.view.form.easy.inherit.extended</field>
+ <field name="model">product.product</field>
+ <field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
+ <field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
+ <field name="arch" type="xml">
+ <field name="standard_price" position="after">
+ <field name="cost_method" invisible='1'/>
+ <field name="bom_count" invisible='1'/>
+ <button name="compute_price"
+ string="Compute from BOM" type="object"
+ attrs="{'invisible': ['|', ('cost_method', '!=', 'standard'), ('bom_count', '=', 0)]}"
+ help="Compute the price of the product using products and operations of related bill of materials, for manufactured products only."
+ class="oe_link oe_read_only"
+ colspan="2"/>
+ </field>
+ </field>
+ </record>
+
</odoo>
diff --git a/addons/project/static/src/js/tour.js b/addons/project/static/src/js/tour.js
index c0aeaa15924..e49c1325136 100644
--- a/addons/project/static/src/js/tour.js
+++ b/addons/project/static/src/js/tour.js
@@ -100,7 +100,7 @@ tour.STEPS.MENU_MORE, {
}, tour.STEPS.TOGGLE_HOME_MENU,
tour.STEPS.MENU_MORE, {
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"], .oe_menu_toggler[data-menu-xmlid="project.menu_main_pm"]',
- content: _t("Good job! Your completed the Project Management tour."),
+ content: _t("Good job! You completed the Project Management tour."),
position: 'bottom',
}]);
diff --git a/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py b/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py
index 90776a534f4..a7e484f18d4 100644
--- a/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py
+++ b/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py
@@ -33,8 +33,7 @@ class TestTimesheetHolidays(TestTimesheet):
# leave dates : from next monday to next wednesday (to avoid crashing tests on weekend, when
# there is no work days in working calendar)
# NOTE: second and millisecond can add a working days
- today = datetime.combine(datetime.today(), time.min)
- self.leave_start_datetime = today.replace(hour=7) + relativedelta(weeks=0, days=1, weekday=0)
+ self.leave_start_datetime = datetime(2018, 2, 5, 7, 0, 0, 0) # this is monday
self.leave_end_datetime = self.leave_start_datetime + relativedelta(days=3)
# all company have those internal project/task (created by default)
diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml
index dc51e377149..d3c075c3ff0 100644
--- a/addons/purchase/data/mail_template_data.xml
+++ b/addons/purchase/data/mail_template_data.xml
@@ -15,7 +15,7 @@
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html"><![CDATA[
<p>Dear
-% if object.partner_id.is_company and object.child_ids:
+% if object.partner_id.is_company and object.partner_id.child_ids:
${object.partner_id.child_ids[0].name}
% else :
${object.partner_id.name}
@@ -55,7 +55,7 @@ from ${object.company_id.name}.
<field name="report_name">PO_${(object.name or '').replace('/','_')}</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html"><![CDATA[
-<p>Dear ${object.partner_id.name}
+<p>Dear ${object.partner_id.name}
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
% endif
diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py
index fa38fa44bae..295632baffb 100644
--- a/addons/purchase/models/stock.py
+++ b/addons/purchase/models/stock.py
@@ -11,17 +11,6 @@ class StockPicking(models.Model):
purchase_id = fields.Many2one('purchase.order', related='move_lines.purchase_line_id.order_id',
string="Purchase Orders", readonly=True)
- @api.model
- def _create_backorder(self, backorder_moves=[]):
- res = super(StockPicking, self)._create_backorder(backorder_moves)
- for picking in self:
- if picking.picking_type_id.code == 'incoming':
- for backorder in self.search([('backorder_id', '=', picking.id)]):
- backorder.message_post_with_view('mail.message_origin_link',
- values={'self': backorder, 'origin': backorder.purchase_id},
- subtype_id=self.env.ref('mail.mt_note').id)
- return res
-
class StockMove(models.Model):
_inherit = 'stock.move'
diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py
index 17753815848..abfa039b9ef 100644
--- a/addons/sale/__init__.py
+++ b/addons/sale/__init__.py
@@ -16,8 +16,6 @@ def uninstall_hook(cr, registry):
db_registry = odoo.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
- if 'account.invoice.report' in env:
- env['account.invoice.report'].init()
if 'crm.team' in env:
recs = env['crm.team'].search([])
for rec in recs:
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 1913f5859ed..5a4c946d14e 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -575,7 +575,7 @@ class SaleOrder(models.Model):
report_pages.append([])
# Append category to current report page
report_pages[-1].append({
- 'name': category and category.name or 'Uncategorized',
+ 'name': category and category.name or _('Uncategorized'),
'subtotal': category and category.subtotal,
'pagebreak': category and category.pagebreak,
'lines': list(lines)
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index 46cc2639fa5..3c518b9a755 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -114,7 +114,7 @@
<span class="label label-danger"><i class="fa fa-fw fa-warning"></i><span class="hidden-xs"> Problem</span></span>
</t>
<t t-if="order.state == 'done'">
- <span class="label label-default hidden-xs"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Done</span></span>
+ <span class="label label-success hidden-xs"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Done</span></span>
</t>
</td>
<td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td>
@@ -148,7 +148,7 @@
<span class="label label-default"><i class="fa fa-fw fa-remove"/> Cancelled</span>
</t>
<t t-if="order.state == 'done'">
- <span class="label label-default"><i class="fa fa-fw fa-remove"/> Done</span>
+ <span class="label label-success"><i class="fa fa-fw fa-check" /> Done</span>
</t>
</h4>
</div>
diff --git a/addons/sale_margin/__init__.py b/addons/sale_margin/__init__.py
index c76a7ba77c4..0d226707e04 100644
--- a/addons/sale_margin/__init__.py
+++ b/addons/sale_margin/__init__.py
@@ -1,20 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from functools import partial
-import odoo
-from odoo import api, SUPERUSER_ID
-
from . import models # noqa
from . import report # noqa
-
-
-def uninstall_hook(cr, registry):
- def recreate_view(dbname):
- db_registry = odoo.modules.registry.Registry.new(dbname)
- with api.Environment.manage(), db_registry.cursor() as cr:
- env = api.Environment(cr, SUPERUSER_ID, {})
- if 'sale.report' in env:
- env['sale.report'].init()
-
- cr.after("commit", partial(recreate_view, cr.dbname))
diff --git a/addons/sale_margin/__manifest__.py b/addons/sale_margin/__manifest__.py
index 7ead9ff4c02..0da258e4932 100644
--- a/addons/sale_margin/__manifest__.py
+++ b/addons/sale_margin/__manifest__.py
@@ -15,5 +15,4 @@ Price and Cost Price.
'depends':['sale_management'],
'demo':['data/sale_margin_demo.xml'],
'data':['security/ir.model.access.csv','views/sale_margin_view.xml'],
- 'uninstall_hook': "uninstall_hook",
}
diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py
index ebb1ee59d9c..cef63155645 100644
--- a/addons/sale_stock/models/stock.py
+++ b/addons/sale_stock/models/stock.py
@@ -50,17 +50,6 @@ class StockPicking(models.Model):
sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True)
- @api.multi
- def _create_backorder(self, backorder_moves=[]):
- res = super(StockPicking, self)._create_backorder(backorder_moves)
- for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'):
- backorder = picking.search([('backorder_id', '=', picking.id)])
- if backorder.sale_id:
- backorder.message_post_with_view(
- 'mail.message_origin_link',
- values={'self': backorder, 'origin': backorder.sale_id},
- subtype_id=self.env.ref('mail.mt_note').id)
- return res
def _log_less_quantities_than_expected(self, moves):
""" Log an activity on sale order that are linked to moves. The
@@ -102,4 +91,4 @@ class StockPicking(models.Model):
documents = self._log_activity_get_documents(moves, 'sale_line_id', 'DOWN', _keys_in_sorted, _keys_in_groupby)
self._log_activity(_render_note_exception_quantity, documents)
- return super(StockPicking, self)._log_less_quantities_than_expected(moves)
+ return super(StockPicking, self)._log_less_quantities_than_expected(moves) \ No newline at end of file
diff --git a/addons/stock/data/stock_data.xml b/addons/stock/data/stock_data.xml
index 70fe78291f1..bc37a9bb572 100644
--- a/addons/stock/data/stock_data.xml
+++ b/addons/stock/data/stock_data.xml
@@ -120,7 +120,6 @@
<record id="warehouse0" model="stock.warehouse">
- <field model="res.company" name="name" search="[]" use="name"/>
<field name="partner_id" ref="base.main_partner"/>
<field name="code">WH</field>
</record>
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 92f3a9d1351..379ae43a456 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1073,18 +1073,8 @@ class StockMove(models.Model):
return moves_todo
if picking:
- moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
- if moves_to_backorder:
- backorder_picking = picking.copy({
- 'name': '/',
- 'move_lines': [],
- 'move_line_ids': [],
- 'backorder_id': picking.id
- })
- picking.message_post(body=_('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % (backorder_picking.id, backorder_picking.name))
- moves_to_backorder.write({'picking_id': backorder_picking.id})
- moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
- moves_to_backorder._action_assign()
+ picking._create_backorder()
+
return moves_todo
def unlink(self):
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index f812df8ea35..fa6f4fae341 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -378,9 +378,9 @@ class Picking(models.Model):
@api.depends('move_lines.date_expected')
def _compute_scheduled_date(self):
if self.move_type == 'direct':
- self.scheduled_date = min(self.move_lines.mapped('date_expected') or [False])
+ self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
else:
- self.scheduled_date = max(self.move_lines.mapped('date_expected') or [False])
+ self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
@api.one
def _set_scheduled_date(self):
@@ -806,31 +806,25 @@ class Picking(models.Model):
@api.multi
def _create_backorder(self, backorder_moves=[]):
- """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
+ """ Move all non-done lines into a new backorder picking.
"""
- # TDE note: o2o conversion, todo multi
backorders = self.env['stock.picking']
for picking in self:
- backorder_moves = backorder_moves or picking.move_lines
- if self._context.get('do_only_split'):
- not_done_bo_moves = backorder_moves.filtered(lambda move: move.id not in self._context.get('split', []))
- else:
- not_done_bo_moves = backorder_moves.filtered(lambda move: move.state not in ('done', 'cancel'))
- if not not_done_bo_moves:
- continue
- backorder_picking = picking.copy({
- 'name': '/',
- 'move_lines': [],
- 'move_line_ids': [],
- 'backorder_id': picking.id
- })
- picking.message_post(body=_("Back order <em>%s</em> <b>created</b>.") % (backorder_picking.name))
- not_done_bo_moves.write({'picking_id': backorder_picking.id})
- if not picking.date_done:
- picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
- backorder_picking.action_confirm()
- backorder_picking.action_assign()
- backorders |= backorder_picking
+ moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
+ if moves_to_backorder:
+ backorder_picking = picking.copy({
+ 'name': '/',
+ 'move_lines': [],
+ 'move_line_ids': [],
+ 'backorder_id': picking.id
+ })
+ picking.message_post(
+ body=_('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % (
+ backorder_picking.id, backorder_picking.name))
+ moves_to_backorder.write({'picking_id': backorder_picking.id})
+ moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
+ backorder_picking.action_assign()
+ backorders |= backorder_picking
return backorders
def _log_activity_get_documents(self, orig_obj_changes, stream_field, stream, sorted_method=False, groupby_method=False):
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 005b89be02d..661c764d972 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -21,7 +21,7 @@ class Warehouse(models.Model):
# namedtuple used in helper methods generating values for routes
Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type'])
- name = fields.Char('Warehouse Name', index=True, required=True)
+ name = fields.Char('Warehouse Name', index=True, required=True, default=lambda self: self.env['res.company']._company_default_get('stock.inventory').name)
active = fields.Boolean('Active', default=True)
company_id = fields.Many2one(
'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.inventory'),
@@ -684,11 +684,11 @@ class Warehouse(models.Model):
def _get_sequence_values(self):
return {
- 'in_type_id': {'name': self.name + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
- 'out_type_id': {'name': self.name + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
- 'pack_type_id': {'name': self.name + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
- 'pick_type_id': {'name': self.name + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
- 'int_type_id': {'name': self.name + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
+ 'in_type_id': {'name': self.name + ' ' + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
+ 'out_type_id': {'name': self.name + ' ' + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
+ 'pack_type_id': {'name': self.name + ' ' + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
+ 'pick_type_id': {'name': self.name + ' ' + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
+ 'int_type_id': {'name': self.name + ' ' + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
}
def _format_rulename(self, from_loc, dest_loc, suffix):
diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml
index 60627d9f596..a4020c61b99 100644
--- a/addons/stock/report/report_deliveryslip.xml
+++ b/addons/stock/report/report_deliveryslip.xml
@@ -53,20 +53,19 @@
<thead>
<tr>
<th><strong>Product</strong></th>
- <th><strong>Ordered Quantity</strong></th>
+ <th><strong>Quantity</strong></th>
</tr>
</thead>
<tbody>
<tr t-foreach="o.move_lines" t-as="move">
<td><span t-field="move.product_id"/></td>
<td>
- <span t-field="move.ordered_qty"/>
+ <span t-field="move.quantity_done"/>
<span t-field="move.product_uom"/>
</td>
</tr>
</tbody>
</table>
- <t t-set="backorder" t-value="False"/>
<table class="table table-condensed mt48" t-if="o.move_line_ids">
<t t-set="has_serial_number" t-value="o.move_line_ids.mapped('lot_id')" groups="stock.group_production_lot"/>
<thead>
@@ -75,10 +74,7 @@
<th name="lot_serial" t-if="has_serial_number">
Lot/Serial Number
</th>
- <th class="text-center"><strong>Ordered Quantity</strong></th>
- <th t-if="any([move_line.state == 'done' for move_line in o.move_line_ids])" class="text-right">
- <strong>Delivered Quantity</strong>
- </th>
+ <th class="text-center"><strong>Quantity</strong></th>
</tr>
</thead>
<tbody>
@@ -112,13 +108,6 @@
</td>
</t>
<td class="text-center">
- <span t-field="move_line.ordered_qty"/>
- <span t-field="move_line.product_uom_id"/>
- </td>
- <td class="text-right" t-if="move_line.state == 'done'">
- <t t-if="move_line.ordered_qty != move_line.qty_done">
- <t t-set="backorder" t-value="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])"/>
- </t>
<span t-field="move_line.qty_done"/>
<span t-field="move_line.product_uom_id"/>
</td>
@@ -129,7 +118,7 @@
This shipment is a backorder of <t t-esc="o.backorder_id.name"/>.
</p>
<p>
- <t t-if="backorder">
+ <t t-if="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])">
All items couldn't be shipped, the remaining ones will be shipped as soon as they become available.
</t>
</p>
diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml
index 06a0e32243c..717a500d4fe 100644
--- a/addons/stock/report/report_stockpicking_operations.xml
+++ b/addons/stock/report/report_stockpicking_operations.xml
@@ -127,13 +127,13 @@
<span t-field="ml.lot_id"/>
<span t-if="not ml.lot_id" t-field="ml.lot_name"/>
</div>
- <div class="col-xs-8">
- <span t-field="ml.qty_done"/>
- <t t-if="o.state!='done'">
- / <span t-field="ml.product_uom_qty"/>
- </t>
- </div>
</t>
+ <div class="col-xs-8">
+ <span t-field="ml.qty_done"/>
+ <t t-if="o.state!='done'">
+ / <span t-field="ml.product_uom_qty"/>
+ </t>
+ </div>
</td>
<td width="30%" align="right">
<div>
diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml
index d7065beb8f8..5b009c78ed8 100644
--- a/addons/stock/views/stock_move_views.xml
+++ b/addons/stock/views/stock_move_views.xml
@@ -73,7 +73,6 @@
<kanban class="o_kanban_mobile">
<field name="name"/>
<field name="product_id"/>
- <field name="date"/>
<field name="priority"/>
<field name="state"/>
<templates>
diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py
index b29e4080e3b..1528d134079 100644
--- a/addons/stock/wizard/stock_change_product_qty.py
+++ b/addons/stock/wizard/stock_change_product_qty.py
@@ -94,7 +94,7 @@ class ProductChangeQuantity(models.TransientModel):
else:
inventory_filter = 'none'
inventory = Inventory.create({
- 'name': _('INV: %s') % tools.ustr(wizard.product_id.name),
+ 'name': _('INV: %s') % tools.ustr(wizard.product_id.display_name),
'filter': inventory_filter,
'product_id': wizard.product_id.id,
'location_id': wizard.location_id.id,
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index a680bd4d8c1..a128f5a6942 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -5,7 +5,7 @@ from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
-from odoo.tools import float_compare, float_round, pycompat
+from odoo.tools import float_compare, float_round, float_is_zero, pycompat
import logging
_logger = logging.getLogger(__name__)
@@ -353,8 +353,11 @@ class StockMove(models.Model):
std_price_update = {}
for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move.product_id.cost_method == 'average'):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
+ rounding = move.product_id.uom_id.rounding
- if product_tot_qty_available == 0:
+ if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
+ new_std_price = move._get_price_unit()
+ elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding):
new_std_price = move._get_price_unit()
else:
# Get the standard price
diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py
index 8f4094d0849..083138e6275 100644
--- a/addons/stock_landed_costs/models/stock_landed_cost.py
+++ b/addons/stock_landed_costs/models/stock_landed_cost.py
@@ -21,7 +21,7 @@ class LandedCost(models.Model):
_inherit = 'mail.thread'
name = fields.Char(
- 'Name', default=lambda self: self.env['ir.sequence'].next_by_code('stock.landed.cost'),
+ 'Name', default=lambda self: _('New'),
copy=False, readonly=True, track_visibility='always')
date = fields.Date(
'Date', default=fields.Date.context_today,
@@ -57,6 +57,12 @@ class LandedCost(models.Model):
def _compute_total_amount(self):
self.amount_total = sum(line.price_unit for line in self.cost_lines)
+ @api.model
+ def create(self, vals):
+ if vals.get('name', _('New')) == _('New'):
+ vals['name'] = self.env['ir.sequence'].next_by_code('stock.landed.cost')
+ return super(LandedCost, self).create(vals)
+
@api.multi
def unlink(self):
self.button_cancel()
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
index 7350d598ab0..fca2e93587f 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -227,9 +227,8 @@ class Survey(models.Model):
# Calculate and return statistics for choice
if question.type in ['simple_choice', 'multiple_choice']:
- answers = {}
comments = []
- [answers.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
+ answers = OrderedDict((label.id, {'text': label.value, 'count': 0, 'answer_id': label.id}) for label in question.labels_ids)
for input_line in question.user_input_line_ids:
if input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
answers[input_line.value_suggested.id]['count'] += 1
diff --git a/addons/web/static/lib/ace/ace.odoo-custom.js b/addons/web/static/lib/ace/ace.odoo-custom.js
index d8f84294316..49af24f5074 100644
--- a/addons/web/static/lib/ace/ace.odoo-custom.js
+++ b/addons/web/static/lib/ace/ace.odoo-custom.js
@@ -1173,15 +1173,16 @@ define("ace/lib/regexp", ["require", "exports", "module"], function(e, t, n) {
if (!t.onCompositionEnd || t.$readOnly) return;
var r = h;
h = !1;
- var i = setTimeout(function() {
- i = null;
+ var i2 = setTimeout(function() {
+ i2 = null;
var e = n.value.replace(/\x01/g, "");
if (h) return;
e == r.lastValue ? w() : !r.lastValue && e && (w(), A(e))
});
k = function(n) {
- return i && clearTimeout(i), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i && t.undo(), n)
- }, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range)
+ return i2 && clearTimeout(i2), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i2 && t.undo(), n)
+ }, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range),
+ i.isChrome && i.isChrome >= 53 && O()
},
I = o.delayedCall(j, 50);
r.addListener(n, "compositionstart", B), i.isGecko ? r.addListener(n, "text", function() {
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 4cf10b8813a..c4d3f591545 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -62,7 +62,6 @@ var ActionManager = Widget.extend({
// dialog (i.e. coming from an action with target='new')
this.currentDialogController = null;
},
-
/**
* @override
*/
@@ -140,7 +139,7 @@ var ActionManager = Widget.extend({
* loaded and appended to the DOM ; rejected if the action can't be
* executed (e.g. if doAction has been called to execute another action
* before this one was complete).
- */
+ */
doAction: function (action, options) {
var self = this;
options = _.defaults({}, options, {
@@ -177,6 +176,11 @@ var ActionManager = Widget.extend({
self._preprocessAction(action, options);
return self._handleAction(action, options).then(function () {
+ // now that the action has been executed, force its 'pushState'
+ // flag to 'true', as we don't want to prevent its controller
+ // from pushing its state if it changes in the future
+ action.pushState = true;
+
return action;
});
});
@@ -354,11 +358,6 @@ var ActionManager = Widget.extend({
// toggle the fullscreen mode for actions in target='fullscreen'
self._toggleFullscreen();
- // now that the action has been executed, force its 'pushState'
- // flag to 'true', as we don't want to prevent its controller
- // from pushing its state if it changes in the future
- action.pushState = true;
-
return action;
})
.fail(function () {
@@ -451,6 +450,7 @@ var ActionManager = Widget.extend({
}
var controllerID = _.uniqueId('controller_');
+ options.controllerID = controllerID;
var controller = {
actionID: action.jsID,
jsID: controllerID,
@@ -903,13 +903,13 @@ var ActionManager = Widget.extend({
}
},
/**
- * Intercepts and triggers a redirection on a link
- *
- * @private
- * @param {OdooEvent} ev
- * @param {integer} ev.data.res_id
- * @param {string} ev.data.res_model
- */
+ * Intercepts and triggers a redirection on a link.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ * @param {integer} ev.data.res_id
+ * @param {string} ev.data.res_model
+ */
_onRedirect: function (ev) {
this.do_action({
type:'ir.actions.act_window',
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index dfb512900e8..4e636de6355 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -224,6 +224,31 @@ ActionManager.include({
return action.controllers[viewType];
},
/**
+ * Destroys the controllers and search view of a given action of type
+ * 'ir.actions.act_window'.
+ *
+ * @private
+ * @param {Object} action
+ */
+ _destroyWindowAction: function (action) {
+ var self = this;
+ _.each(action.controllers, function (controllerDef) {
+ controllerDef.then(function (controller) {
+ delete self.controllers[controller.jsID];
+ if (controller.widget) {
+ controller.widget.destroy();
+ }
+ });
+ // reject the deferred if it is not yet resolved, so that the
+ // controller is correctly destroyed as soon as it is ready, and
+ // its reference is removed
+ controllerDef.reject();
+ });
+ if (action.searchView) {
+ action.searchView.destroy();
+ }
+ },
+ /**
* Executes actions of type 'ir.actions.act_window'.
*
* @private
@@ -245,7 +270,7 @@ ActionManager.include({
}
action.flags = this._generateActionFlags(action);
- return this._loadViews(action).then(function (fieldsViews) {
+ return this.dp.add(this._loadViews(action)).then(function (fieldsViews) {
var views = self._generateActionViews(action, fieldsViews);
action._views = action.views; // save the initial attribute
action.views = views;
@@ -291,7 +316,7 @@ ActionManager.include({
if (mainView) {
defs.push(self._createViewController(action, mainView.type, {}, {lazy: true}));
}
- return $.when.apply($, defs);
+ return self.dp.add($.when.apply($, defs));
}).then(function (controller, lazyLoadedController) {
action.controllerID = controller.jsID;
return self._executeAction(action, options).done(function () {
@@ -302,7 +327,7 @@ ActionManager.include({
}, {clear: false});
}
});
- });
+ }).fail(self._destroyWindowAction.bind(self, action));
});
},
/**
@@ -490,25 +515,10 @@ ActionManager.include({
* @private
*/
_removeAction: function (actionID) {
- var self = this;
var action = this.actions[actionID];
if (action.type === 'ir.actions.act_window') {
delete this.actions[action.jsID];
- _.each(action.controllers, function (controllerDef) {
- controllerDef.then(function (controller) {
- delete self.controllers[controller.jsID];
- if (controller.widget) {
- controller.widget.destroy();
- }
- });
- // reject the deferred if it is not yet resolved, so that the
- // controller is correctly destroyed as soon as it is ready, and
- // its reference is removed
- controllerDef.reject();
- });
- if (action.searchView) {
- action.searchView.destroy();
- }
+ this._destroyWindowAction(action);
} else {
this._super.apply(this, arguments);
}
diff --git a/addons/web/static/src/js/chrome/control_panel.js b/addons/web/static/src/js/chrome/control_panel.js
index 03b9e756bb7..558dde933e9 100644
--- a/addons/web/static/src/js/chrome/control_panel.js
+++ b/addons/web/static/src/js/chrome/control_panel.js
@@ -252,9 +252,6 @@ var ControlPanel = Widget.extend({
*/
_update_search_view: function (searchview, isHidden, groupable) {
if (searchview) {
- // Set the $buttons div (in the DOM) of the searchview as the $buttons
- // have been appended to a jQuery node not in the DOM at SearchView initialization
- searchview.$buttons = this.nodes.$searchview_buttons;
searchview.toggle_visibility(!isHidden);
if (groupable !== undefined){
searchview.groupby_menu.do_toggle(groupable);
diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js
index 1e5e2922186..1e253ba54f7 100644
--- a/addons/web/static/src/js/chrome/menu.js
+++ b/addons/web/static/src/js/chrome/menu.js
@@ -147,15 +147,22 @@ var Menu = Widget.extend({
});
},
/**
- * Call open_menu with the first menu_item matching an action_id
+ * Call open_menu on a menu_item that matches the action_id
+ *
+ * If `menuID` is a match on this action, open this menu_item.
+ * Otherwise open the first menu_item that matches the action_id.
*
* @param {Number} id the action_id to match
+ * @param {Number} [menuID] a menu ID that may match with provided action
*/
- open_action: function (id) {
+ open_action: function (id, menuID) {
var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
- var menu_id = $menu.data('menu');
- if (menu_id) {
- this.open_menu(menu_id);
+ if (!(menuID && $menu.filter("[data-menu='" + menuID + "']").length)) {
+ // menuID doesn't match action, so pick first menu_item
+ menuID = $menu.data('menu');
+ }
+ if (menuID) {
+ this.open_menu(menuID);
}
},
/**
diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js
index 3f832a6a49f..e0ce1f2c9f2 100644
--- a/addons/web/static/src/js/chrome/web_client.js
+++ b/addons/web/static/src/js/chrome/web_client.js
@@ -136,7 +136,7 @@ return AbstractWebClient.extend({
self.action_manager.loadState(state, !!self._current_state).then(function () {
var action = self.action_manager.getCurrentAction();
if (action) {
- self.menu.open_action(action.id);
+ self.menu.open_action(action.id, state.menu_id);
}
});
}
@@ -157,7 +157,7 @@ return AbstractWebClient.extend({
var completed = $.Deferred();
self.action_manager.doAction(result, {
clear_breadcrumbs: true,
- action_menu_id: self.menu.current_menu,
+ action_menu_id: options.id,
}).fail(function() {
self.menu.open_menu(options.previous_menu_id);
}).always(function() {
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 3b55efbc826..3a5a1537387 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -2062,8 +2062,9 @@ var FieldToggleBoolean = AbstractField.extend({
* @private
*/
_render: function () {
- var className = this.value ? 'o_toggle_button_success' : 'text-muted';
- this.$('i').addClass('fa fa-circle ' + className);
+ this.$('i')
+ .toggleClass('o_toggle_button_success', !!this.value)
+ .toggleClass('text-muted', !this.value);
var title = this.value ? this.attrs.options.active : this.attrs.options.inactive;
this.$el.attr('title', title);
},
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index ecd7dd688e6..8f5ffd65fac 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -878,10 +878,15 @@ var FieldX2Many = AbstractField.extend({
this.pager = new Pager(this, this.value.count, this.value.offset + 1, this.value.limit, {
single_page_hidden: true,
withAccessKey: false,
+ validate: function () {
+ var isList = self.view.arch.tag === 'tree';
+ // TODO: we should have some common method in the basic renderer...
+ return isList ? self.renderer.unselectRow() : $.when();
+ },
});
this.pager.on('pager_changed', this, function (new_state) {
- this.trigger_up('load', {
- id: this.value.id,
+ self.trigger_up('load', {
+ id: self.value.id,
limit: new_state.limit,
offset: new_state.current_min - 1,
on_success: function (value) {
@@ -930,6 +935,7 @@ var FieldX2Many = AbstractField.extend({
* did not agree to discard.
*/
_saveLine: function (recordID) {
+ var self = this;
var def = $.Deferred();
var fieldNames = this.renderer.canBeSaved(recordID);
if (fieldNames.length) {
@@ -941,7 +947,9 @@ var FieldX2Many = AbstractField.extend({
} else {
this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def));
}
- return def;
+ return def.then(function () {
+ self.pager.updateState({ size: self.value.count });
+ });
},
/**
* Parses the 'columnInvisibleFields' attribute to search for the domains
@@ -1178,7 +1186,20 @@ var FieldOne2Many = FieldX2Many.extend({
return this._super.apply(this, arguments).then(function () {
if (ev && ev.target === self && ev.data.changes && self.view.arch.tag === 'tree') {
if (ev.data.changes[self.name].operation === 'CREATE') {
- var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
+ var index = 0;
+ if (self.editable !== 'top') {
+ index = self.value.data.length - 1;
+ // we consider that a new record, created in the bottom,
+ // does not count as a record worth mentioning in the
+ // pager, at least not until the line has been saved.
+ // We prevent the pager from increasing its size, which
+ // means that the pager is not displayed when we just
+ // reach the limit. For example, if limit is 3, if we
+ // have 3 records, and we click on add, we will see the
+ // 4 records on the same page, but we do not want a
+ // pager.
+ self.pager.updateState({ size: self.value.count - 1});
+ }
var newID = self.value.data[index].id;
self.renderer.editRecord(newID);
}
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index ee39fd95596..4fff15d5663 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -330,7 +330,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
if (this.searchView) {
_.extend(cpContent, {
$searchview: this.searchView.$el,
- $searchview_buttons: this.searchView.$buttons.contents(),
+ $searchview_buttons: this.searchView.$buttons,
});
}
this.update_control_panel({
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 3013cb32f85..4cf6f9fa27b 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -370,6 +370,11 @@ var BasicModel = AbstractModel.extend({
elem._changes = null;
elem._isDirty = false;
}
+ elem.offset = 0;
+ if (elem.tempLimitIncrement) {
+ elem.limit -= elem.tempLimitIncrement;
+ delete elem.tempLimitIncrement;
+ }
});
},
/**
@@ -820,6 +825,7 @@ var BasicModel = AbstractModel.extend({
this.discardChanges(id, {rollback: false});
}
} else if (element._changes) {
+ delete element.tempLimitIncrement;
_.each(element._changes, function (change) {
delete change.isNew;
});
@@ -1657,6 +1663,12 @@ var BasicModel = AbstractModel.extend({
// handle multiple add: command[2] may be a dict of values (1
// record added) or an array of dict of values
var data = _.isArray(command.ids) ? command.ids : [command.ids];
+
+ // Ensure the local data repository (list) boundaries can handle incoming records (data)
+ if (data.length + list.res_ids.length > list.limit) {
+ list.limit = data.length + list.res_ids.length;
+ }
+
var list_records = {};
_.each(data, function (d) {
rec = self._makeDataPoint({
@@ -1701,6 +1713,10 @@ var BasicModel = AbstractModel.extend({
case 'CREATE':
var options = {position: command.position};
def = this._addX2ManyDefaultRecord(list, options).then(function (id) {
+ if (command.position === 'bottom' && list.orderedResIDs.length >= list.limit) {
+ list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1;
+ list.limit += 1;
+ }
// FIXME: hack for lunch widget, which does useless default_get and onchange
if (command.data) {
return self._applyChange(id, command.data);
@@ -3856,7 +3872,8 @@ var BasicModel = AbstractModel.extend({
// generate the current count and res_ids list by applying the changes
var currentCount = list.count;
var currentResIDs = list.res_ids;
- var upperBound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
+ var effectiveLimit = (list.limit || 0) - (list.tempLimitIncrement || 0);
+ var upperBound = effectiveLimit ? Math.min(list.offset + effectiveLimit, currentCount) : currentCount;
var fieldNames = list.getFieldNames();
for (var i = list.offset; i < upperBound; i++) {
var resId = currentResIDs[i];
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index b28d25c5b90..f6430d4a251 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -613,6 +613,10 @@ var FormRenderer = BasicRenderer.extend({
var $result = $('<div/>', {class: 'o_group'});
var colSize = Math.max(1, Math.round(12 / (parseInt(node.attrs.col, 10) || 2)));
+ if (node.attrs.string) {
+ var $sep = $('<div/>', {class: 'o_horizontal_separator'}).text(node.attrs.string);
+ $result.append($sep);
+ }
$result.append(_.map(node.children, function (child) {
if (child.tag === 'newline') {
return $('<br/>');
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index 1b74068d44d..d4ae709008c 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -252,6 +252,7 @@ var SelectCreateDialog = ViewDialog.extend({
}
},
selection_changed: function (event) {
+ event.stopPropagation();
this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length);
},
search: function (event) {
diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less
index d28221ede63..b9eec962eaf 100644
--- a/addons/web/static/src/less/kanban_view.less
+++ b/addons/web/static/src/less/kanban_view.less
@@ -229,7 +229,7 @@
height: 20px;
}
.oe_kanban_bottom_left {
- .o-flex(1, 0, auto);
+ .o-flex(1, 1, auto);
> * {
margin-right: 6px;
@@ -242,7 +242,7 @@
}
}
.oe_kanban_bottom_right{
- .o-flex(0, 0, auto);
+ .o-flex(0, 1, auto);
.oe_kanban_avatar {
width: 20px;
diff --git a/addons/web/static/src/less/kanban_view_mobile.less b/addons/web/static/src/less/kanban_view_mobile.less
index 72ba087e0c2..d09b6968925 100644
--- a/addons/web/static/src/less/kanban_view_mobile.less
+++ b/addons/web/static/src/less/kanban_view_mobile.less
@@ -29,7 +29,8 @@
}
}
- &.o_kanban_small_column .o_kanban_group:not(.o_column_folded) {
+ // [class] to get same specificity as elsewhere (kanban_view.less)
+ &[class] .o_kanban_group:not(.o_column_folded) {
.o-position-absolute(@top: @o-kanban-mobile-tabs-height, @left: 0, @bottom: 0);
overflow-x: scroll;
width: 100%;
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 748addc4b87..cc046a2bb89 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -209,7 +209,7 @@
<t t-name="WebClient.DebugManager">
<li class="o_debug_manager">
- <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown">
+ <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-bug"/>
</a>
<ul class="dropdown-menu o_debug_dropdown" role="menu"/>
@@ -325,12 +325,12 @@
<t t-name="Sidebar">
<t t-foreach="widget.sections" t-as="section">
<div class="btn-group o_dropdown">
- <button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
+ <button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/>
<t t-esc="section.label"/> <span class="caret"/>
</button>
<t t-if="section.name == 'buttons'" t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname">
- <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank">
+ <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'" target="_blank">
<t t-raw="item.label"/>
</a>
</t>
@@ -352,7 +352,7 @@
<t t-raw="item.write_uid[1]"/> <t t-esc="item.write_date_string"/>
</t>
</t>
- <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url">
+ <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'">
<t t-raw="item.label"/>
<span t-if="section.name == 'files' and widget.options.editable and !item.callback" class="fa fa-trash-o o_sidebar_delete_attachment" t-att-data-id="item.id" title="Delete this attachment"/>
</a>
@@ -469,7 +469,7 @@
</form>
<t t-name="GraphView.buttons">
<div class="btn-group btn-group-sm" role="group">
- <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">
+ <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Measures <span class="caret"/>
</button>
<ul class="dropdown-menu o_graph_measures_list">
@@ -498,7 +498,7 @@
</div>
<t t-name="PivotView.buttons">
<div class="btn-group btn-group-sm" role="group">
- <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown">
+ <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
Measures <span class="caret"/>
</button>
<ul class="dropdown-menu o_pivot_measures_list">
@@ -852,7 +852,7 @@
<t t-call="FieldStatus.content.button"/>
</li>
</ul>
- <button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown">More <span class="caret"/></button>
+ <button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">More <span class="caret"/></button>
</t>
<t t-foreach="selection_unfolded.reverse()" t-as="i">
<t t-call="FieldStatus.content.button"/>
@@ -1011,7 +1011,7 @@
<ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul>
</t>
<div t-name="SearchView.FilterMenu" class="btn-group o_dropdown">
- <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
+ <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-filter"/> Filters <span class="caret"/>
</button>
<ul class="dropdown-menu o_filters_menu" role="menu">
@@ -1123,7 +1123,7 @@
</select>
</t>
<div t-name="SearchView.GroupByMenu" class="btn-group hidden-xs o_dropdown">
- <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
+ <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-bars"/> Group By <span class="caret"/>
</button>
<ul class="dropdown-menu o_group_by_menu" role="menu">
@@ -1144,7 +1144,7 @@
</li>
</t>
<div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown">
- <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown">
+ <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-star"/> Favorites <span class="caret"/>
</button>
<ul class="dropdown-menu o_favorites_menu" role="menu">
@@ -1273,8 +1273,8 @@
<span class="o_stat_text"><t t-esc="text"/></span>
</t>
<t t-name="toggle_button">
- <button type="button" class="o_icon_button" t-att-title="widget.string">
- <i t-attf-class="fa #{widget.icon}" t-att-title="widget.string"/>
+ <button type="button" class="o_icon_button" t-att-title="widget.string" t-att-aria-label="widget.string">
+ <i class="fa fa-circle" t-att-title="widget.string"/>
</button>
</t>
@@ -1288,8 +1288,8 @@
<t t-set="att_prev" t-value="{'accesskey': 'p'}" />
<t t-set="att_next" t-value="{'accesskey': 'n'}" />
</t>
- <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev"/>
- <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next"/>
+ <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev" aria-label="Previous"/>
+ <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next" aria-label="Next"/>
</span>
</div>
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index 4d46b9167d1..c805da4e67c 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -277,6 +277,102 @@ QUnit.module('ActionManager', {
testUtils.unpatch(Widget);
});
+ QUnit.test('no memory leaks when executing an action while loading views', function (assert) {
+ assert.expect(1);
+
+ var def;
+ var delta = 0;
+ testUtils.patch(Widget, {
+ init: function () {
+ delta += 1;
+ this._super.apply(this, arguments);
+ },
+ destroy: function () {
+ delta -= 1;
+ this._super.apply(this, arguments);
+ },
+ });
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'load_views') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // execute action 4 to know the number of widgets it instantiates
+ actionManager.doAction(4);
+ var n = delta;
+
+ // execute a first action (its 'load_views' RPC is blocked)
+ def = $.Deferred();
+ actionManager.doAction(3, {clear_breadcrumbs: true});
+
+ // execute another action meanwhile (and unlock the RPC)
+ actionManager.doAction(4, {clear_breadcrumbs: true});
+ def.resolve();
+
+ assert.strictEqual(n, delta,
+ "all widgets of action 3 should have been destroyed");
+
+ actionManager.destroy();
+ testUtils.unpatch(Widget);
+ });
+
+ QUnit.test('no memory leaks when executing an action while loading data of default view', function (assert) {
+ assert.expect(1);
+
+ var def;
+ var delta = 0;
+ testUtils.patch(Widget, {
+ init: function () {
+ delta += 1;
+ this._super.apply(this, arguments);
+ },
+ destroy: function () {
+ delta -= 1;
+ this._super.apply(this, arguments);
+ },
+ });
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route) {
+ var result = this._super.apply(this, arguments);
+ if (route === '/web/dataset/search_read') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // execute action 4 to know the number of widgets it instantiates
+ actionManager.doAction(4);
+ var n = delta;
+
+ // execute a first action (its 'search_read' RPC is blocked)
+ def = $.Deferred();
+ actionManager.doAction(3, {clear_breadcrumbs: true});
+
+ // execute another action meanwhile (and unlock the RPC)
+ actionManager.doAction(4, {clear_breadcrumbs: true});
+ def.resolve();
+
+ assert.strictEqual(n, delta,
+ "all widgets of action 3 should have been destroyed");
+
+ actionManager.destroy();
+ testUtils.unpatch(Widget);
+ });
+
QUnit.test('action with "no_breadcrumbs" set to true', function (assert) {
assert.expect(2);
@@ -831,6 +927,56 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('should not push a loaded state of a client action', function (assert) {
+ assert.expect(4);
+
+ var ClientAction = Widget.extend({
+ init: function (parent, action, options) {
+ this._super.apply(this, arguments);
+ this.controllerID = options.controllerID;
+ },
+ start: function () {
+ var self = this;
+ var $button = $('<button>').text('Click Me!');
+ $button.on('click', function () {
+ self.trigger_up('push_state', {
+ controllerID: self.controllerID,
+ state: {someValue: 'X'},
+ });
+ });
+ this.$el.append($button);
+ return this._super.apply(this, arguments);
+ },
+ });
+ core.action_registry.add('ClientAction', ClientAction);
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ intercepts: {
+ push_state: function (ev) {
+ assert.step('push_state');
+ assert.deepEqual(ev.data.state, {
+ action: 9,
+ someValue: 'X',
+ title: 'A Client Action',
+ });
+ },
+ },
+ });
+ actionManager.loadState({action: 9});
+
+ assert.verifySteps([], "should not push the loaded state");
+
+ actionManager.$('button').click();
+
+ assert.verifySteps(['push_state'],
+ "should push the state of it changes afterwards");
+
+ actionManager.destroy();
+ });
+
QUnit.test('change a param of an ir.actions.client in the url', function (assert) {
assert.expect(7);
@@ -1253,6 +1399,101 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('execute a new action while loading views', function (assert) {
+ assert.expect(10);
+
+ var def;
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ assert.step(args.method || route);
+ if (args.method === 'load_views') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // execute a first action (its 'load_views' RPC is blocked)
+ def = $.Deferred();
+ actionManager.doAction(3);
+
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "should not display the list view of action 3");
+
+ // execute another action meanwhile (and unlock the RPC)
+ actionManager.doAction(4);
+ def.resolve();
+
+ assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
+ "should display the kanban view of action 4");
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "should not display the list view of action 3");
+ assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
+ "there should be one controller in the breadcrumbs");
+
+ assert.verifySteps([
+ '/web/action/load', // action 3
+ 'load_views', // action 3
+ '/web/action/load', // action 4
+ 'load_views', // action 4
+ '/web/dataset/search_read', // search read action 4
+ ]);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('execute a new action while loading data of default view', function (assert) {
+ assert.expect(11);
+
+ var def;
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ assert.step(args.method || route);
+ if (route === '/web/dataset/search_read') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // execute a first action (its 'search_read' RPC is blocked)
+ def = $.Deferred();
+ actionManager.doAction(3);
+
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "should not display the list view of action 3");
+
+ // execute another action meanwhile (and unlock the RPC)
+ actionManager.doAction(4);
+ def.resolve();
+
+ assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
+ "should display the kanban view of action 4");
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "should not display the list view of action 3");
+ assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
+ "there should be one controller in the breadcrumbs");
+
+ assert.verifySteps([
+ '/web/action/load', // action 3
+ 'load_views', // action 3
+ '/web/dataset/search_read', // search read action 3
+ '/web/action/load', // action 4
+ 'load_views', // action 4
+ '/web/dataset/search_read', // search read action 4
+ ]);
+
+ actionManager.destroy();
+ });
+
QUnit.module('Client Actions');
QUnit.test('can execute client actions from tag name', function (assert) {
@@ -2466,6 +2707,31 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test("search menus are still available when switching between actions", function (assert) {
+ assert.expect(3);
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+
+ actionManager.doAction(1);
+ assert.strictEqual($('.o_search_options .o_dropdown:visible .o_filters_menu').length, 1,
+ "the search options should be available");
+
+ actionManager.doAction(3);
+ assert.strictEqual($('.o_search_options .o_dropdown:visible .o_filters_menu').length, 1,
+ "the search options should be available");
+
+ // go back using the breadcrumbs
+ $('.o_control_panel .breadcrumb a:first').click();
+ assert.strictEqual($('.o_search_options .o_dropdown:visible .o_filters_menu').length, 1,
+ "the search options should be available");
+
+ actionManager.destroy();
+ });
+
QUnit.module('Actions in target="new"');
QUnit.test('can execute act_window actions in target="new"', function (assert) {
@@ -2647,7 +2913,6 @@ QUnit.module('ActionManager', {
QUnit.test('doAction resolved with an action', function (assert) {
assert.expect(4);
- var done = assert.async();
this.actions.push({
id: 21,
@@ -2670,7 +2935,6 @@ QUnit.module('ActionManager', {
assert.strictEqual(action.type, 'ir.actions.act_window_close',
"should be resolved with correct action type");
actionManager.destroy();
- done();
});
});
});
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 11f39bc8147..cc041c08d7b 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -387,7 +387,6 @@ QUnit.module('basic_fields', {
'<field name="bar" widget="toggle_button" ' +
'options="{&quot;active&quot;: &quot;Reported in last payslips&quot;, &quot;inactive&quot;: &quot;To Report in Payslip&quot;}"/>' +
'</tree>',
- res_id: 2,
});
assert.strictEqual(list.$('button i.fa.fa-circle.o_toggle_button_success').length, 4,
@@ -411,6 +410,81 @@ QUnit.module('basic_fields', {
list.destroy();
});
+ QUnit.test('toggle_button in form view (edit mode)', function (assert) {
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar" widget="toggle_button" ' +
+ 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.step('write');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 2,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
+ 1, "should be green");
+
+ // click on the button to toggle the value
+ form.$('.o_field_widget[name=bar]').click();
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
+ 1, "should be gray");
+ assert.verifySteps([]);
+
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
+ 1, "should still be gray");
+ assert.verifySteps(['write']);
+
+ form.destroy();
+ });
+
+ QUnit.test('toggle_button in form view (readonly mode)', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar" widget="toggle_button" ' +
+ 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.step('write');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
+ 1, "should be green");
+
+ // click on the button to toggle the value
+ form.$('.o_field_widget[name=bar]').click();
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
+ 1, "should be gray");
+ assert.verifySteps(['write']);
+
+ form.destroy();
+ });
QUnit.module('FieldFloat');
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 7b1ad052196..d3916ef9a50 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -2078,7 +2078,7 @@ QUnit.module('relational_fields', {
data: this.data,
arch: '<form>' +
'<field name="p">' +
- '<tree editable="bttom">' +
+ '<tree editable="bottom">' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
@@ -2173,6 +2173,92 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2many list add *many* records, remove, re-add', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.timmy.domain = [['color', '=', 2]];
+ this.data.partner.fields.timmy.onChange = true;
+ this.data.partner_type.fields.product_ids = {string: "Product", type: "many2many", relation: 'product'};
+
+ for (var i=0; i<50; i++) {
+ var new_record_partner_type = {id: 100+i, display_name: "batch" + i, color: 2};
+ this.data.partner_type.records.push(new_record_partner_type);
+ }
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '<field name="product_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/><field name="color"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // First round: add 51 records in batch
+ form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ var $modal = $('.modal-dialog.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ $modal.find('thead input[type=checkbox]').click(); //select all the records we created in batch + 'gold'
+
+ $modal.find('.btn.btn-sm.btn-primary.o_select_button').click();
+
+ var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)');
+ assert.equal(m2m_records.length, 51,
+ 'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold'
+
+ form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_save').click();
+
+ // Secound round: remove one record
+ form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click();
+ var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_remove');
+
+ trash_buttons.first().click();
+
+ var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
+ assert.equal(pager_limit.text(), '50',
+ 'We should have 50 records in the m2m field');
+
+ // Third round: re-add 1 records
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ $modal = $('.modal-dialog.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ $modal.find('thead input[type=checkbox]').click();
+
+ $modal.find('.btn.btn-sm.btn-primary.o_select_button').click();
+
+ pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
+ assert.equal(pager_limit.text(), '51',
+ 'We should have 51 records in the m2m field');
+
+ form.destroy();
+ });
+
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -2784,7 +2870,7 @@ QUnit.module('relational_fields', {
});
QUnit.test('onchange followed by edition on the second page', function (assert) {
- assert.expect(20);
+ assert.expect(12);
var ids = [];
for (var i=1; i<85; i++) {
@@ -2854,10 +2940,32 @@ QUnit.module('relational_fields', {
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line");
form.destroy();
+ });
+
+ QUnit.test('onchange followed by edition on the second page (part 2)', function (assert) {
+ assert.expect(8);
+
+ var ids = [];
+ for (var i=1; i<85; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.turtle.records.push({
+ id: id,
+ turtle_int: id/3|0,
+ turtle_foo: "#" + i,
+ });
+ }
+ ids.splice(41, 0, 1, 2, 3);
+ this.data.partner.records[0].turtles = ids;
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ },
+ };
// bottom order
- form = createView({
+ var form = createView({
View: FormView,
model: 'partner',
data: this.data,
@@ -2889,16 +2997,16 @@ QUnit.module('relational_fields', {
form.$('.o_field_x2many_list_row_add a').click();
- assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
+ assert.strictEqual(form.$('.o_data_row').length, 41, "should display 41 records and the create line");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line");
- assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position");
+ assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position");
form.$('.o_data_row input:first').val('value 3').trigger('input');
form.$('.o_field_x2many_list_row_add a').click();
- assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
+ assert.strictEqual(form.$('.o_data_row').length, 42, "should display 42 records and the create line");
assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line");
- assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position");
+ assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position");
form.destroy();
});
@@ -3979,6 +4087,37 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editable one2many list, pager is updated', function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
+ this.data.partner.records[0].turtles = [1,2,3,4];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record, then click in form view to confirm it
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$el.click();
+
+ assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 5',
+ "pager should display the correct total");
+ form.destroy();
+ });
+
+
QUnit.test('one2many list (non editable): edition', function (assert) {
assert.expect(12);
@@ -4287,6 +4426,185 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editable one2many list, adding line when only one page', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles=[1,2,3];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record, to reach the page size limit
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ // the record currently being added should not count in the pager
+ assert.ok(!form.$('.o_cp_pager').is(':visible'),
+ "pager should not be visible");
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, adding line, then discarding', function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
+ this.data.partner.records[0].turtles = [1,2,3,4];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record, then discard
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$buttons.find('.o_form_button_cancel').click();
+
+ // confirm the discard operation
+ $('.modal .modal-footer .btn-primary').click(); // click on confirm
+
+ assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
+ "pager should still be visible");
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, required field and pager', function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.partner.records[0].turtles = [1,2,3,4];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a (empty) record
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ // go on next page. The new record is not valid and should be discarded
+ form.$('.o_pager_next').click();
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ "should have 1 data rows");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, required field, pager and confirm discard', function (assert) {
+ assert.expect(3);
+
+ this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'});
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.partner.records[0].turtles = [1,2,3,4];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record with a dirty state, but not valid
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('input[name="turtle_int"]').val(4321).trigger('input');
+
+ // go to next page. The new record is not valid, but dirty. we should
+ // see a confirm dialog
+ form.$('.o_pager_next').click();
+
+ assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
+ "pager should still display the correct total");
+
+ // click on cancel
+ $('.modal .modal-footer .btn-default').click(); // click on cancel
+
+ assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4',
+ "pager should again display the correct total");
+ assert.strictEqual(form.$('.o_field_one2many input.o_field_invalid').length, 1,
+ "there should be an invalid input in the one2many");
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, adding, discarding, and pager', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a 4 records record (to make the pager appear)
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ // go on next page
+ form.$('.o_pager_next').click();
+
+ // discard
+ form.$buttons.find('.o_form_button_cancel').click();
+ $('.modal .modal-footer .btn-primary').click();
+
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ "should have 1 data row");
+ assert.ok(!form.$('.o_cp_pager').is(':visible'),
+ "pager should not be visible");
+
+ form.destroy();
+ });
+
QUnit.test('unselecting a line with missing required data', function (assert) {
assert.expect(5);
@@ -7602,6 +7920,42 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editing tabbed one2many (editable=bottom), again...', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [];
+ for (var i = 0; i < 9; i++) {
+ var id = 100 + i;
+ this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)});
+ this.data.partner.records[0].turtles.push(id);
+ }
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input');
+ form.$('.o_x2m_control_panel .o_pager_next').click();
+ form.$('.o_x2m_control_panel .o_pager_next').click();
+
+ assert.strictEqual(form.$('tr.o_data_row').length, 3,
+ "should have 3 data rows on the current page");
+ form.destroy();
+ });
+
QUnit.test('editing tabbed one2many (editable=top)', function (assert) {
assert.expect(15);
@@ -8217,6 +8571,49 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('add a new line after limit is reached should behave nicely', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1,2,3];
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 1, {turtle_foo: "yop"}],
+ [1, 2, {turtle_foo: "blip"}],
+ [1, 3, {turtle_foo: "kawa"}],
+ [0, obj.turtles[3][2], {turtle_foo: "abc"}],
+ ];
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="3" editable="bottom">' +
+ '<field name="turtle_foo" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('.o_data_row').length, 4, 'should have 4 data rows');
+ form.$('.o_input[name="turtle_foo"]').val('a').trigger('input');
+ assert.strictEqual(form.$('.o_data_row').length, 4,
+ 'should still have 4 data rows (the limit is increased to 4)');
+
+ form.destroy();
+ });
+
QUnit.test('onchange in a one2Many, but not in main record, with non inline view', function (assert) {
assert.expect(6);
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 2a64a2f30da..65b8ab5da38 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -5198,6 +5198,46 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('outer and inner groups string attribute', function (assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group string="parent group" class="parent_group">' +
+ '<group string="child group 1" class="group_1">' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '<group string="child group 2" class="group_2">' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $parentGroup = form.$('.parent_group');
+ var $group1 = form.$('.group_1');
+ var $group2 = form.$('.group_2');
+
+ assert.strictEqual(form.$('table.o_inner_group').length, 2,
+ "should contain two inner groups");
+ assert.strictEqual($group1.find('.o_horizontal_separator').length, 1,
+ "inner group should contain one string separator");
+ assert.strictEqual($group1.find('.o_horizontal_separator:contains(child group 1)').length, 1,
+ "first inner group should contain 'child group 1' string");
+ assert.strictEqual($group2.find('.o_horizontal_separator:contains(child group 2)').length, 1,
+ "second inner group should contain 'child group 2' string");
+ assert.strictEqual($parentGroup.find('> div.o_horizontal_separator:contains(parent group)').length, 1,
+ "outer group should contain 'parent group' string");
+
+ form.destroy();
+ });
+
QUnit.test('form group with newline tag inside', function (assert) {
assert.expect(6);
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 6ec4f50ab4b..39484795f5a 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -5,6 +5,9 @@ var dialogs = require('web.view_dialogs');
var ListController = require('web.ListController');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
+var FormView = require('web.FormView');
+
+var createView = testUtils.createView;
QUnit.module('Views', {
beforeEach: function () {
@@ -14,6 +17,7 @@ QUnit.module('Views', {
display_name: { string: "Displayed name", type: "char" },
foo: {string: "Foo", type: 'char'},
bar: {string: "Bar", type: "boolean"},
+ instrument: {string: 'Instruments', type: 'many2one', relation: 'instrument'},
},
records: [
{id: 1, foo: 'blip', display_name: 'blipblip', bar: true},
@@ -21,8 +25,34 @@ QUnit.module('Views', {
{id: 3, foo: 'piou piou', display_name: "Jack O'Neill", bar: true},
],
},
+ instrument: {
+ fields: {
+ name: {string: "name", type: "char"},
+ badassery: {string: 'level', type: 'many2many', relation: 'badassery', domain: [['level', '=', 'Awsome']]},
+ },
+ },
+
+ badassery: {
+ fields: {
+ level: {string: 'level', type: "char"},
+ },
+ records: [
+ {id: 1, level: 'Awsome'},
+ ],
+ },
+
+ product: {
+ fields : {
+ name: {string: "name", type: "char" },
+ partner : {string: 'Doors', type: 'one2many', relation: 'partner'},
+ },
+ records: [
+ {id: 1, name: 'The end'},
+ ],
+ },
};
},
+
}, function () {
QUnit.module('view_dialogs');
@@ -242,6 +272,98 @@ QUnit.module('Views', {
parent.destroy();
});
+ QUnit.test('SelectCreateDialog cascade x2many in create mode', function (assert) {
+ assert.expect(5);
+
+ var form = createView({
+ View: FormView,
+ model: 'product',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="name"/>' +
+ '<field name="partner" widget="one2many_list" >' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '<field name="instrument"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form>' +
+ '<field name="name"/>' +
+ '<field name="instrument" widget="one2many_list" mode="tree"/>' +
+ '</form>',
+
+ 'instrument,false,form': '<form>'+
+ '<field name="name"/>'+
+ '<field name="badassery">' +
+ '<tree>'+
+ '<field name="level"/>'+
+ '</tree>' +
+ '</field>' +
+ '</form>',
+
+ 'badassery,false,list': '<tree>'+
+ '<field name="level"/>'+
+ '</tree>',
+
+ 'badassery,false,search': '<search>'+
+ '<field name="level"/>'+
+ '</search>',
+ },
+
+ mockRPC: function(route, args) {
+ if (route === '/web/dataset/call_kw/partner/get_formview_id') {
+ return $.when(false);
+ }
+ if (route === '/web/dataset/call_kw/instrument/get_formview_id') {
+ return $.when(false);
+ }
+ if (route === '/web/dataset/call_kw/instrument/create') {
+ assert.deepEqual(args.args, [{badassery: [[6, false, [1]]], name: false}],
+ 'The method create should have been called with the right arguments');
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_widget .o_field_many2one[name=instrument] input').click();
+ $('ul.ui-autocomplete.ui-front.ui-menu.ui-widget.ui-widget-content li.o_m2o_dropdown_option').first().click();
+
+ var $modal = $('.modal-dialog.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ $modal.find('.o_field_x2many_list_row_add a').click();
+
+ var $modals = $('.modal-dialog.modal-lg');
+
+ assert.equal($modals.length, 2,
+ 'There should be two modals');
+
+ var $second_modal = $modals.not($modal);
+ $second_modal.find('.o_list_view.table.table-condensed.table-striped.o_list_view_ungrouped .o_data_row input[type=checkbox]').click();
+
+ $second_modal.find('.o_select_button').click();
+
+ $modal = $('.modal-dialog.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ assert.equal($modal.find('.o_data_cell').text(), 'Awsome',
+ 'There should be one item in the list of the modal');
+
+ $modal.find('.btn.btn-sm.btn-primary').click();
+
+ form.destroy();
+ });
+
QUnit.test('SelectCreateDialog: save current search', function (assert) {
assert.expect(4);
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index b14c58ae938..68051f8c07c 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -147,7 +147,10 @@ class Web_Editor(http.Controller):
Attachments = request.env['ir.attachment'] # registry for the attachment table
res_model = kwargs.get('res_model', 'ir.ui.view')
- res_id = res_model != 'ir.ui.view' and kwargs.get('res_id') or None
+ if res_model != 'ir.ui.view' and kwargs.get('res_id'):
+ res_id = int(kwargs['res_id'])
+ else:
+ res_id = None
uploads = []
message = None
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 188b563f238..097f3d3b12f 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -1233,7 +1233,7 @@ registry.many2one = SnippetOption.extend({
method: 'search_read',
args: [domain, this.Model === 'res.partner' ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']],
kwargs: {
- order: 'name DESC',
+ order: [{name: 'name', asc: false}],
limit: 5,
context: weContext.get(),
},
diff --git a/addons/web_editor/static/src/less/web_editor.backend.less b/addons/web_editor/static/src/less/web_editor.backend.less
index 76da07437a5..a9d3c4e2198 100644
--- a/addons/web_editor/static/src/less/web_editor.backend.less
+++ b/addons/web_editor/static/src/less/web_editor.backend.less
@@ -22,6 +22,7 @@
margin: 0;
padding: 0;
border: 0;
+ word-wrap: break-word;
ul > li > p {
margin: 0px;
}
@@ -57,9 +58,9 @@
position: static !important;
top: 0 !important;
left: 0 !important;
- overflow: visible !important;
width: auto !important;
overflow: hidden !important;
+ transform: none !important;
}
}
diff --git a/addons/web_editor/static/src/less/web_editor.variables.less b/addons/web_editor/static/src/less/web_editor.variables.less
index 382969d96b3..7b6f9565b4c 100644
--- a/addons/web_editor/static/src/less/web_editor.variables.less
+++ b/addons/web_editor/static/src/less/web_editor.variables.less
@@ -94,6 +94,12 @@
border-radius: 0;
}
+ .has-error {
+ .form-control {
+ border-color: @state-danger-text;
+ }
+ }
+
select.form-control {
.o-appearance(none);
background: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPScxLjEnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZycgeG1 sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnIHdpZHRoPScyNCcgaGVpZ2 h0PScyNCcgdmlld0JveD0nMCAwIDI0IDI0Jz48cGF0aCBpZD0nc3ZnXzEnIGQ9J203LjQwNiw3L jgyOGw0LjU5NCw0LjU5NGw0LjU5NCwtNC41OTRsMC40MDYsMS40MDZsLTUsNC43NjZsLTUsLTQu NzY2bDAuNDA2LC0xLjQwNnonIGZpbGw9JyM4ODgnLz48L3N2Zz4=');
diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js
index ad45199ce27..09881b4ec1b 100644
--- a/addons/website/static/src/js/backend/dashboard.js
+++ b/addons/website/static/src/js/backend/dashboard.js
@@ -214,6 +214,7 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
self.handle_analytics_auth($analytics_components);
gapi.analytics.auth.on('signIn', function() {
+ delete window.onOriginError;
self.handle_analytics_auth($analytics_components);
});
diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js
index d7c29359b95..602556c03cc 100644
--- a/addons/website/static/src/js/content/snippets.animation.js
+++ b/addons/website/static/src/js/content/snippets.animation.js
@@ -877,7 +877,7 @@ registry.socialShare = Animation.extend({
var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag)
var social_network = {
'facebook': 'https://www.facebook.com/sharer/sharer.php?u=' + url,
- 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url),
+ 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ') + url,
'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + url + '&title=' + encodeURIComponent(title),
'google-plus': 'https://plus.google.com/share?url=' + url,
};
diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js
index 331edf06906..0b5af478da7 100644
--- a/addons/website/static/src/js/editor/snippets.options.js
+++ b/addons/website/static/src/js/editor/snippets.options.js
@@ -63,9 +63,10 @@ options.registry.company_data = options.Class.extend({
start: function () {
var proto = options.registry.company_data.prototype;
var def;
+ var self = this;
if (proto.__link === undefined) {
def = this._rpc({route: '/web/session/get_session_info'}).then(function (session) {
- return this._rpc({
+ return self._rpc({
model: 'res.users',
method: 'read',
args: [session.uid, ['company_id']],
diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js
index a931cc057ef..c66b340c699 100644
--- a/addons/website/static/src/js/menu/content.js
+++ b/addons/website/static/src/js/menu/content.js
@@ -157,7 +157,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
},
};
if (this.page.date_publish) {
- datepickersOptions.defaultDate = this.page.date_publish;
+ datepickersOptions.defaultDate = time.str_to_datetime(this.page.date_publish);
}
this.$('#date_publish_container').datetimepicker(datepickersOptions);
@@ -183,9 +183,15 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
var context = weContext.get();
var url = this.$('#page_url').val();
- var date_publish = this.$('#date_publish').val();
- if (date_publish !== '') {
- date_publish = time.datetime_to_str(new Date(date_publish));
+ var $date_publish = this.$("#date_publish");
+ $date_publish.closest(".form-group").removeClass('has-error');
+ var date_publish = $date_publish.val();
+ if (date_publish !== "") {
+ date_publish = this._parse_date(date_publish);
+ if (!date_publish) {
+ $date_publish.closest(".form-group").addClass('has-error');
+ return;
+ }
}
var params = {
id: this.page.id,
@@ -280,6 +286,28 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
id: m[2] | 0,
};
},
+ /**
+ * Converts a string representing the browser datetime
+ * (exemple: Albanian: '2018-Qer-22 15.12.35.')
+ * to a string representing UTC in Odoo's datetime string format
+ * (exemple: '2018-04-22 13:12:35').
+ *
+ * The time zone of the datetime string is assumed to be the one of the
+ * browser and it will be converted to UTC (standard for Odoo).
+ *
+ * @private
+ * @param {String} value A string representing a datetime.
+ * @returns {String|false} A string representing an UTC datetime if the given value is valid, false otherwise.
+ */
+ _parse_date: function (value) {
+ var datetime = moment(value, time.getLangDatetimeFormat(), true);
+ if (datetime.isValid()) {
+ return time.datetime_to_str(datetime.toDate());
+ }
+ else {
+ return false;
+ }
+ },
//--------------------------------------------------------------------------
// Handlers
diff --git a/addons/website_event/README.md b/addons/website_event/README.md
index e4691d2619b..89943120484 100644
--- a/addons/website_event/README.md
+++ b/addons/website_event/README.md
@@ -72,7 +72,7 @@ And Much More...
- Calendar of Events
- Publish related documents
- Ressources allocation
-- Automate purchases (cathering...)
+- Automate purchases (catering...)
- Multiple locations and organizers
- Mobile Interface
diff --git a/addons/website_event_sale/views/event_templates.xml b/addons/website_event_sale/views/event_templates.xml
index 3af11508112..789ea5995b2 100644
--- a/addons/website_event_sale/views/event_templates.xml
+++ b/addons/website_event_sale/views/event_templates.xml
@@ -39,7 +39,8 @@
</t>
<t t-set="price">
<t t-if="ticket.price or editable">
- <t t-if="(ticket.price-website.get_current_pricelist().currency_id.compute(ticket.price_reduce, event.company_id.sudo().currency_id)) &gt; 1">
+ <t t-if="(ticket.price-website.get_current_pricelist().currency_id.compute(ticket.price_reduce, event.company_id.sudo().currency_id)) &gt; 1
+ and website.get_current_pricelist().discount_policy == 'without_discount'">
<del class="text-danger mr4" t-field="ticket.price" t-options='{
"widget": "monetary",
"from_currency": event.company_id.sudo().currency_id,
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 5cb4f856a20..9ef132937eb 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -10,6 +10,7 @@ from odoo.addons.base.models.ir_qweb_fields import nl2br
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.website.controllers.main import QueryURL
from odoo.exceptions import ValidationError
+from odoo.addons.website.controllers.main import Website
from odoo.addons.website_form.controllers.main import WebsiteForm
_logger = logging.getLogger(__name__)
@@ -116,7 +117,19 @@ class WebsiteSaleForm(WebsiteForm):
return json.dumps({'id': order.id})
+class Website(Website):
+ @http.route()
+ def get_switchable_related_views(self, key):
+ views = super(Website, self).get_switchable_related_views(key)
+ if key == 'website_sale.product':
+ if not request.env.user.has_group('product.group_product_variant'):
+ view_product_variants = request.env.ref('website_sale.product_variants')
+ views[:] = [v for v in views if v['id'] != view_product_variants.id]
+ return views
+
+
class WebsiteSale(http.Controller):
+
def _get_compute_currency_and_context(self):
pricelist_context = dict(request.env.context)
pricelist = False
diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml
index d195702f5ad..d40143e033f 100644
--- a/addons/website_sale/data/demo.xml
+++ b/addons/website_sale/data/demo.xml
@@ -713,7 +713,6 @@ Weight: 31 grams</field>
</record>
<record model="product.pricelist" id="product.list0">
- <field name="name">USD</field>
<field name="selectable" eval="True" />
<field name="website_id" ref="website.default_website" />
<field name="sequence">3</field>
diff --git a/addons/website_sale/tests/test_website_sale_pricelist.py b/addons/website_sale/tests/test_website_sale_pricelist.py
index 754a49432d6..2995ca139cf 100644
--- a/addons/website_sale/tests/test_website_sale_pricelist.py
+++ b/addons/website_sale/tests/test_website_sale_pricelist.py
@@ -57,11 +57,11 @@ class TestWebsitePriceList(TransactionCase):
current_pl = False
country_list = {
- False: ['USD', 'EUR', 'Benelux', 'Canada'],
+ False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'],
'BE': ['EUR', 'Benelux'],
'IT': ['EUR'],
'CA': ['Canada'],
- 'US': ['USD', 'EUR', 'Benelux', 'Canada']
+ 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada']
}
for country, result in country_list.items():
pls = self.get_pl(show, current_pl, country)
@@ -73,10 +73,10 @@ class TestWebsitePriceList(TransactionCase):
current_pl = False
country_list = {
- False: ['USD', 'EUR', 'Benelux', 'Christmas', 'Canada'],
+ False: ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'],
'BE': ['EUR', 'Benelux', 'Christmas'],
'IT': ['EUR', 'Christmas'],
- 'US': ['USD', 'EUR', 'Benelux', 'Christmas', 'Canada'],
+ 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'],
'CA': ['Canada']
}
@@ -116,11 +116,11 @@ class TestWebsitePriceList(TransactionCase):
current_pl = False
country_list = {
- False: ['USD', 'EUR', 'Benelux', 'Canada'],
+ False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'],
'BE': ['EUR', 'Benelux'],
'IT': ['EUR'],
'CA': ['EUR', 'Canada'],
- 'US': ['USD', 'EUR', 'Benelux', 'Canada']
+ 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada']
}
for country, result in country_list.items():
pls = self.get_pl(show, current_pl, country)
diff --git a/addons/website_sale/views/sale_report_views.xml b/addons/website_sale/views/sale_report_views.xml
index 256472179b7..61b04df2c2b 100644
--- a/addons/website_sale/views/sale_report_views.xml
+++ b/addons/website_sale/views/sale_report_views.xml
@@ -31,7 +31,7 @@
<field name="name">sale.report.view.pivot.website</field>
<field name="model">sale.report</field>
<field name="arch" type="xml">
- <pivot string="Sales Report">
+ <pivot string="Sales Report" disable_linking="True">
<field name="date" type="row"/>
<field name="state" type="col"/>
<field name="price_subtotal" type="measure"/>
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 6522059057b..7b1e43ac9ba 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -416,7 +416,7 @@
<div class="col-sm-7">
<span itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.template/{{product.id}}/image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image'}" groups="!website_sale.group_website_multi_image"/>
<t groups="website_sale.group_website_multi_image">
- <t t-set="variant_img" t-value="product.product_variant_id.image_variant"/>
+ <t t-set="variant_img" t-value="any(product.mapped('product_variant_ids.image_variant'))"/>
<t t-set="image_ids" t-value="product.product_image_ids"/>
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
<div class="carousel-outer">
@@ -1234,7 +1234,7 @@
<input type="hidden" name="partner_id" t-att-value="partner_id or '0'" />
<input type="hidden" name="callback" t-att-value="callback" />
<!-- Example -->
- <input type="hidden" name="field_required" t-att-value="mode[1] == 'billing' and 'phone,name' or ''" />
+ <input type="hidden" name="field_required" t-att-value="'phone,name'" />
<div class="clearfix"/>
<div style='padding: 0 15px'>
diff --git a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js
index 0241c2d7d2f..c5a9f593b6e 100644
--- a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js
+++ b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js
@@ -1,7 +1,7 @@
odoo.define('website_sale_comparison.comparison', function (require) {
"use strict";
-require('web.dom_ready')
+require('web.dom_ready');
var ajax = require('web.ajax');
var core = require('web.core');
var _t = core._t;
@@ -55,7 +55,11 @@ var ProductComparison = Widget.extend({
if (self.comparelist_product_ids.length < self.product_compare_limit) {
var prod = $(this).data('product-product-id');
if (e.currentTarget.classList.contains('o_add_compare_dyn')) {
- prod = parseInt($(this).parent().find('.product_id').val());
+ prod = $(this).parent().find('.product_id').val();
+ if (!prod) { // case List View Variants
+ prod = $(this).parent().find('input:checked').first().val();
+ }
+ prod = parseInt(prod, 10);
}
self.add_new_products(prod);
website_sale_utils.animate_clone($('#comparelist .o_product_panel_header'), $(this).closest('form'), -50, 10);
@@ -68,6 +72,12 @@ var ProductComparison = Widget.extend({
$('body').on('click', '.comparator-popover .o_comparelist_products .o_remove', function (e){
self.rm_from_comparelist(e);
});
+ $('body').on('click', '.o_comparelist_remove', function (e){
+ self.rm_from_comparelist(e);
+ var new_link = '/shop/compare/?products=' + self.comparelist_product_ids.toString();
+ window.location = _.isEmpty(self.comparelist_product_ids) ? '/shop' : new_link;
+ });
+
$("#o_comparelist_table tr").click(function(){
$($(this).data('target')).children().slideToggle(100);
$(this).find('.fa-chevron-circle-down, .fa-chevron-circle-right').toggleClass('fa-chevron-circle-down fa-chevron-circle-right');
diff --git a/addons/website_sale_comparison/views/website_sale_comparison_template.xml b/addons/website_sale_comparison/views/website_sale_comparison_template.xml
index 258b419897b..f472c5db811 100644
--- a/addons/website_sale_comparison/views/website_sale_comparison_template.xml
+++ b/addons/website_sale_comparison/views/website_sale_comparison_template.xml
@@ -74,7 +74,7 @@
<thead>
<td class='td-top-left'></td>
<td t-foreach="products" t-as="product">
- <a t-attf-href="/shop/compare/?products=#{','.join([str(id) for id in products._ids if id != product.id])}" t-att-data-product_product_id="product.id" class="o_comparelist_remove" t-if="len(products) &gt; 2">
+ <a href="#" t-att-data-product_product_id="product.id" class="o_comparelist_remove" t-if="len(products) &gt; 2">
<strong class='pull-right'>x</strong>
</a>
<a t-att-href="'/shop/product/%s' % slug(product.product_tmpl_id)">
diff --git a/addons/website_sale_options/static/src/js/website_sale.test.js b/addons/website_sale_options/static/src/js/website_sale.test.js
index 3b4b6970f9d..38662b52713 100644
--- a/addons/website_sale_options/static/src/js/website_sale.test.js
+++ b/addons/website_sale_options/static/src/js/website_sale.test.js
@@ -47,8 +47,39 @@ tour.register('shop_customize', {
},
{
content: "open customize menu",
- trigger: '#customize-menu > a',
+ trigger: '#customize-menu',
extra_trigger: "#product_detail",
+ run: function() {
+ // ENABLE VARIANT GROUP
+ $('body').addClass('notReady');
+ var ajax = odoo.__DEBUG__.services['web.ajax'];
+ var get_group_payload = {
+ model: 'ir.model.data',
+ method: 'xmlid_to_res_id',
+ args: ['product.group_product_variant', false],
+ kwargs: {}
+ };
+ ajax.jsonpRpc('/web/dataset/call_kw', 'call', get_group_payload).then(function(group_id) {
+ ajax.jsonpRpc('/web/dataset/call_kw', 'call', {
+ model: 'res.groups',
+ method: 'write',
+ args: [group_id, {'users': [[4, 1]]}],
+ kwargs: {}
+ }).then(function() {
+ location.reload(true);
+ });
+ });
+ },
+ },
+ {
+ content: "open customize menu",
+ trigger: '#customize-menu > a',
+ extra_trigger: 'body:not(.notReady)',
+ },
+ {
+ content: "check page loaded after enable variant group",
+ trigger: '#customize-menu a:contains(List View of Variants)',
+ run: function () {}, // it's a check
},
{
content: "click on 'List View of Variants'",
diff --git a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js
index 8f0c3ec0359..34ed9c47957 100644
--- a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js
+++ b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js
@@ -20,7 +20,7 @@ var ProductWishlist = Widget.extend({
init: function(){
var self = this;
this.wishlist_product_ids = [];
- $.get('/shop/wishlist', {'count': 1}).then(function(res) {
+ var wish_loading = $.get('/shop/wishlist', {'count': 1}).then(function(res) {
self.wishlist_product_ids = JSON.parse(res);
self.update_wishlist_view();
});
@@ -44,12 +44,37 @@ var ProductWishlist = Widget.extend({
else {
$el.prop("disabled", true).addClass('disabled').attr('disabled', 'disabled');
}
+ $el.data('product-product-id', parseInt($product_id.val(), 10));
+
+ });
+
+ // manage "List View of variants"
+ $('.oe_website_sale').on('change', 'input.js_product_change', function(ev) {
+ var product_id = ev.currentTarget.value;
+ var $el = $(ev.target).closest('.js_add_cart_variants').find("[data-action='o_wishlist']");
+
+ if (!_.contains(self.wishlist_product_ids, parseInt(product_id, 10))) {
+ $el.prop("disabled", false).removeClass('disabled').removeAttr('disabled');
+ }
+ else {
+ $el.prop("disabled", true).addClass('disabled').attr('disabled', 'disabled');
+ }
+ $el.data('product-product-id', product_id);
});
+ wish_loading.then(function() {
+ if ($('input.js_product_change').length) { // manage "List View of variants"
+ $('input.js_product_change:checked').first().trigger('change');
+ }
+ else {
+ $('input.js_variant_change').trigger('change');
+ }
+ });
+
},
add_new_products:function($el, e){
var self = this;
- var product_id = $el.data('product-product-id');
- if (e.currentTarget.classList.contains('o_add_wishlist_dyn')) {
+ var product_id = parseInt($el.data('product-product-id'), 10);
+ if (!product_id && e.currentTarget.classList.contains('o_add_wishlist_dyn')) {
product_id = parseInt($el.parent().find('.product_id').val());
}
if (!_.contains(self.wishlist_product_ids, product_id)) {
diff --git a/doc/_extensions/odoo_ext/layout.html b/doc/_extensions/odoo_ext/layout.html
index 793ed47806f..47efef8dc84 100644
--- a/doc/_extensions/odoo_ext/layout.html
+++ b/doc/_extensions/odoo_ext/layout.html
@@ -289,29 +289,33 @@
<div class="col-xs-6 col-sm-4">
<span class="menu_title">Services</span>
<ul>
- <li><a href="http://www.odoo.com/pricing">Editions</a></li>
- <li><a href="http://www.odoo.com/pricing-online">Cloud Pricing</a></li>
- <li><a href="http://www.odoo.com/page/upgrade">Upgrade</a></li>
+ <li><a href="https://www.odoo.sh">Odoo Cloud Platform</a></li>
+ <li class="divider"></li>
+ <li><a href="http://www.odoo.com/help">Support</a></li>
+ <li><a href="https://upgrade.odoo.com">Upgrade</a></li>
<li class="divider"></li>
<li><a href="http://www.odoo.com/partners">Find a partner</a></li>
<li><a href="http://www.odoo.com/page/become-a-partner">Become a partner</a></li>
<li class="divider"></li>
+ <li><a href="http://training.odoo.com/courses/odoo-functional">Training Center</a></li>
<li><a href="http://www.odoo.com/page/education-program">Education</a></li>
<li class="divider"></li>
<li><a href="http://www.odoo.com/page/security">Security</a></li>
</ul>
</div>
- <div class="col-sm-4 mb64">
+ <div class="col-xs-12 col-sm-4 mb64">
<span class="menu_title">About us</span>
<ul>
<li><a href="http://www.odoo.com/page/about-us">Our company</a></li>
<li><a href="http://www.odoo.com/page/contactus">Contact</a></li>
<li class="divider" />
<li><a href="http://www.odoo.com/event">Events</a></li>
- <li><a href="http://www.odoo.com/blog/">Blog</a></li>
+ <li><a href="http://www.odoo.com/blog">Blog</a></li>
<li><a href="http://www.odoo.com/blog/6">Customers</a></li>
<li class="divider" />
<li><a href="http://www.odoo.com/jobs">Jobs</a></li>
+ <li class="divider" />
+ <li><a href="http://www.odoo.com/page/legal">Legal</a> | <a href="http://www.odoo.com/privacy">Privacy</a></li>
</ul>
</div>
</div>
diff --git a/doc/cla/corporate/dajmi5.md b/doc/cla/corporate/dajmi5.md
new file mode 100644
index 00000000000..e26ab73ef0d
--- /dev/null
+++ b/doc/cla/corporate/dajmi5.md
@@ -0,0 +1,15 @@
+Croatia, 2017-10-31
+
+DAJ MI 5 agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Davor Bojkić bole@dajmi5.com https://github.com/dajmi5
+
+List of contributors:
+
+Davor Bojkić bole@dajmi5.com https://github.com/badbole \ No newline at end of file
diff --git a/doc/cla/corporate/kazacube.md b/doc/cla/corporate/kazacube.md
new file mode 100644
index 00000000000..ae4d2868080
--- /dev/null
+++ b/doc/cla/corporate/kazacube.md
@@ -0,0 +1,16 @@
+Morocco, 2017-10-11
+
+KAZACUBE SARL agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Damien DELANGLE ddelangle@kazacube.com https://github.com/kazacube-damien
+
+List of contributors:
+
+ddelangle@kazacube.com https://github.com/kazacube-damien
+cloudmaster@kazacube.com https://github.com/sharecodekzc
diff --git a/doc/cla/corporate/odoo_house.md b/doc/cla/corporate/odoo_house.md
new file mode 100644
index 00000000000..34306354122
--- /dev/null
+++ b/doc/cla/corporate/odoo_house.md
@@ -0,0 +1,15 @@
+Denmark, 2018-03-18
+
+Odoo House ApS agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Henning Dinsen henning@odoohouse.dk https://github.com/odoohouse
+
+List of contributors:
+
+Henning Dinsen henning@odoohouse.dk https://github.com/odoohouse
diff --git a/doc/cla/individual/SimoRubi.md b/doc/cla/individual/SimoRubi.md
new file mode 100644
index 00000000000..07a69054170
--- /dev/null
+++ b/doc/cla/individual/SimoRubi.md
@@ -0,0 +1,11 @@
+Italy, 2018-02-05
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Simone Rubino simone.rubino@agilebg.com https://github.com/SimoRubi
diff --git a/doc/cla/individual/agente2012.md b/doc/cla/individual/agente2012.md
new file mode 100644
index 00000000000..d01ad0893f9
--- /dev/null
+++ b/doc/cla/individual/agente2012.md
@@ -0,0 +1,12 @@
+
+Portugal, 2018-03-14
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+João Santos joaopns05@gmail.com https://github.com/agente2012
diff --git a/doc/cla/individual/ernesto-telematel.md b/doc/cla/individual/ernesto-telematel.md
new file mode 100644
index 00000000000..c7f093485af
--- /dev/null
+++ b/doc/cla/individual/ernesto-telematel.md
@@ -0,0 +1,11 @@
+México, 2017-12-12
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Luis Ernesto García Medina ernesto.garcia@telematel.com https://github.com/ernesto-telematel \ No newline at end of file
diff --git a/doc/cla/individual/kurkop.md b/doc/cla/individual/kurkop.md
index ead18fbc4cb..99527b55009 100644
--- a/doc/cla/individual/kurkop.md
+++ b/doc/cla/individual/kurkop.md
@@ -9,3 +9,4 @@ declaration.
Signed,
Juan Pablo Arias jpa@devco.co https://github.com/kurkop
+Juan Pablo Arias j@kurlabs.com https://github.com/kurkop
diff --git a/doc/cla/individual/mginshe.md b/doc/cla/individual/mginshe.md
new file mode 100644
index 00000000000..23e87975a32
--- /dev/null
+++ b/doc/cla/individual/mginshe.md
@@ -0,0 +1,9 @@
+New Zealand, 2018-03-15
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Ryan COLE mginshe@gmail.com https://github.com/mginshe
diff --git a/doc/cla/individual/mpanarin.md b/doc/cla/individual/mpanarin.md
new file mode 100644
index 00000000000..ea0a988978e
--- /dev/null
+++ b/doc/cla/individual/mpanarin.md
@@ -0,0 +1,10 @@
+Ukraine, 2018-03-16
+
+I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+Panarin Mykhailo Ihorevich m.panarin@mobilunity.com https://github.com/mpanarin
+
diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst
index ec31f63af53..7eef640326d 100644
--- a/doc/howtos/backend.rst
+++ b/doc/howtos/backend.rst
@@ -258,6 +258,13 @@ be declared in the ``'data'`` list (always loaded) or in the ``'demo'`` list
.. patch::
+.. tip:: The content of the data files is only loaded when a module is
+ installed or updated.
+
+ After making some changes, do not forget to use
+ :ref:`odoo-bin -u openacademy <reference/cmdline>` to save the changes
+ to your database.
+
Actions and Menus
-----------------
@@ -1036,9 +1043,9 @@ their most common attributes are:
``date_start``
record's field holding the start date/time for the event
``date_stop`` (optional)
- record's field holding the end date/time for the event
-
-field (to define the label for each calendar event)
+   record's field holding the end date/time for the event
+``string``
+ record's field to define the label for each calendar event
.. code-block:: xml
diff --git a/doc/reference/guidelines.rst b/doc/reference/guidelines.rst
index f04050b011e..a965c3a689b 100644
--- a/doc/reference/guidelines.rst
+++ b/doc/reference/guidelines.rst
@@ -6,17 +6,26 @@
Odoo Guidelines
===============
-This page introduces the new Odoo Coding Guidelines. Those aim to improve the quality of the code (e.g. better readability of source) and Odoo Apps. Indeed, proper code eases maintenance, aids debugging, lowers complexity and promotes reliability.
+This page introduces the Odoo Coding Guidelines. Those aim to improve the
+quality of Odoo Apps code. Indeed proper code improves readability, eases
+maintenance, helps debugging, lowers complexity and promotes reliability.
+These guidelines should be applied to every new module and to all new development.
-These guidelines should be applied to every new module, and new development.
+.. warning::
+
+ When modifying existing files in **stable version** the original file style
+ strictly supersedes any other style guidelines. In other words please never
+ modify existing files in order to apply these guidelines. It avoids disrupting
+ the revision history of code lines. Diff should be kept minimal. For more
+ details, see our `pull request guide <https://odoo.com/submit-pr>`_.
.. warning::
- These guidelines are written with new modules and new files in mind. When
- modifying existing files, the original style of the file strictly supersedes
- any other style guidelines. In other words, never modify existing files in
- order to apply these guidelines, to avoid disrupting the revision history of
- each line. For more details, see our `pull request guide <https://odoo.com/submit-pr>`_.
+ When modifying existing files in **master (development) version** apply those
+ guidelines to existing code only for modified code or if most of the file is
+ under revision. In other words modify existing files structure only if it is
+ going under major changes. In that case first do a **move** commit then apply
+ the changes related to the feature.
Module structure
================
@@ -913,50 +922,138 @@ Javascript and CSS
Git
===
-Commit message
---------------
-
-Prefix your commit with
-
-- **[IMP]** for improvements
-- **[FIX]** for bug fixes
-- **[REF]** for refactoring
-- **[ADD]** for adding new resources
-- **[REM]** for removing of resources
-- **[MOV]** for moving files (Do not change content of moved file, otherwise
- Git will loose track, and the history will be lost !), or simply moving code
- from a file to another one.
-- **[MERGE]** for merge commits (only for forward/back-port)
-- **[CLA]** for signing the Odoo Individual Contributor License
-
-Then, in the message itself, specify the part of the code impacted by
-your changes (module name, lib, transversal object, ...) and a description
-of the changes.
-
-- Always include a meaningful commit message: it should be self explanatory
- (long enough) including the name of the module that has been changed and the
- reason behind the change. Do not use single words like "bugfix" or
- "improvements".
-- Avoid commits which simultaneously impact multiple modules. Try to
- split into different commits where impacted modules are different
- (It will be helpful if we need to revert a module separately).
+Configure your git
+------------------
+
+Based on ancestral experience and oral tradition, the following things go a long
+way towards making your commits more helpful:
+
+- Be sure to define both the user.email and user.name in your local git config
+
+ .. code-block:: text
+
+ git config --global <var> <value>
+
+- Be sure to add your full name to your Github profile here. Please feel fancy
+ and add your team, avatar, your favorite quote, and whatnot ;-)
+
+Commit message structure
+------------------------
+
+Commit message has four parts: tag, module, short description and full
+description. Try to follow the preferred structure for your commit messages
+
+.. code-block:: text
+
+ [TAG] module: describe your change in a short sentence (ideally < 50 chars)
+
+ Long version of the change description, including the rationale for the change,
+ or a summary of the feature being introduced.
+
+ Please spend a lot more time describing WHY the change is being done rather
+ than WHAT is being changed. This is usually easy to grasp by actually reading
+ the diff. WHAT should be explained only if there are technical choices
+ or decision involved. In that case explain WHY this decision was taken.
+
+ End the message with references, such as task or bug numbers, PR numbers, and
+ OPW tickets, following the suggested format:
+ Related to task #taskId
+ Fixes #12345 (link and close issue on Github)
+ Closes #7865 (link and close PR on Github)
+ OPW-112233
+
+Tag and module name
+-------------------
+
+Tags are used to prefix your commit. They should be one of the following
+
+- **[FIX]** for bug fixes: mostly used in stable version but also valid if you
+ are fixing a recent bug in development version;
+- **[REF]** for refactoring: when a feature is heavily rewritten;
+- **[ADD]** for adding new modules;
+- **[REM]** for removing resources: removing dead code, removing views,
+ removing modules, ...;
+- **[REV]** for reverting commits: if a commit causes issues or is not wanted
+ reverting it is done using this tag;
+- **[MOV]** for moving files: use git move and do not change content of moved file
+ otherwise Git may loose track and history of the file; also used when moving
+ code from one file to another;
+- **[REL]** for release commits: new major or minor stable versions;
+- **[IMP]** for improvements: most of the changes done in development version
+ are incremental improvements not related to another tag;
+- **[MERGE]** for merge commits: used in forward port of bug fixes but also as
+ main commit for feature involving several separated commits;
+- **[CLA]** for signing the Odoo Individual Contributor License;
+- **[I18N]** for changes in translation files;
+
+After tag comes the modified module name. Use the technical name as functional
+name may change with time. If several modules are modified, list them or use
+various to tell it is cross-modules. Unless really required or easier avoid
+modifying code across several modules in the same commit. Understanding module
+history may become difficult.
+
+Commit message header
+---------------------
+
+After tag and module name comes a meaningful commit message header. It should be
+self explanatory and include the reason behind the change. Do not use single words
+like "bugfix" or "improvements". Try to limit the header length to about 50 characters
+for readability.
+
+Commit message header should make a valid sentence once concatenated with
+``if applied, this commit will <header>``. For example ``[IMP] base: prevent to
+archive users linked to active partners`` is correct as it makes a valid sentence
+``if applied, this commit will prevent users to archive...``.
+
+Commit message full description
+-------------------------------
+
+In the message description specify the part of the code impacted by your changes
+(module name, lib, transversal object, ...) and a description of the changes.
+
+First explain WHY you are modifying code. What is important if someone goes back
+to your commit in about 4 decades (or 3 days) is why you did it. It is the
+purpose of the change.
+
+What you did can be found in the commit itself. If there was some technical choices
+involved it is a good idea to explain it also in the commit message after the why.
+For Odoo R&D developers "PO team asked me to do it" is not a valid why, by the way.
+
+Please avoid commits which simultaneously impact multiple modules. Try to split
+into different commits where impacted modules are different. It will be helpful
+if we need to revert changes in a given module separately.
+
+Don't hesitate to be a bit verbose. Most people will only see your commit message
+and judge everything you did in your life just based on those few sentences.
+No pressure at all.
+
+**You spend several hours, days or weeks working on meaningful features. Take
+some time to calm down and write clear and understandable commit messages.**
+
+If you are an Odoo R&D developer the WHY should be the purpose of the task you
+are working on. Full specifications make the core of the commit message.
+**If you are working on a task that lacks purpose and specifications please
+consider making them clear before continuing.**
+
+Finally here are some examples of correct commit messages :
.. code-block:: text
- [FIX] website, website_mail: remove unused alert div, fixes look of input-group-btn
+ [REF] models: use `parent_path` to implement parent_store
- Bootstrap's CSS depends on the input-group-btn
- element being the first/last child of its parent.
- This was not the case because of the invisible
- and useless alert.
+ This replaces the former modified preorder tree traversal (MPTT) with the
+ fields `parent_left`/`parent_right`[...]
- [IMP] fields: reduce memory footprint of list/set field attributes
+ [FIX] account: remove frenglish
- [REF] web: add module system to the web client
+ [...]
- This commit introduces a new module system for the javascript code.
- Instead of using global ...
+ Closes #22793
+ Fixes #22769
+ [FIX] website: remove unused alert div, fixes look of input-group-btn
-.. note:: Use the long description to explain the *why* not the
- *what*, the *what* can be seen in the diff
+ Bootstrap's CSS depends on the input-group-btn
+ element being the first/last child of its parent.
+ This was not the case because of the invisible
+ and useless alert.
diff --git a/doc/reference/mobile.rst b/doc/reference/mobile.rst
index e645d03a404..355164e82c9 100644
--- a/doc/reference/mobile.rst
+++ b/doc/reference/mobile.rst
@@ -21,7 +21,7 @@ Vibration, Notification and Toast through Odoo Web (via JavaScript). For this, y
do not need to be a mobile developer, if you know Odoo JavaScript API you can
access all available mobile features.
-.. warning:: This features works with **Odoo Enterprise 10.0+** only
+.. warning:: These features work with **Odoo Enterprise 10.0+** only
How does it work?
=================
diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst
index 00b284f9cb2..9ce03c8709a 100644
--- a/doc/reference/orm.rst
+++ b/doc/reference/orm.rst
@@ -476,6 +476,11 @@ added.
``onchange`` methods work on virtual records assignment on these records
is not written to the database, just used to know which value to send back
to the client
+
+.. warning::
+
+ It is not possible for a ``one2many`` or ``many2many`` field to modify
+ itself via onchange. This is a webclient limitation - see `#2693 <https://github.com/odoo/odoo/issues/2693>`_.
Low-level SQL
-------------
@@ -630,6 +635,9 @@ Model Reference
If set to ``False``, override :meth:`.init` to create the database
table
+
+ .. tip:: To create a model without any table, inherit
+ from ``odoo.models.AbstractModel``
.. attribute:: _table
diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml
index b0c001d7aad..fdbb6fff9f2 100644
--- a/odoo/addons/base/data/res_country_data.xml
+++ b/odoo/addons/base/data/res_country_data.xml
@@ -1237,6 +1237,7 @@
<field file="base/static/img/country_flags/pa.png" name="image" type="base64" />
<field name="currency_id" ref="PAB" />
<field eval="507" name="phone_code" />
+ <field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s %(state_name)s %(zip)s\n%(country_name)s'" />
</record>
<record id="pe" model="res.country">
<field name="name">Peru</field>
diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml
index 2d32fedb9c5..6e2d8cf5dc8 100644
--- a/odoo/addons/base/data/res_currency_data.xml
+++ b/odoo/addons/base/data/res_currency_data.xml
@@ -218,7 +218,7 @@
<record id="JPY" model="res.currency">
<field name="name">JPY</field>
<field name="symbol">¥</field>
- <field name="rounding">0.01</field>
+ <field name="rounding">1.00</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Yen</field>
<field name="currency_subunit_label">Cen</field>
@@ -999,7 +999,7 @@
<record id="MAD" model="res.currency">
<field name="name">MAD</field>
- <field name="symbol"> د.م. </field>
+ <field name="symbol">DH</field>
<field name="rounding">0.01</field>
<field name="active" eval="False"/>
<field name="currency_unit_label">Dirham</field>
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 0bc80b6998a..330cdc67a0f 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -172,6 +172,7 @@ xpath_utils['hasclass'] = _hasclass
TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS))
WRONGCLASS = re.compile(r"(@class\s*=|=\s*@class|contains\(@class)")
+READONLY = re.compile(r"\breadonly\b")
class View(models.Model):
@@ -786,12 +787,16 @@ actual arch.
attrs = {}
field = Model._fields.get(node.get('name'))
if field:
+ editable = self.env.context.get('view_is_editable', True) and self._field_is_editable(field, node)
children = False
views = {}
for f in node:
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(f)
- xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(field.comodel_name, f, view_id)
+ xarch, xfields = self.with_context(
+ base_model_name=model,
+ view_is_editable=editable,
+ ).postprocess_and_fields(field.comodel_name, f, view_id)
views[str(f.tag)] = {
'arch': xarch,
'fields': xfields,
@@ -866,6 +871,17 @@ actual arch.
return arch
+ def _view_is_editable(self, node):
+ """ Return whether the node is an editable view. """
+ return node.tag == 'form' or node.tag == 'tree' and node.get('editable')
+
+ def _field_is_editable(self, field, node):
+ """ Return whether a field is editable (not always readonly). """
+ return (
+ (not field.readonly or READONLY.search(str(field.states or ""))) and
+ (node.get('readonly') != "1" or READONLY.search(node.get('attrs') or ""))
+ )
+
def get_attrs_symbols(self):
""" Return a set of predefined symbols for evaluating attrs. """
return {
@@ -885,10 +901,11 @@ actual arch.
'current_date',
}
- def get_attrs_field_names(self, arch):
+ def get_attrs_field_names(self, arch, model, editable):
""" Retrieve the field names appearing in context, domain and attrs, and
return a list of triples ``(field_name, attr_name, attr_value)``.
"""
+ VIEW_TYPES = {item[0] for item in type(self).type.selection}
symbols = self.get_attrs_symbols() | {None}
result = []
@@ -918,8 +935,17 @@ actual arch.
if isinstance(arg, (tuple, list)):
process_expr(str(arg[0]), get, key, expr)
- def process(node, get=get_name):
+ def process(node, model, editable, get=get_name):
""" traverse `node` and collect triples """
+ if node.tag in VIEW_TYPES:
+ # determine whether this view is editable
+ editable = editable and self._view_is_editable(node)
+ elif node.tag == 'field':
+ # determine whether the field is editable
+ field = model._fields.get(node.get('name'))
+ if field:
+ editable = editable and self._field_is_editable(field, node)
+
for key, val in node.items():
if not val:
continue
@@ -927,13 +953,21 @@ actual arch.
process_expr(val, get, key, val)
elif key == 'attrs':
process_attrs(val, get, key, val)
- if node.tag == 'field':
+
+ if node.tag == 'field' and field and field.relational:
+ if editable and not node.get('domain'):
+ domain = field._description_domain(self.env)
+ # process the field's domain as if it was in the view
+ if isinstance(domain, pycompat.string_types):
+ process_expr(domain, get, 'domain', domain)
# retrieve subfields of 'parent'
+ model = self.env[field.comodel_name]
get = partial(get_subname, get)
+
for child in node:
- process(child, get)
+ process(child, model, editable, get)
- process(arch)
+ process(arch, model, editable)
return result
@api.model
@@ -974,7 +1008,8 @@ actual arch.
attrs_fields = []
if self.env.context.get('check_field_names'):
- attrs_fields = self.get_attrs_field_names(node)
+ editable = self.env.context.get('view_is_editable', True)
+ attrs_fields = self.get_attrs_field_names(node, Model, editable)
fields_def = self.postprocess(model, node, view_id, False, fields)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py
index 60f42b16c1c..421da4a899c 100644
--- a/odoo/addons/base/models/res_config.py
+++ b/odoo/addons/base/models/res_config.py
@@ -370,9 +370,8 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
``execute``` will save its value in an ir.config.parameter (global setting for the
database).
- * For the other fields, the method ``execute`` invokes all methods with a name
- that starts with 'set_'; such methods can be defined to implement the effect
- of those fields.
+ * For the other fields, the method ``execute`` invokes `set_values`.
+ Override it to implement the effect of those fields.
The method ``default_get`` retrieves values that reflect the current status of the
fields like 'default_XXX', 'group_XXX', 'module_XXX' and config_XXX.
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index abb25a4bc2e..efcf203edf0 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -4,11 +4,13 @@ import pytz
import datetime
import itertools
import logging
+import hmac
from collections import defaultdict
from itertools import chain, repeat
from lxml import etree
from lxml.builder import E
+from hashlib import sha256
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationError
@@ -374,6 +376,8 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
+ if any(key in values for key in self._get_session_token_fields()):
+ self._invalidate_session_cache()
return res
@@ -384,6 +388,7 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
+ self._invalidate_session_cache()
return super(Users, self).unlink()
@api.model
@@ -509,6 +514,34 @@ class Users(models.Model):
finally:
cr.close()
+ def _get_session_token_fields(self):
+ return {'id', 'login', 'password', 'active'}
+
+ @tools.ormcache('sid')
+ def _compute_session_token(self, sid):
+ """ Compute a session token given a session id and a user id """
+ # retrieve the fields used to generate the session token
+ session_fields = ', '.join(sorted(self._get_session_token_fields()))
+ self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret')
+ FROM res_users
+ WHERE id=%%s""" % (session_fields), (self.id,))
+ if self.env.cr.rowcount != 1:
+ self._invalidate_session_cache()
+ return False
+ data_fields = self.env.cr.fetchone()
+ # generate hmac key
+ key = (u'%s' % (data_fields,)).encode('utf-8')
+ # hmac the session id
+ data = sid.encode('utf-8')
+ h = hmac.new(key, data, sha256)
+ # keep in the cache the token
+ return h.hexdigest()
+
+ @api.multi
+ def _invalidate_session_cache(self):
+ """ Clear the sessions cache """
+ self._compute_session_token.clear_cache(self)
+
@api.model
def change_password(self, old_passwd, new_passwd):
"""Change current user password. Old password must be provided explicitly
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index 7a1a5bfb617..567df2b352a 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -1107,6 +1107,183 @@ class TestViews(ViewCase):
})
@mute_logger('odoo.addons.base.models.ir_ui_view')
+ def test_domain_on_field_in_view(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+ <form string="View">
+ <field name="name"/>%s
+ <field name="inherit_id"/>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % '<field name="model"/>',
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % '',
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_field_in_subview(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+ <form string="View">
+ <field name="name"/>%s
+ <field name="inherit_children_ids">
+ <form string="Children">
+ <field name="name"/>%s
+ <field name="inherit_id"/>
+ </form>
+ </field>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', '<field name="model"/>'),
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', ''),
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('<field name="model"/>', ''),
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_field_in_subview_with_parent(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', parent.model)]")
+
+ arch = """
+ <form string="View">
+ <field name="name"/>%s
+ <field name="inherit_children_ids">
+ <form string="Children">
+ <field name="name"/>%s
+ <field name="inherit_id"/>
+ </form>
+ </field>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('<field name="model"/>', ''),
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', ''),
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', '<field name="model"/>'),
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_field_in_noneditable_subview(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+ <form string="View">
+ <field name="name"/>
+ <field name="inherit_children_ids">
+ <tree string="Children"%s>
+ <field name="name"/>
+ <field name="inherit_id"/>
+ </tree>
+ </field>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % '',
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ' editable="bottom"',
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_readonly_field_in_view(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+ <form string="View">
+ <field name="name"/>
+ <field name="inherit_id" readonly="1"/>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch,
+ })
+
+ self.patch(field, 'readonly', True)
+ arch = """
+ <form string="View">
+ <field name="name"/>
+ <field name="inherit_id"/>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch,
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_readonly_field_in_subview(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+ <form string="View">
+ <field name="name"/>
+ <field name="inherit_children_ids"%s>
+ <form string="Children">
+ <field name="name"/>
+ <field name="inherit_id"/>
+ </form>
+ </field>
+ </form>
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ' readonly="1"',
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % '',
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
def test_attrs_field(self):
arch = """
<form string="View">
diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js
index 7b08672385c..48417b619f1 100644
--- a/odoo/addons/test_main_flows/static/src/js/tour.js
+++ b/odoo/addons/test_main_flows/static/src/js/tour.js
@@ -57,6 +57,22 @@ tour.register('main_flow_tour', {
content: _t('Uncheck Make To Order'),
position: 'right',
}, {
+ trigger: '.o_notebook a:contains("General Information")',
+ content: _t('Go to main tab'),
+ position: 'top',
+}, {
+ trigger: ".o_field_widget[name=taxes_id] input",
+ content: _t("Focus on customer taxes field."),
+ run: function(actions) {
+ actions.click();
+ var $e = $(".ui-menu-item.ui-state-focus:not(.o_m2o_dropdown_option)");
+ if ($e.length) {
+ actions.click($e);
+ } else {
+ actions.click(); // close dropdown
+ }
+ },
+}, {
trigger: '.o_form_button_save',
content: _t('Save this product and the modifications you\'ve made to it.'),
position: 'bottom',
@@ -208,6 +224,18 @@ tour.register('main_flow_tour', {
position: 'left',
run: 'text "service"',
}, {
+ trigger: ".o_field_widget[name=taxes_id] input",
+ content: _t("Focus on customer taxes field."),
+ run: function(actions) {
+ actions.click();
+ var $e = $(".ui-menu-item.ui-state-focus:not(.o_m2o_dropdown_option)");
+ if ($e.length) {
+ actions.click($e);
+ } else {
+ actions.click(); // close dropdown
+ }
+ },
+}, {
trigger: '.o_notebook a:contains("Sales")',
content: _t('Go to sales tab'),
position: 'bottom',
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index bd33123af89..a7d0f977968 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -278,6 +278,14 @@ class TestFields(common.TransactionCase):
self.assertEqual(ewan.parent, cath)
self.assertEqual(ewan.name, "Erwan")
+ # write on non-stored inverse field on severals records
+ foo1 = Category.create({'name': 'Foo'})
+ foo2 = Category.create({'name': 'Foo'})
+ (foo1 + foo2).write({'display_name': 'Bar'})
+ self.assertEqual(foo1.name, 'Bar')
+ self.assertEqual(foo2.name, 'Bar')
+
+
# create/write on 'foo' should only invoke the compute method
log = []
model = self.env['test_new_api.compute.inverse'].with_context(log=log)
diff --git a/odoo/api.py b/odoo/api.py
index 0e9975f25c9..3f55669023e 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -1021,8 +1021,10 @@ class Cache(object):
for field, field_cache in self._data.items():
src_key = field.cache_key(src)
dst_key = field.cache_key(dst)
- for record_id, record_cache in field_cache.items():
- if src_key in record_cache:
+ for record_cache in field_cache.values():
+ if src_key in record_cache and not isinstance(record_cache[src_key], SpecialValue):
+ # But not if it's a SpecialValue, which often is an access error
+ # because the other environment (eg. sudo()) is well expected to have access.
record_cache[dst_key] = record_cache[src_key]
def invalidate(self, spec=None):
diff --git a/odoo/fields.py b/odoo/fields.py
index 46898e3fcd4..8980fae3e29 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -1553,6 +1553,9 @@ class Date(Field):
""" Convert a :class:`date` value into the format expected by the ORM. """
return value.strftime(DATE_FORMAT) if value else False
+ def convert_to_column(self, value, record, values=None, validate=True):
+ return super(Date, self).convert_to_column(value or None, record, values, validate)
+
def convert_to_cache(self, value, record, validate=True):
if not value:
return False
@@ -1623,6 +1626,9 @@ class Datetime(Field):
""" Convert a :class:`datetime` value into the format expected by the ORM. """
return value.strftime(DATETIME_FORMAT) if value else False
+ def convert_to_column(self, value, record, values=None, validate=True):
+ return super(Datetime, self).convert_to_column(value or None, record, values, validate)
+
def convert_to_cache(self, value, record, validate=True):
if not value:
return False
diff --git a/odoo/http.py b/odoo/http.py
index 4cfe76a76a7..9c6c0092e25 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -1035,7 +1035,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
- self.password = password
+ self.session_token = uid and security.compute_session_token(self)
request.uid = uid
request.disable_db = False
@@ -1050,7 +1050,10 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
- security.check(self.db, self.uid, self.password)
+
+ # here we check if the session is still valid
+ if not security.check_session(self):
+ raise SessionExpiredException("Session expired")
def logout(self, keep_db=False):
for k in list(self):
@@ -1063,7 +1066,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.setdefault("db", None)
self.setdefault("uid", None)
self.setdefault("login", None)
- self.setdefault("password", None)
+ self.setdefault("session_token", None)
self.setdefault("context", {})
def get_context(self):
diff --git a/odoo/models.py b/odoo/models.py
index 6834dca5b6a..4aa616ed5dc 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -3025,17 +3025,27 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self.env[model_name].browse(parent_ids).write(parent_vals)
if inverse_vals:
- # put the values of inverse fields in cache, and inverse them
self.modified(set(inverse_vals) - set(store_vals))
- for record in self:
- record._cache.update(record._convert_to_cache(inverse_vals, update=True))
# in case several fields use the same inverse method, call it once
for _inv, fields in groupby(inverse_fields, attrgetter('inverse')):
- fields[0].determine_inverse(self)
+ # If a field is not stored, its inverse method will probably
+ # write on its dependencies, which will invalidate the field
+ # on all records. We therefore inverse the field one record
+ # at a time.
+ batches = [self] if all(f.store for f in fields) else list(self)
+ # put the values of fields in cache, and inverse them
+ inv_vals = {f.name: inverse_vals[f.name] for f in fields}
+ for records in batches:
+ for record in records:
+ record._cache.update(
+ record._convert_to_cache(inv_vals, update=True)
+ )
+ fields[0].determine_inverse(records)
self.modified(set(inverse_vals) - set(store_vals))
+
# check Python constraints for inversed fields
self._validate_fields(set(inverse_vals) - set(store_vals))
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index 083fda4203b..4dda572fb13 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -400,7 +400,12 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
cr.commit()
_logger.info('Reloading registry once more after uninstalling modules')
api.Environment.reset()
- return odoo.modules.registry.Registry.new(cr.dbname, force_demo, status, update_module)
+ registry = odoo.modules.registry.Registry.new(
+ cr.dbname, force_demo, status, update_module
+ )
+ registry.check_tables_exist(cr)
+ cr.commit()
+ return registry
# STEP 6: verify custom views on every model
if update_module:
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index fb568185d9a..4352b0583f9 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -303,8 +303,16 @@ class Registry(Mapping):
models[0].recompute()
# make sure all tables are present
+ self.check_tables_exist(cr)
+
+ def check_tables_exist(self, cr):
+ """
+ Verify that all tables are present and try to initialize those that are missing.
+ """
+ env = odoo.api.Environment(cr, SUPERUSER_ID, {})
table2model = {model._table: name for name, model in env.items() if not model._abstract}
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
+
if missing_tables:
missing = {table2model[table] for table in missing_tables}
_logger.warning("Models have no table: %s.", ", ".join(missing))
diff --git a/odoo/service/security.py b/odoo/service/security.py
index 264e88d42bd..229e4b84077 100644
--- a/odoo/service/security.py
+++ b/odoo/service/security.py
@@ -11,3 +11,16 @@ def login(db, login, password):
def check(db, uid, passwd):
res_users = odoo.registry(db)['res.users']
return res_users.check(db, uid, passwd)
+
+def compute_session_token(session):
+ with odoo.registry(session.db).cursor() as cr:
+ self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
+ return self._compute_session_token(session.sid)
+
+def check_session(session):
+ with odoo.registry(session.db).cursor() as cr:
+ self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
+ if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token):
+ return True
+ self._invalidate_session_cache()
+ return False
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index df4178ee0e9..7e974a96656 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -44,6 +44,8 @@ except ImportError:
import odoo
from odoo import api
+from odoo.service import security
+
_logger = logging.getLogger(__name__)
@@ -361,7 +363,7 @@ class HttpCase(TransactionCase):
session.db = db
session.uid = uid
session.login = user
- session.password = password
+ session.session_token = uid and security.compute_session_token(session)
session.context = env['res.users'].context_get() or {}
session.context['uid'] = uid
session._fix_lang(session.context)
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index e6dc561188b..5edf8df21bc 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -460,7 +460,7 @@ def quote(s):
.replace('\n', '\\n"\n"')
re_escaped_char = re.compile(r"(\\.)")
-re_escaped_replacements = {'n': '\n', }
+re_escaped_replacements = {'n': '\n', 't': '\t',}
def _sub_replacement(match_obj):
return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])