summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-03-20 11:45:11 +0100
committerChristophe Simonis <chs@odoo.com>2018-03-20 11:45:11 +0100
commite0345a4a3f4cfa8f4de8723c6b72b741b06483a1 (patch)
tree5ad51dbebfd89cd7e56bf12df2da3ecf4d8e9509 /addons
parente9db6b6d9de1e0589e8871ca2e5e8dde8ec8cab7 (diff)
parent2835d299792ffa925d86ad3460057be713ac4fac (diff)
[MERGE] forward port branch 11.0 up to 2835d299792
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account.py21
-rw-r--r--addons/account/models/account_invoice.py3
-rw-r--r--addons/account/models/account_move.py13
-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/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/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/thread.js2
-rw-r--r--addons/mail/static/src/xml/thread.xml2
-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_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/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/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/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_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
109 files changed, 1266 insertions, 336 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index baa0aed666e..cde55f97bf4 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -412,7 +412,7 @@ class AccountJournal(models.Model):
belongs_to_company = fields.Boolean('Belong to the user\'s current company', compute="_belong_to_company", search="_search_company_journals",)
# Bank journals fields
- bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False, domain="[('partner_id','=', company_id)]")
+ bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False)
bank_statements_source = fields.Selection([('undefined', 'Undefined Yet'),('manual', 'Record Manually')], string='Bank Feeds', default='undefined')
bank_acc_number = fields.Char(related='bank_account_id.acc_number')
bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id')
@@ -522,11 +522,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.'))
@@ -542,8 +547,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_invoice.py b/addons/account/models/account_invoice.py
index e05b02ae058..bca650dcc65 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 df18aa8fca9..5f9a2cf26ba 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:
@@ -936,7 +936,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.
@@ -974,8 +976,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'
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 d280b6cb850..4de3fedbbd4 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 dbfd933c99a..a41803f7062 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 2da6c42abe7..852b2a14e00 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/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 50124617526..6fdc285a97b 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -252,7 +252,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"/>
@@ -299,7 +299,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>
@@ -397,7 +397,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"/>
@@ -500,7 +500,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 c652267ad5d..ddc2d48ce76 100644
--- a/addons/account_voucher/models/account_voucher.py
+++ b/addons/account_voucher/models/account_voucher.py
@@ -154,6 +154,7 @@ class AccountVoucher(models.Model):
@api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self):
+ pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
if self.pay_now != 'pay_now':
if self.partner_id:
self.account_id = self.partner_id.property_account_receivable_id \
@@ -163,6 +164,12 @@ class AccountVoucher(models.Model):
domain = [('deprecated', '=', False), ('internal_type', '=', account_type)]
self.account_id = self.env['account.account'].search(domain, limit=1)
+ 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 2710b693ccd..258e6ca8af8 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="company_id" invisible="1"/>
<field name="company_currency" invisible="1"/>
<label for="planned_revenue"/>
@@ -267,6 +265,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 49d8a9e0859..21d302a63c5 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr.py
@@ -126,7 +126,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 5d6c49a0bf7..af9d6c342a5 100644
--- a/addons/hr_payroll/models/hr_salary_rule.py
+++ b/addons/hr_payroll/models/hr_salary_rule.py
@@ -76,7 +76,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.")
@@ -89,7 +89,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/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/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 26c093c3e84..eef37598164 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/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 1b6246290fa..9e53f94d101 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 4b24fdf00e6..f59d6f81264 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 0e0a8a0c103..ffa74c60037 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 6532067bb57..006711c5d39 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"
@@ -340,7 +341,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_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 e16fe051da9..4ffddfeef20 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 a8b54e88b39..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 openerp
-from openerp import api, SUPERUSER_ID
-
from . import models # noqa
from . import report # noqa
-
-
-def uninstall_hook(cr, registry):
- def recreate_view(dbname):
- db_registry = openerp.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 3dbea80703b..1bf167d5731 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1068,18 +1068,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 1147c9e6a96..f09d9ca1160 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):
@@ -813,31 +813,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 73368db6ad4..5711400941e 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 9c5c7552d4b..cbb472a946f 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 1a012255ae1..cd945d3bfe0 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 a1eb43c76c6..49ff9488376 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 60c387fd395..ab7ac5daa7c 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/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 76aadf49463..db440cd742f 100644
--- a/addons/web/static/src/js/chrome/web_client.js
+++ b/addons/web/static/src/js/chrome/web_client.js
@@ -135,7 +135,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);
}
});
}
@@ -156,7 +156,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 75aa1acd164..4b46db8a8ff 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -2007,8 +2007,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 f81485a6b08..d5add00e38c 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/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 21259671505..c6afa9c4921 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -359,6 +359,11 @@ var BasicModel = AbstractModel.extend({
elem._changes = null;
elem._isDirty = false;
}
+ elem.offset = 0;
+ if (elem.tempLimitIncrement) {
+ elem.limit -= elem.tempLimitIncrement;
+ delete elem.tempLimitIncrement;
+ }
});
},
/**
@@ -809,6 +814,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;
});
@@ -1640,6 +1646,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({
@@ -1684,6 +1696,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);
@@ -3824,7 +3840,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 b289663fb19..b328b88f5e5 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -610,6 +610,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 f53a5d185c3..34999256fa5 100644
--- a/addons/web/static/src/less/kanban_view.less
+++ b/addons/web/static/src/less/kanban_view.less
@@ -230,7 +230,7 @@
height: 20px;
}
.oe_kanban_bottom_left {
- .o-flex(1, 0, auto);
+ .o-flex(1, 1, auto);
> * {
margin-right: 6px;
@@ -243,7 +243,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 fd263a07eb6..3ef2a213398 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"/>
@@ -985,7 +985,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">
@@ -1097,7 +1097,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">
@@ -1118,7 +1118,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">
@@ -1247,8 +1247,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>
@@ -1262,8 +1262,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/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 8488076b3f0..a60e3419bdb 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 9b8df65ab29..dbdde1cb566 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_delete');
+
+ 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 e9b652659ff..192efa338bd 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -5202,6 +5202,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 6a4d7c7a609..86626229867 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 = Widget.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_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 3cf961ada09..ca6f4147418 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 7d6b4ee7524..9fa440df145 100644
--- a/addons/website_sale/data/demo.xml
+++ b/addons/website_sale/data/demo.xml
@@ -722,7 +722,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 662d2212838..1ca35623f09 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -415,7 +415,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">
@@ -1224,7 +1224,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 b94f4493a2a..1c4cb821445 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)) {