summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-08-10 17:01:05 +0200
committerChristophe Simonis <chs@odoo.com>2017-08-10 17:01:05 +0200
commit48b2ce60ed12b9b0f3aaa0322465504798bd154e (patch)
tree0ac2c53a78f678ad4701943eb9eab6a4c8f5ff48
parent298eaca4d1e268c43185e9c1dd6d6973e02d9b57 (diff)
parent85571bb78c9acc4e867cc3b480be9236a756fc22 (diff)
[MERGE] forward port branch saas-16 up to 85571bb78c9
-rw-r--r--addons/account/models/account_bank_statement.py20
-rw-r--r--addons/account/models/account_invoice.py4
-rw-r--r--addons/account/models/account_move.py124
-rw-r--r--addons/account/tests/test_reconciliation.py144
-rw-r--r--addons/account/views/account_config_settings_views.xml2
-rw-r--r--addons/account_asset/models/account_invoice.py3
-rw-r--r--addons/account_asset/static/tests/account_asset_tests.js5
-rw-r--r--addons/base_setup/views/base_config_settings_views.xml2
-rw-r--r--addons/event/models/event.py3
-rw-r--r--addons/event/views/event_config_settings_views.xml2
-rw-r--r--addons/event_sale/models/event.py6
-rw-r--r--addons/gamification/data/goal_base.xml21
-rw-r--r--addons/hr_attendance/views/attendance_config_settings_views.xml4
-rw-r--r--addons/hr_expense/models/hr_expense.py2
-rw-r--r--addons/hr_expense/views/hr_expense_config_settings_views.xml2
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml2
-rw-r--r--addons/hr_payroll/views/hr_payroll_config_settings_views.xml2
-rw-r--r--addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml2
-rw-r--r--addons/hr_timesheet/models/hr_timesheet.py2
-rw-r--r--addons/mail/models/mail_template.py1
-rw-r--r--addons/mail/models/mail_thread.py8
-rw-r--r--addons/mass_mailing/views/mass_mailing_config_settings_views.xml2
-rw-r--r--addons/mrp/views/mrp_config_settings_views.xml2
-rw-r--r--addons/pad/models/pad.py2
-rw-r--r--addons/pad/static/src/js/pad.js2
-rw-r--r--addons/pad/static/tests/pad_tests.js36
-rw-r--r--addons/payment_stripe/models/payment.py8
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js32
-rw-r--r--addons/point_of_sale/models/pos_order.py2
-rw-r--r--addons/point_of_sale/tests/test_point_of_sale_flow.py7
-rw-r--r--addons/point_of_sale/views/pos_config_settings_views.xml2
-rw-r--r--addons/product/models/product.py4
-rw-r--r--addons/product/models/product_pricelist.py3
-rw-r--r--addons/product/models/product_template.py6
-rw-r--r--addons/project/views/project_config_settings_views.xml2
-rw-r--r--addons/purchase/views/purchase_config_settings_views.xml2
-rw-r--r--addons/sales_team/views/sale_config_settings_views.xml4
-rw-r--r--addons/stock/models/stock_location.py2
-rw-r--r--addons/stock/views/stock_config_settings_views.xml2
-rw-r--r--addons/web/controllers/main.py12
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js33
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js14
-rw-r--r--addons/web/static/src/js/views/abstract_view.js9
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js12
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js35
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js7
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js20
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_controller.js8
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js2
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js20
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js251
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js59
-rw-r--r--addons/web/static/tests/views/calendar_tests.js42
-rw-r--r--addons/web/static/tests/views/form_tests.js15
-rw-r--r--addons/web/static/tests/views/list_tests.js14
-rw-r--r--addons/web_editor/static/src/js/backend.js2
-rw-r--r--addons/web_editor/static/tests/web_editor_tests.js30
-rw-r--r--addons/website/static/src/js/website.snippets.animation.js3
-rw-r--r--addons/website/views/website_config_settings_views.xml2
-rw-r--r--addons/website_event/controllers/main.py3
-rw-r--r--addons/website_event/views/event_templates.xml3
-rw-r--r--addons/website_event_sale/controllers/main.py7
-rw-r--r--addons/website_sale/controllers/main.py3
-rw-r--r--addons/website_sale/views/templates.xml10
-rw-r--r--doc/cla/corporate/vizucom.md14
-rw-r--r--odoo/addons/base/res/res_currency.py2
-rw-r--r--odoo/addons/test_main_flows/static/src/js/tour.js2
-rw-r--r--odoo/fields.py3
-rw-r--r--odoo/http.py6
-rw-r--r--odoo/models.py17
-rw-r--r--odoo/service/wsgi_server.py2
-rw-r--r--odoo/tests/common.py8
-rw-r--r--odoo/tools/misc.py8
73 files changed, 956 insertions, 203 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 5f1ae94b93a..d565b07e54c 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -307,7 +307,10 @@ class AccountBankStatement(models.Model):
#try to assign partner to bank_statement_line
stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id]
refs = list(set([st.name for st in st_lines_left if not stl.partner_id]))
- if st_lines_left and stl_to_assign_partner and refs:
+ if st_lines_left and stl_to_assign_partner and refs\
+ and st_lines_left[0].journal_id.default_credit_account_id\
+ and st_lines_left[0].journal_id.default_debit_account_id:
+
sql_query = """SELECT aml.partner_id, aml.ref, stl.id
FROM account_move_line aml
JOIN account_account acc ON acc.id = aml.account_id
@@ -605,13 +608,14 @@ class AccountBankStatementLine(models.Model):
acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
select_clause = "SELECT aml.id "
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
- where_clause = """WHERE aml.company_id = %(company_id)s
- AND (
- (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s
- AND aml.payment_id IS NOT NULL)
- OR
- ("""+acc_type+""" AND aml.reconciled = false)
- )"""
+ account_clause = ''
+ if self.journal_id.default_credit_account_id and self.journal_id.default_debit_account_id:
+ account_clause = "(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s AND aml.payment_id IS NOT NULL) OR"
+ where_clause = """WHERE aml.company_id = %(company_id)s
+ AND (
+ """ + account_clause + """
+ ("""+acc_type+""" AND aml.reconciled = false)
+ )"""
where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause
where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause
if split:
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 9fd47eb1592..d5630cdc134 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -837,7 +837,6 @@ class AccountInvoice(models.Model):
if tax.amount_type == "group":
for child_tax in tax.children_tax_ids:
done_taxes.append(child_tax.id)
- done_taxes.append(tax.id)
res.append({
'invoice_tax_line_id': tax_line.id,
'tax_line_id': tax_line.tax_id.id,
@@ -849,8 +848,9 @@ class AccountInvoice(models.Model):
'account_id': tax_line.account_id.id,
'account_analytic_id': tax_line.account_analytic_id.id,
'invoice_id': self.id,
- 'tax_ids': [(6, 0, done_taxes)] if tax_line.tax_id.include_base_amount else []
+ 'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else []
})
+ done_taxes.append(tax.id)
return res
def inv_line_characteristic_hashcode(self, invoice_line):
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 413c2e39440..d2369a6292b 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -539,6 +539,7 @@ class AccountMoveLine(models.Model):
{2}
WHERE
a.reconcile IS TRUE
+ AND l.full_reconcile_id is NULL
{3}
{4}
{5}
@@ -1557,6 +1558,50 @@ class AccountPartialReconcile(models.Model):
move.post()
return line_to_reconcile, partial_rec
+ # Do not forwardport in master as of 2017-07-20
+ def _fix_multiple_exchange_rates_diff(self, amls_to_fix, amount_diff, diff_in_currency, currency, move):
+ self.ensure_one()
+ move_lines = self.env['account.move.line'].with_context(check_move_validity=False)
+ partial_reconciles = self.with_context(skip_full_reconcile_check=True)
+ amount_diff = self.company_id.currency_id.round(amount_diff)
+ diff_in_currency = currency.round(diff_in_currency)
+
+ for aml in amls_to_fix:
+ account_payable_line = move_lines.create({
+ 'name': _('Currency exchange rate difference'),
+ 'debit': amount_diff < 0 and -aml.amount_residual or 0.0,
+ 'credit': amount_diff > 0 and aml.amount_residual or 0.0,
+ 'account_id': self.debit_move_id.account_id.id,
+ 'move_id': move.id,
+ 'currency_id': currency.id,
+ 'amount_currency': -aml.amount_residual_currency,
+ 'partner_id': self.debit_move_id.partner_id.id,
+ })
+
+ move_lines.create({
+ 'name': _('Currency exchange rate difference'),
+ 'debit': amount_diff > 0 and aml.amount_residual or 0.0,
+ 'credit': amount_diff < 0 and -aml.amount_residual or 0.0,
+ 'account_id': amount_diff > 0 and self.company_id.currency_exchange_journal_id.default_debit_account_id.id or self.company_id.currency_exchange_journal_id.default_credit_account_id.id,
+ 'move_id': move.id,
+ 'currency_id': currency.id,
+ 'amount_currency': aml.amount_residual_currency,
+ 'partner_id': self.debit_move_id.partner_id.id})
+
+ partial_rec = super(AccountPartialReconcile, partial_reconciles).create({
+ 'debit_move_id': aml.credit and account_payable_line.id or aml.id,
+ 'credit_move_id': aml.debit and account_payable_line.id or aml.id,
+ 'amount': abs(aml.amount_residual),
+ 'amount_currency': abs(aml.amount_residual_currency),
+ 'currency_id': currency.id,
+ })
+
+ move_lines |= account_payable_line
+ partial_reconciles |= partial_rec
+
+ partial_reconciles._compute_partial_lines()
+ return move_lines, partial_reconciles
+
def create_tax_cash_basis_entry(self, percentage_before_rec):
self.ensure_one()
move_date = self.debit_move_id.date
@@ -1659,30 +1704,16 @@ class AccountPartialReconcile(models.Model):
}
return self.env['account.move'].create(move_vals)
- @api.model
- def create(self, vals):
- aml = []
- if vals.get('debit_move_id', False):
- aml.append(vals['debit_move_id'])
- if vals.get('credit_move_id', False):
- aml.append(vals['credit_move_id'])
- # Get value of matched percentage from both move before reconciliating
- lines = self.env['account.move.line'].browse(aml)
- if lines[0].account_id.internal_type in ('receivable', 'payable'):
- percentage_before_rec = lines._get_matched_percentage()
- # Reconcile
- res = super(AccountPartialReconcile, self).create(vals)
- # if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
- if lines[0].account_id.internal_type in ('receivable', 'payable'):
- res.create_tax_cash_basis_entry(percentage_before_rec)
+ # Do not forwardport in master as of 2017-07-20
+ def _compute_partial_lines(self):
if self._context.get('skip_full_reconcile_check'):
#when running the manual reconciliation wizard, don't check the partials separately for full
#reconciliation or exchange rate because it is handled manually after the whole processing
- return res
+ return self
#check if the reconcilation is full
#first, gather all journal items involved in the reconciliation just created
- partial_rec_set = OrderedDict.fromkeys([x for x in res])
- aml_set = self.env['account.move.line']
+ partial_rec_set = OrderedDict.fromkeys([x for x in self])
+ aml_set = aml_to_balance = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
@@ -1690,7 +1721,7 @@ class AccountPartialReconcile(models.Model):
#possible to compute the exchange difference entry and it has to be done manually.
currency = list(partial_rec_set)[0].currency_id
maxdate = None
- aml_to_balance = None
+
for partial_rec in partial_rec_set:
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
@@ -1698,7 +1729,7 @@ class AccountPartialReconcile(models.Model):
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
if aml.amount_residual or aml.amount_residual_currency:
- aml_to_balance = aml
+ aml_to_balance |= aml
maxdate = max(aml.date, maxdate)
total_debit += aml.debit
total_credit += aml.credit
@@ -1716,16 +1747,21 @@ class AccountPartialReconcile(models.Model):
aml_ids = aml_set.ids
#then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
+
if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
exchange_move_id = False
exchange_partial_rec_id = False
if currency and aml_to_balance:
+ exchange_move = (self.env['account.move']
+ .create(self.env['account.full.reconcile']
+ ._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id)))
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
- rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate)
- aml_ids.append(rate_diff_aml.id)
- partial_rec_ids.append(rate_diff_partial_rec.id)
- exchange_move_id = rate_diff_aml.move_id.id
- exchange_partial_rec_id = rate_diff_partial_rec.id
+ rate_diff_amls, rate_diff_partial_recs = partial_rec._fix_multiple_exchange_rates_diff(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
+ aml_ids += rate_diff_amls.ids
+ partial_rec_ids += rate_diff_partial_recs.ids
+ exchange_move.post()
+ exchange_move_id = exchange_move.id
+ exchange_partial_rec_id = rate_diff_partial_recs[-1:].id
#mark the reference of the full reconciliation on the partial ones and on the entries
self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids],
@@ -1733,6 +1769,25 @@ class AccountPartialReconcile(models.Model):
'exchange_move_id': exchange_move_id,
'exchange_partial_rec_id': exchange_partial_rec_id,
})
+
+ # Do not forwardport in master as of 2017-07-20
+ @api.model
+ def create(self, vals):
+ aml = []
+ if vals.get('debit_move_id', False):
+ aml.append(vals['debit_move_id'])
+ if vals.get('credit_move_id', False):
+ aml.append(vals['credit_move_id'])
+ # Get value of matched percentage from both move before reconciliating
+ lines = self.env['account.move.line'].browse(aml)
+ if lines[0].account_id.internal_type in ('receivable', 'payable'):
+ percentage_before_rec = lines._get_matched_percentage()
+ # Reconcile
+ res = super(AccountPartialReconcile, self).create(vals)
+ # if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
+ if lines[0].account_id.internal_type in ('receivable', 'payable'):
+ res.create_tax_cash_basis_entry(percentage_before_rec)
+ res._compute_partial_lines()
return res
@api.multi
@@ -1778,3 +1833,20 @@ class AccountFullReconcile(models.Model):
rec.exchange_move_id = False
to_reverse.reverse_moves()
return super(AccountFullReconcile, self).unlink()
+
+ # Do not forwardport in master as of 2017-07-20
+ @api.model
+ def _prepare_exchange_diff_move(self, move_date, company):
+ if not company.currency_exchange_journal_id:
+ raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
+ if not company.income_currency_exchange_account_id.id:
+ raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
+ if not company.expense_currency_exchange_account_id.id:
+ raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
+ res = {'journal_id': company.currency_exchange_journal_id.id}
+ # The move date should be the maximum date between payment and invoice
+ # (in case of payment in advance). However, we should make sure the
+ # move date is not recorded after the end of year closing.
+ if move_date > company.fiscalyear_lock_date:
+ res['date'] = move_date
+ return res
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index 50012321db5..0093915eee5 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -477,3 +477,147 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.credit, 2), line['credit'])
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
+
+ def test_partial_reconcile_currencies(self):
+ # client Account (payable, rsa)
+ # Debit Credit
+ # --------------------------------------------------------
+ # Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
+ # Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
+ # Pay c: 25/0.8 = 31.25 |
+ #
+ # Debit_currency = 100 | Credit currency = 100
+ # Debit = 147.91 | Credit = 166.66
+ # Balance Debit = 18.75
+ # Counterpart Credit goes in Exchange diff
+
+ dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1)
+ account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1)
+
+ self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id,
+ 'default_credit_account_id': self.account_rsa.id})
+ dest_journal_id.write({'default_debit_account_id': self.account_rsa.id,
+ 'default_credit_account_id': self.account_rsa.id})
+ # Setting up rates for USD (main_company is in EUR)
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
+ 'rate': 0.5,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.env.ref('base.main_company').id})
+
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01',
+ 'rate': 0.75,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.env.ref('base.main_company').id})
+
+ self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01',
+ 'rate': 0.80,
+ 'currency_id': self.currency_usd_id,
+ 'company_id': self.env.ref('base.main_company').id})
+
+ # Preparing Invoices (from vendor)
+ invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
+ 'reference_type': 'none',
+ 'currency_id': self.currency_usd_id,
+ 'name': 'invoice to vendor',
+ 'account_id': self.account_rsa.id,
+ 'type': 'in_invoice',
+ 'date_invoice': time.strftime('%Y') + '-' + '07' + '-01',
+ })
+ self.account_invoice_line_model.create({'product_id': self.product.id,
+ 'quantity': 1,
+ 'price_unit': 50,
+ 'invoice_id': invoice_a.id,
+ 'name': 'product that cost ' + str(50),
+ 'account_id': account_expenses.id,
+ })
+
+ invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
+ 'reference_type': 'none',
+ 'currency_id': self.currency_usd_id,
+ 'name': 'invoice to vendor',
+ 'account_id': self.account_rsa.id,
+ 'type': 'in_invoice',
+ 'date_invoice': time.strftime('%Y') + '-' + '08' + '-01',
+ })
+ self.account_invoice_line_model.create({'product_id': self.product.id,
+ 'quantity': 1,
+ 'price_unit': 50,
+ 'invoice_id': invoice_b.id,
+ 'name': 'product that cost ' + str(50),
+ 'account_id': account_expenses.id,
+ })
+
+ invoice_a.action_invoice_open()
+ invoice_b.action_invoice_open()
+
+ # Preparing Payments
+ # One partial for invoice_a (fully assigned to it)
+ payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.env.ref('base.main_company').id,
+ 'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'destination_journal_id': dest_journal_id.id,
+ 'partner_type': 'supplier'})
+
+ # One that will complete the payment of a, the rest goes to b
+ payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 50,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.env.ref('base.main_company').id,
+ 'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'destination_journal_id': dest_journal_id.id,
+ 'partner_type': 'supplier'})
+
+ # The last one will complete the payment of b
+ payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
+ 'amount': 25,
+ 'currency_id': self.currency_usd_id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'company_id': self.env.ref('base.main_company').id,
+ 'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
+ 'partner_id': self.partner_agrolait_id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
+ 'destination_journal_id': dest_journal_id.id,
+ 'partner_type': 'supplier'})
+
+ payment_a.post()
+ payment_b.post()
+ payment_c.post()
+
+ # Assigning payments to invoices
+ debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
+ debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
+ debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
+
+ invoice_a.assign_outstanding_credit(debit_line_a.id)
+ invoice_a.assign_outstanding_credit(debit_line_b.id)
+ invoice_b.assign_outstanding_credit(debit_line_b.id)
+ invoice_b.assign_outstanding_credit(debit_line_c.id)
+
+ # Asserting correctness (only in the payable account)
+ full_reconcile = False
+ for inv in (invoice_a + invoice_b):
+ self.assertTrue(inv.reconciled)
+ for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa):
+ self.assertEqual(aml.amount_residual, 0.0)
+ self.assertEqual(aml.amount_residual_currency, 0.0)
+ self.assertTrue(aml.reconciled)
+ if not full_reconcile:
+ full_reconcile = aml.full_reconcile_id
+ else:
+ self.assertTrue(aml.full_reconcile_id == full_reconcile)
+
+ full_rec_move = full_reconcile.exchange_move_id
+ # Globally check whether the amount is correct
+ self.assertEqual(full_rec_move.amount, 18.75)
+
+ # Checking if the direction of the move is correct
+ full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
+ self.assertEqual(full_rec_payable.balance, 18.75)
diff --git a/addons/account/views/account_config_settings_views.xml b/addons/account/views/account_config_settings_views.xml
index d7aad5c3dfe..9bb0a845201 100644
--- a/addons/account/views/account_config_settings_views.xml
+++ b/addons/account/views/account_config_settings_views.xml
@@ -16,7 +16,7 @@
<form string="Configure Accounting" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py
index f4cafe12319..d4b65d6b020 100644
--- a/addons/account_asset/models/account_invoice.py
+++ b/addons/account_asset/models/account_invoice.py
@@ -108,3 +108,6 @@ class AccountInvoiceLine(models.Model):
elif invoice.type == 'in_invoice':
self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id
super(AccountInvoiceLine, self)._set_additional_fields(invoice)
+
+ def get_invoice_line_account(self, type, product, fpos, company):
+ return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
diff --git a/addons/account_asset/static/tests/account_asset_tests.js b/addons/account_asset/static/tests/account_asset_tests.js
index 8f353f3bf5c..3ae5ac40073 100644
--- a/addons/account_asset/static/tests/account_asset_tests.js
+++ b/addons/account_asset/static/tests/account_asset_tests.js
@@ -125,7 +125,7 @@ QUnit.test('basic rendering', function (assert) {
});
QUnit.test('click events are correctly triggered', function (assert) {
- assert.expect(3);
+ assert.expect(2);
var form = createView({
View: FormView,
@@ -146,8 +146,7 @@ QUnit.test('click events are correctly triggered', function (assert) {
intercepts: {
execute_action: function (event) {
var data = event.data;
- assert.strictEqual(data.model, 'line', "should have correct model");
- assert.deepEqual(data.res_ids, [4], "should have correct res_ids");
+ assert.strictEqual(data.env.model, 'line', "should have correct model");
assert.strictEqual(data.action_data.name, 'create_move',
"should call correct method");
},
diff --git a/addons/base_setup/views/base_config_settings_views.xml b/addons/base_setup/views/base_config_settings_views.xml
index 6d4f9277de1..a0dab58c570 100644
--- a/addons/base_setup/views/base_config_settings_views.xml
+++ b/addons/base_setup/views/base_config_settings_views.xml
@@ -7,7 +7,7 @@
<form string="General Settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div id="business_documents">
diff --git a/addons/event/models/event.py b/addons/event/models/event.py
index f0d6d0312cf..95310a67122 100644
--- a/addons/event/models/event.py
+++ b/addons/event/models/event.py
@@ -316,6 +316,9 @@ class EventEvent(models.Model):
for attendee in self.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)
+ @api.multi
+ def _is_event_registrable(self):
+ return True
class EventRegistration(models.Model):
_name = 'event.registration'
diff --git a/addons/event/views/event_config_settings_views.xml b/addons/event/views/event_config_settings_views.xml
index aba1ec2f607..5d9ea62c0f8 100644
--- a/addons/event/views/event_config_settings_views.xml
+++ b/addons/event/views/event_config_settings_views.xml
@@ -8,7 +8,7 @@
<form String="Event settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Advertising</h2>
diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py
index a0d27bd268f..d1c28b32d2a 100644
--- a/addons/event_sale/models/event.py
+++ b/addons/event_sale/models/event.py
@@ -53,6 +53,12 @@ class Event(models.Model):
})
for ticket in self.event_type_id.event_ticket_ids]
+ @api.multi
+ def _is_event_registrable(self):
+ self.ensure_one()
+ if not self.event_ticket_ids:
+ return True
+ return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active))
class EventTicket(models.Model):
_name = 'event.event.ticket'
diff --git a/addons/gamification/data/goal_base.xml b/addons/gamification/data/goal_base.xml
index 3b983ebd256..de76683ddc0 100644
--- a/addons/gamification/data/goal_base.xml
+++ b/addons/gamification/data/goal_base.xml
@@ -98,7 +98,7 @@
</table>
% else:
<span style="color:#A8A8A8;font-size:13px;">
- The following message contains the current progress for the challenge <strong>${object.name}</strong>.
+ Challenge: <strong>${object.name}</strong>.
</span>
% for line in challenge_lines:
<!-- Header + Button table -->
@@ -113,7 +113,7 @@
% if len(line['goals'])>2:
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:10px;">
<tr><td style="padding:0 30px;">
- <table cellspacing="0" cellpadding="0" width="100%">
+ <table cellspacing="0" cellpadding="0" width="100%" style="table-layout: fixed;">
<tr>
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]]
% for goal in top_goals:
@@ -144,9 +144,10 @@
<div style="height:${heightA}px;">
${extra_div | safe}
<div style="height:55px;">
- <img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="/web/image?model=res.users&amp;field=image_small&amp;id=${goal['user_id']}" alt=""/>
+ % set path = object.env['res.users'].browse(goal['user_id']).partner_id.image_small
+ <img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="data:image/jpeg;base64,${path}" alt="${goal['name']}"/>
</div>
- <div align="center" style ="color:${bgColor};">
+ <div align="center" style ="color:${bgColor};height:20px">
${goal['name']}
</div
</div>
@@ -177,8 +178,8 @@
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:30px;margin-bottom:5px;color:#666666;">
<thead>
<tr style="color:#9A6C8E; font-size:12px;">
- <th style="width:10%;text-align:center;">Rank</th>
- <th style="width:15%;text-align:left;">Name</th>
+ <th style="width:15%;text-align:center;">Rank</th>
+ <th style="width:25%;text-align:left;">Name</th>
<th style="width:30%;text-align:right;">Performance
% if line['suffix']:
(${line['suffix']})
@@ -199,12 +200,12 @@
% set tdColor = 'gray'
% set mutedColor = '#AAAAAA'
% set tdPercentageColor = '#9A6C8E'
- <td width="10%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
+ <td width="15%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
</td>
- <td width="15%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
- <td width="20%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
+ <td width="25%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
+ <td width="30%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
</td>
- <td width="20%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
+ <td width="30%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#DADADA;"></td>
diff --git a/addons/hr_attendance/views/attendance_config_settings_views.xml b/addons/hr_attendance/views/attendance_config_settings_views.xml
index b8c11252fbf..cf72f9f2220 100644
--- a/addons/hr_attendance/views/attendance_config_settings_views.xml
+++ b/addons/hr_attendance/views/attendance_config_settings_views.xml
@@ -7,7 +7,7 @@
<form string="Configuration" class= "oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Check-In/Out</h2>
@@ -64,4 +64,4 @@
<menuitem id="hr_attendance.menu_hr_attendance_settings" name="Configuration" parent="menu_hr_attendance_root"
sequence="99" action="action_hr_attendance_settings" groups="hr_attendance.group_hr_attendance_manager"/>
-</odoo> \ No newline at end of file
+</odoo>
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index cdcf25b491f..ce0054a5636 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -385,7 +385,7 @@ class HrExpenseSheet(models.Model):
_order = "accounting_date desc, id desc"
name = fields.Char(string='Expense Report Summary', required=True)
- expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
+ expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
state = fields.Selection([('submit', 'Submitted'),
('approve', 'Approved'),
('post', 'Posted'),
diff --git a/addons/hr_expense/views/hr_expense_config_settings_views.xml b/addons/hr_expense/views/hr_expense_config_settings_views.xml
index 19078d6071c..897e78837ca 100644
--- a/addons/hr_expense/views/hr_expense_config_settings_views.xml
+++ b/addons/hr_expense/views/hr_expense_config_settings_views.xml
@@ -9,7 +9,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Recording</h2>
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index 26a58ea79ce..643e62b5505 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -392,7 +392,7 @@
<field name="accounting_date"/>
<field name="employee_id"/>
<field name="currency_id" invisible="1"/>
- <field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
+ <field name="total_amount" sum="Total Amount"/>
<field name="state"/>
<field name="message_unread" invisible="1"/>
</tree>
diff --git a/addons/hr_payroll/views/hr_payroll_config_settings_views.xml b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml
index 548ccddcc28..673e35c1992 100644
--- a/addons/hr_payroll/views/hr_payroll_config_settings_views.xml
+++ b/addons/hr_payroll/views/hr_payroll_config_settings_views.xml
@@ -15,7 +15,7 @@
<form string="Configure Payroll" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<field name="module_l10n_fr_hr_payroll" invisible="1"/>
<field name="module_l10n_be_hr_payroll" invisible="1"/>
diff --git a/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml b/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml
index 54499005565..6617b8d4153 100644
--- a/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml
+++ b/addons/hr_recruitment/views/hr_recruitment_config_settings_views.xml
@@ -8,7 +8,7 @@
<form string="Configure HR Recruitment" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Job Posting</h2>
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index d8d07c42fd8..3023fdcea59 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.model
def default_get(self, field_list):
result = super(AccountAnalyticLine, self).default_get(field_list)
- if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'):
+ if 'employee_id' in field_list and result.get('user_id'):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 988fed44727..e9bf605c382 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -571,6 +571,7 @@ class MailTemplate(models.Model):
'name': attachment[0],
'datas_fname': attachment[0],
'datas': attachment[1],
+ 'type': 'binary',
'res_model': 'mail.message',
'res_id': mail.mail_message_id.id,
}
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index c672cab2835..0d23032d051 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -950,13 +950,14 @@ class MailThread(models.AbstractModel):
# Alias: check alias_contact settings
if alias:
+ obj = None
if thread_id:
obj = record_set[0]
- elif alias.alias_parent_thread_id:
+ elif alias.alias_parent_model_id and alias.alias_parent_thread_id:
obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id)
- elif model and hasattr(record_set, '_alias_check_contact'):
+ elif model:
obj = self.env[model]
- else:
+ if not hasattr(obj, '_alias_check_contact'):
obj = self.env['mail.alias.mixin']
check_result = obj._alias_check_contact(message, message_dict, alias)
if check_result is not True:
@@ -1727,6 +1728,7 @@ class MailThread(models.AbstractModel):
data_attach = {
'name': name,
'datas': base64.b64encode(str(content)),
+ 'type': 'binary',
'datas_fname': name,
'description': name,
'res_model': message_data['model'],
diff --git a/addons/mass_mailing/views/mass_mailing_config_settings_views.xml b/addons/mass_mailing/views/mass_mailing_config_settings_views.xml
index d7164d017d7..7c9dd4e0382 100644
--- a/addons/mass_mailing/views/mass_mailing_config_settings_views.xml
+++ b/addons/mass_mailing/views/mass_mailing_config_settings_views.xml
@@ -7,7 +7,7 @@
<form string="Configure Mass Mailing" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Design</h2>
diff --git a/addons/mrp/views/mrp_config_settings_views.xml b/addons/mrp/views/mrp_config_settings_views.xml
index df407343714..5b2ca1593f8 100644
--- a/addons/mrp/views/mrp_config_settings_views.xml
+++ b/addons/mrp/views/mrp_config_settings_views.xml
@@ -8,7 +8,7 @@
<form string="Configure Manufacturing" class= "oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Operations</h2>
diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py
index aab5abe3244..945e24f4308 100644
--- a/addons/pad/models/pad.py
+++ b/addons/pad/models/pad.py
@@ -122,7 +122,7 @@ class PadCommon(models.AbstractModel):
# Update the pad if the `pad_content_field` is modified
for k, field in pycompat.items(self._fields):
- if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field):
+ if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
company = self.env.user.sudo().company_id
myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
path = self[k].split('/p/')[1]
diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js
index 4068ff24ead..1d162bdbc87 100644
--- a/addons/pad/static/src/js/pad.js
+++ b/addons/pad/static/src/js/pad.js
@@ -55,7 +55,7 @@ var FieldPad = AbstractField.extend({
// Guess what we decided...
var url = {};
url.toJSON = _.constant(this.url);
- this._setValue(url);
+ this._setValue(url, {doNotSetDirty: true});
}
return this._super.apply(this, arguments);
diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js
index 5d18e55c1e0..dedf799db66 100644
--- a/addons/pad/static/tests/pad_tests.js
+++ b/addons/pad/static/tests/pad_tests.js
@@ -193,5 +193,41 @@ QUnit.module('pad widget', {
delete FieldPad.prototype.isPadConfigured;
});
+ QUnit.test('pad widget is not considered dirty at edition', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'task',
+ data: this.data,
+ arch:'<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="description" widget="pad"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (!args.method) {
+ return $.when(true);
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ userName: "batman",
+ },
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ var def = form.canBeDiscarded();
+
+ assert.strictEqual($('.modal').length, 0,
+ "should have no confirmation modal opened");
+
+ assert.strictEqual(def.state(), 'resolved',
+ "can be discarded was succesfully resolved");
+ form.destroy();
+ delete FieldPad.prototype.isPadConfigured;
+ });
});
diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py
index 9455f4e1491..e43e0cbd71f 100644
--- a/addons/payment_stripe/models/payment.py
+++ b/addons/payment_stripe/models/payment.py
@@ -15,6 +15,12 @@ _logger = logging.getLogger(__name__)
# changelog https://stripe.com/docs/upgrades#api-changelog
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
+# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
+INT_CURRENCIES = [
+ 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV',
+ 'VND', 'XOF'
+];
+
class PaymentAcquirerStripe(models.Model):
_inherit = 'payment.acquirer'
@@ -84,7 +90,7 @@ class PaymentTransactionStripe(models.Model):
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = {
- 'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
+ 'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100),
'currency': self.currency_id.name,
'metadata[reference]': self.reference
}
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 159bdc14214..3a736330657 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -1,6 +1,14 @@
odoo.define('payment_stripe.stripe', function(require) {
"use strict";
+
var ajax = require('web.ajax');
+ // The following currencies are integer only, see
+ // https://stripe.com/docs/currencies#zero-decimal
+ var int_currencies = [
+ 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí',
+ 'RWF', 'KRW', 'VUV', 'VND', 'XOF'
+ ];
+
var handler = StripeCheckout.configure({
key: $("input[name='stripe_key']").val(),
image: $("input[name='stripe_image']").val(),
@@ -50,19 +58,31 @@ odoo.define('payment_stripe.stripe', function(require) {
e.preventDefault();
if ($('.o_website_payment').length !== 0) {
+ var currency = $("input[name='currency']").val();
+ var amount = parseFloat($("input[name='amount']").val() || '0.0');
+ if (!_.contains(int_currencies, currency)) {
+ amount = amount*100;
+ }
+
ajax.jsonRpc('/website_payment/transaction', 'call', {
reference: $("input[name='invoice_num']").val(),
- amount: $("input[name='amount']").val(),
- currency_id: $("input[name='currency_id']").val(),
+ amount: amount,
+ currency_id: currency,
acquirer_id: acquirer_id
})
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
- currency: $("input[name='currency']").val(),
- amount: $("input[name='amount']").val()*100
+ currency: currency,
+ amount: amount,
});
} else {
+ var currency = $("input[name='currency']").val();
+ var amount = parseFloat($("input[name='amount']").val() || '0.0');
+ if (!_.contains(int_currencies, currency)) {
+ amount = amount*100;
+ }
+
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
so_id: so_id,
access_token: access_token
@@ -71,8 +91,8 @@ odoo.define('payment_stripe.stripe', function(require) {
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
- currency: $("input[name='currency']").val(),
- amount: $("input[name='amount']").val()*100
+ currency: currency,
+ amount: amount,
});
});
}
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index f848a64cbe1..5e059a41cfc 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -870,7 +870,7 @@ class PosOrderLine(models.Model):
def create(self, values):
if values.get('order_id') and not values.get('name'):
# set name based on the sequence specified on the config
- config_id = self.env['pos.order'].browse(values['order_id']).session_id.config_id.id
+ config_id = self.order_id.browse(values['order_id']).session_id.config_id.id
# HACK: sequence created in the same transaction as the config
# cf TODO master is pos.config create
# remove me saas-15
diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py
index ac9831e343d..b621d5ba16f 100644
--- a/addons/point_of_sale/tests/test_point_of_sale_flow.py
+++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py
@@ -397,12 +397,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.')
# I generate an invoice from the order
- self.invoice = self.pos_order_pos1.action_pos_order_invoice()
+ res = self.pos_order_pos1.action_pos_order_invoice()
+ self.assertIn('res_id', res, "No invoice created")
# I test that the total of the attached invoice is correct
- self.amount_total = self.pos_order_pos1.amount_total
+ invoice = self.env['account.invoice'].browse(res['res_id'])
self.assertEqual(
- float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
+ float_compare(invoice.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
"""In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports"""
diff --git a/addons/point_of_sale/views/pos_config_settings_views.xml b/addons/point_of_sale/views/pos_config_settings_views.xml
index 8edc687273a..7cb3922e444 100644
--- a/addons/point_of_sale/views/pos_config_settings_views.xml
+++ b/addons/point_of_sale/views/pos_config_settings_views.xml
@@ -7,7 +7,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Point of Sale</h2>
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index be9ea3db7c6..638e898aed8 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -295,7 +295,9 @@ class ProductProduct(models.Model):
@api.model
def create(self, vals):
product = super(ProductProduct, self.with_context(create_product_product=True)).create(vals)
- product._set_standard_price(vals.get('standard_price', 0.0))
+ # When a unique variant is created from tmpl then the standard price is set by _set_standard_price
+ if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1):
+ product._set_standard_price(vals.get('standard_price') or 0.0)
return product
@api.multi
diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py
index 41e9b79110b..a0457f021ba 100644
--- a/addons/product/models/product_pricelist.py
+++ b/addons/product/models/product_pricelist.py
@@ -113,8 +113,7 @@ class Pricelist(models.Model):
uom_id = self._context['uom']
if uom_id:
# rebrowse with uom if given
- product_ids = [item[0].id for item in products_qty_partner]
- products = self.env['product.product'].with_context(uom=uom_id).browse(product_ids)
+ products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
else:
products = [item[0] for item in products_qty_partner]
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index efb701a6092..ccfbcf28b96 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -172,7 +172,9 @@ class ProductTemplate(models.Model):
# Support context pricelists specified as display_name or ID for compatibility
if isinstance(pricelist_id_or_name, basestring):
- pricelist = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
+ pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
+ if pricelist_data:
+ pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0])
elif isinstance(pricelist_id_or_name, pycompat.integer_types):
pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)
@@ -288,7 +290,7 @@ class ProductTemplate(models.Model):
tools.image_resize_images(vals)
template = super(ProductTemplate, self).create(vals)
if "create_product_product" not in self._context:
- template.create_variant_ids()
+ template.with_context(create_from_tmpl=True).create_variant_ids()
# This is needed to set given values to first variant after creation
related_vals = {}
diff --git a/addons/project/views/project_config_settings_views.xml b/addons/project/views/project_config_settings_views.xml
index 8ec189d2e02..bff22fc3803 100644
--- a/addons/project/views/project_config_settings_views.xml
+++ b/addons/project/views/project_config_settings_views.xml
@@ -8,7 +8,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Task Management</h2>
diff --git a/addons/purchase/views/purchase_config_settings_views.xml b/addons/purchase/views/purchase_config_settings_views.xml
index a1e253ca6e5..92ba3057d9a 100644
--- a/addons/purchase/views/purchase_config_settings_views.xml
+++ b/addons/purchase/views/purchase_config_settings_views.xml
@@ -7,7 +7,7 @@
<form string="Configure Purchases" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="po_double_validation" invisible="1"/>
diff --git a/addons/sales_team/views/sale_config_settings_views.xml b/addons/sales_team/views/sale_config_settings_views.xml
index 4e2c4625ef0..5ebea57ebb9 100644
--- a/addons/sales_team/views/sale_config_settings_views.xml
+++ b/addons/sales_team/views/sale_config_settings_views.xml
@@ -8,7 +8,7 @@
<form string="Configure Sales" class="oe_form_configuration" name="sale_config_form">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2 id="integration_h2" invisible="1">Integrations</h2>
@@ -84,4 +84,4 @@
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
-</odoo> \ No newline at end of file
+</odoo>
diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py
index 3e7fccf5b5f..8b7e70f80f7 100644
--- a/addons/stock/models/stock_location.py
+++ b/addons/stock/models/stock_location.py
@@ -69,7 +69,7 @@ class Location(models.Model):
_sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !')]
@api.one
- @api.depends('name', 'location_id')
+ @api.depends('name', 'location_id.name')
def _compute_complete_name(self):
""" Forms complete name of location from parent location to child location. """
name = self.name
diff --git a/addons/stock/views/stock_config_settings_views.xml b/addons/stock/views/stock_config_settings_views.xml
index c86a9d08394..3719c4ab3f3 100644
--- a/addons/stock/views/stock_config_settings_views.xml
+++ b/addons/stock/views/stock_config_settings_views.xml
@@ -8,7 +8,7 @@
<form string="Configure Warehouse" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index fd2d596a4b2..9a29c2ec3e8 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -684,7 +684,7 @@ class Database(http.Controller):
request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception as e:
- error = "Database creation error: %s" % str(e) or repr(e)
+ error = "Database creation error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
@@ -695,7 +695,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database duplication error: %s" % str(e) or repr(e)
+ error = "Database duplication error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
@@ -705,7 +705,7 @@ class Database(http.Controller):
request._cr = None # dropping a database leads to an unusable cursor
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database deletion error: %s" % str(e) or repr(e)
+ error = "Database deletion error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
@@ -723,7 +723,7 @@ class Database(http.Controller):
return response
except Exception as e:
_logger.exception('Database.backup')
- error = "Database backup error: %s" % str(e) or repr(e)
+ error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@@ -733,7 +733,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Database restore error: %s" % str(e) or repr(e)
+ error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
@@ -742,7 +742,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new])
return http.local_redirect('/web/database/manager')
except Exception as e:
- error = "Master password update error: %s" % str(e) or repr(e)
+ error = "Master password update error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/list', type='json', auth='none')
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index a4a1d65f7b6..5962bb28acb 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -20,7 +20,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
custom_events: {
execute_action: function(event) {
var data = event.data;
- this.do_execute_action(data.action_data, data.model, data.res_ids, data.on_closed)
+ this.do_execute_action(data.action_data, data.env, data.on_closed)
.then(data.on_success, data.on_fail);
},
search: function(event) {
@@ -554,16 +554,19 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
* @param {string} [action_data.effect] if given, a visual effect (a
* rainbowman by default) will be displayed when the action is complete,
* with the string (evaluated) given as options.
- * @param {DataSet} dataset a dataset object used to communicate with the server
- * @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set)
+ * @param {Object} env
+ * @param {string} env.model the model of the record(s) triggering the action
+ * @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered
+ * @param {integer} [env.currentID] the id of the record triggering the action
+ * @param {Object} [env.context] a context to pass to the action
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
*/
- do_execute_action: function (action_data, model, res_ids, on_closed) {
+ do_execute_action: function (action_data, env, on_closed) {
var self = this;
var result_handler = on_closed || function () {};
- var context = new Context(this.env.context, action_data.context || {});
+ var context = new Context(env.context, action_data.context || {});
// OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
- var record_id = res_ids && res_ids[0] || null;
+ var recordID = env.currentID || null;
// response handler
var handler = function (action) {
@@ -585,11 +588,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}))
);
ncontext.add(action_data.context || {});
- ncontext.add({active_model: self.env.modelName});
- if (res_ids) {
+ ncontext.add({active_model: env.model});
+ if (recordID) {
ncontext.add({
- active_id: record_id,
- active_ids: res_ids,
+ active_id: recordID,
+ active_ids: [recordID],
});
}
ncontext.add(action.context || {});
@@ -611,7 +614,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
if (action_data.special) {
return handler({"type":"ir.actions.act_window_close"});
} else if (action_data.type === "object") {
- var args = res_ids ? [res_ids] : [this.env.ids];
+ var args = recordID ? [[recordID]] : [env.resIDs];
if (action_data.args) {
try {
// Warning: quotes and double quotes problem due to json and xml clash
@@ -623,13 +626,13 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
}
args.push(context);
- var dataset = new data.DataSet(this, model, this.env.context);
+ var dataset = new data.DataSet(this, env.model, env.context);
return dataset.call_button(action_data.name, args).then(handler);
} else if (action_data.type === "action") {
return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
- active_model: this.env.modelName,
- active_ids: this.env.ids,
- active_id: record_id,
+ active_model: env.model,
+ active_ids: env.resIDs,
+ active_id: recordID,
})).then(handler);
}
},
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 64049d8b1ec..0dce6ec2bb4 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -219,10 +219,16 @@ var FieldMany2One = AbstractField.extend({
/**
* @private
* @param {string} name
+ * @returns {Deferred} resolved after the name_create or when the slowcreate
+ * modal is closed.
*/
_quickCreate: function (name) {
var self = this;
- var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
+ var def = $.Deferred();
+ var slowCreate = function () {
+ var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
+ dialog.on('closed', self, def.resolve.bind(def));
+ };
if (this.nodeOptions.quick_create) {
this.trigger_up('mutexify', {
action: function () {
@@ -235,12 +241,16 @@ var FieldMany2One = AbstractField.extend({
if (self.mode === "edit") {
self.reinitialize({id: result[0], display_name: result[1]});
}
- }, slowCreate);
+ def.resolve();
+ }).fail(function () {
+ slowCreate();
+ });
},
});
} else {
slowCreate();
}
+ return def;
},
/**
* @private
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 8de118b29b7..644ba8ac985 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -29,6 +29,7 @@ var ajax = require('web.ajax');
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractController = require('web.AbstractController');
+var Context = require('web.Context');
var AbstractView = Class.extend({
// name displayed in view switchers
@@ -152,9 +153,9 @@ var AbstractView = Class.extend({
});
},
/**
- * Returns the view model or create an instance of it if there is noney
+ * Returns the view model or create an instance of it if none
*
- * @param {Widget} parent the parent of the model
+ * @param {Widget} parent the parent of the model, if it has to be created
* @return {Object} instance of the view model
*/
getModel: function (parent) {
@@ -167,7 +168,7 @@ var AbstractView = Class.extend({
/**
* Returns the a new view renderer instance
*
- * @param {Widget} parent the parent of the model
+ * @param {Widget} parent the parent of the model, if it has to be created
* @param {Object} state the information related to the rendered view
* @return {Object} instance of the view renderer
*/
@@ -239,7 +240,7 @@ var AbstractView = Class.extend({
* Loads the subviews for x2many fields when they are not inline
*
* @private
- * @param {Widget} parent the parent of the model
+ * @param {Widget} parent the parent of the model, if it has to be created
* @returns {Deferred}
*/
_loadSubviews: function (parent) {
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 3a47f65ab03..6ba9dbea477 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -232,13 +232,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return self.isDestroyed() ? $.when() : self.reload();
};
record = record || this.model.get(this.handle);
- var recordID = record.data.id;
+
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
- context: record.getContext({additionalContext: attrs.context || {}}),
+ context: record.getContext({additionalContext: attrs.context || {}}),
}),
- model: record.model,
- res_ids: [recordID],
+ env: {
+ context: record.getContext(),
+ currentID: record.data.id,
+ model: record.model,
+ resIDs: record.res_ids,
+ },
on_closed: function (reason) {
if (!_.isObject(reason)) {
reload(reason);
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 04fe384afdc..8755805f993 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -649,7 +649,7 @@ var BasicModel = AbstractModel.extend({
var element = this.localData[id];
if (element.type === 'record') {
- if ('currentId' in options && !options.currentId) {
+ if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
var params = {
context: element.context,
fieldsInfo: element.fieldsInfo,
@@ -2310,12 +2310,20 @@ var BasicModel = AbstractModel.extend({
});
list = this._applyX2ManyOperations(list);
if (type === 'many2many' || list._forceM2MLink) {
+ var relRecordCreated = _.filter(relRecordAdded, function (rec) {
+ return typeof rec.res_id === 'string';
+ });
+ var realIDs = _.difference(list.res_ids, _.pluck(relRecordCreated, 'res_id'));
// deliberately generate a single 'replace' command instead
// of a 'delete' and a 'link' commands with the exact diff
// because 1) performance-wise it doesn't change anything
// and 2) to guard against concurrent updates (policy: force
// a complete override of the actual value of the m2m)
- commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids));
+ commands[fieldName].push(x2ManyCommands.replace_with(realIDs));
+ _.each(relRecordCreated, function (relRecord) {
+ var changes = self._generateChanges(relRecord, options);
+ commands[fieldName].push(x2ManyCommands.create(changes));
+ });
// generate update commands for records that have been
// updated (it may happen with editable lists)
_.each(relRecordUpdated, function (relRecord) {
@@ -2435,6 +2443,12 @@ var BasicModel = AbstractModel.extend({
*/
_getDomain: function (element, options) {
if (options && options.fieldName) {
+ if (element._domains[options.fieldName]) {
+ return Domain.prototype.stringToArray(
+ element._domains[options.fieldName],
+ this._getEvalContext(element, true)
+ );
+ }
var viewType = options.viewType || element.viewType;
var fieldInfo = element.fieldsInfo[viewType][options.fieldName];
if (fieldInfo && fieldInfo.domain) {
@@ -2697,6 +2711,7 @@ var BasicModel = AbstractModel.extend({
var dataPoint = {
_cache: type === 'list' ? {} : undefined,
_changes: null,
+ _domains: {},
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
@@ -2870,6 +2885,9 @@ var BasicModel = AbstractModel.extend({
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
+ if (r._changes[name] === null) {
+ continue;
+ }
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
var rec = self._makeDataPoint({
@@ -3021,12 +3039,7 @@ var BasicModel = AbstractModel.extend({
record._warning = true;
}
if (result.domain) {
- var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
- for (var fieldName in result.domain) {
- if (fieldsInfo[fieldName]) {
- fieldsInfo[fieldName].domain = result.domain[fieldName];
- }
- }
+ record._domains = _.extend(record._domains, result.domain);
}
return self._applyOnChange(result.value, record).then(function () {
return result;
@@ -3083,7 +3096,9 @@ var BasicModel = AbstractModel.extend({
*/
_readGroup: function (list) {
var self = this;
- var fields = _.uniq(list.getFieldNames().concat(list.groupedBy));
+ var groupByField = list.groupedBy[0];
+ var rawGroupBy = groupByField.split(':')[0];
+ var fields = _.uniq(list.getFieldNames().concat(rawGroupBy));
return this._rpc({
model: list.model,
method: 'read_group',
@@ -3094,8 +3109,6 @@ var BasicModel = AbstractModel.extend({
lazy: true,
})
.then(function (groups) {
- var groupByField = list.groupedBy[0];
- var rawGroupBy = groupByField.split(':')[0];
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 4f5719063ff..5fb79cfa3a0 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -322,10 +322,11 @@ return AbstractRenderer.extend({
element.addClass($render.attr('class'));
var display_hour = '';
if (!event.allDay) {
- display_hour = (event.start.format('HH:mm') === '00:00' ? event.r_start.format('HH:mm') : event.start.format('HH:mm')) + ' - ' +
- (event.end && event.end.format('HH:mm') !== '00:00' ? event.end.format('HH:mm') : event.r_end.format('HH:mm'));
+ var start = event.r_start || event.start;
+ var end = event.r_end || event.end;
+ display_hour = start.format('HH:mm') + ' - ' + end.format('HH:mm');
if (display_hour === '00:00 - 00:00') {
- display_hour = _t('All the day');
+ display_hour = _t('All day');
}
}
element.find('.fc-content .fc-time').text(display_hour);
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index cdfe2cb8d5d..0718b4e202e 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -217,7 +217,7 @@ var FormController = BasicController.extend({
*
* @private
* @override method from field manager mixin
- * @param {string} id
+ * @param {string} id - id of the previously changed record
* @returns {Deferred}
*/
_confirmSave: function (id) {
@@ -228,15 +228,21 @@ var FormController = BasicController.extend({
return this._setMode('readonly');
}
} else {
- // a subrecord changed, so update the corresponding relational field
+ // A subrecord has changed, so update the corresponding relational field
// i.e. the one whose value is a record with the given id or a list
// having a record with the given id in its data
var record = this.model.get(this.handle);
- var fieldsChanged = _.findKey(record.data, function (d) {
- return _.isObject(d) &&
- (d.id === id || _.findWhere(d.data, {id: id}));
- });
- return this.renderer.confirmChange(record, record.id, [fieldsChanged]);
+
+ // Callback function which returns true
+ // if a value recursively contains a record with the given id.
+ // This will be used to determine the list of fields to reload.
+ var containsChangedRecord = function (value) {
+ return _.isObject(value) &&
+ (value.id === id || _.find(value.data, containsChangedRecord));
+ };
+
+ var changedFields = _.findKey(record.data, containsChangedRecord);
+ return this.renderer.confirmChange(record, record.id, [changedFields]);
}
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 2cf6a94285e..6870dbbbe24 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -237,8 +237,12 @@ var KanbanController = BasicController.extend({
}
this.trigger_up('execute_action', {
action_data: attrs,
- model: record.model,
- res_ids: [record.res_id],
+ env: {
+ context: record.getContext(),
+ currentID: record.res_id,
+ model: record.model,
+ resIDs: record.res_ids,
+ },
on_closed: function () {
self.model.reload(record.id).then(function (db_id) {
var data = self.model.get(db_id);
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 3977ac0df87..71402fc92ec 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -240,7 +240,7 @@ var KanbanModel = BasicModel.extend({
*/
_readTooltipFields: function (list) {
var self = this;
- var groupedByField = list.fields[list.groupedBy[0]];
+ var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') {
return $.when();
}
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 295266db276..6e69c0217b2 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -281,15 +281,19 @@ var ListRenderer = BasicRenderer.extend({
this._registerModifiers(node, record, $button);
- // TODO this should be moved to a handler
- var self = this;
- $button.on("click", function (e) {
- e.stopPropagation();
- self.trigger_up('button_clicked', {
- attrs: node.attrs,
- record: record,
+ if (record.res_id) {
+ // TODO this should be moved to a handler
+ var self = this;
+ $button.on("click", function (e) {
+ e.stopPropagation();
+ self.trigger_up('button_clicked', {
+ attrs: node.attrs,
+ record: record,
+ });
});
- });
+ } else {
+ $button.prop('disabled', true);
+ }
return $button;
},
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 0ad9aa2a416..29f8666455a 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -922,6 +922,140 @@ QUnit.module('relational_fields', {
});
});
+ QUnit.test('list in form: create with one2many with many2one', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({p: [[0, 0, {display_name: 'new record'}]]});
+ } else if (args.method === 'name_get') {
+ // This should not be called at all and thus is not accounted for
+ // in the assert.expect. If this is called, you broke this test.
+ assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'),
+ "should not call name_get for the m2o inside o2m which has no value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
+ "should have created the new record in the o2m with the correct name");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: default_get with x2many create', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges.timmy = function (obj) {
+ assert.deepEqual(
+ obj.timmy,
+ [
+ [6, false, []],
+ [0, false, {display_name: 'brandon is the new timmy', name: 'brandon'}]
+ ],
+ "should have properly created the x2many command list");
+ obj.int_field = obj.timmy.length;
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="timmy">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'default_get') {
+ return $.when({timmy: [[0, 0, {display_name: 'brandon is the new timmy', name: 'brandon'}]]});
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy',
+ "should have created the new record in the m2m with the correct name");
+ assert.strictEqual($('input.o_field_integer').val(), '2',
+ "should have called and executed the onchange properly");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: call button in sub view', function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/get_formview_id') {
+ return $.when(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ execute_action: function (event) {
+ assert.strictEqual(event.data.env.model, 'product',
+ 'should call with correct model in env');
+ assert.strictEqual(event.data.env.currentID, 37,
+ 'should call with correct currentID in env');
+ assert.deepEqual(event.data.env.resIDs, [37],
+ 'should call with correct resIDs in env');
+ },
+ },
+ archs: {
+ 'product,false,form': '<form string="Partners">' +
+ '<header>' +
+ '<button name="action" type="action" string="Just do it !"/>' +
+ '<button name="object" type="object" string="Just don\'t do it !"/>' +
+ '<field name="display_name"/>' +
+ '</header>' +
+ '</form>',
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('td.o_data_cell:first').click(); // edit first one2many line
+ form.$('.o_external_button').click(); // open product sub view in modal
+ $('button:contains("Just do it !")').click(); // click on action button
+ $('button:contains("Just don\'t do it !")').click(); // click on object button
+
+ form.destroy();
+ });
+
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
@@ -1418,6 +1552,115 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('many2one in one2many: domain updated by an onchange', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ trululu: function () {},
+ };
+
+ var domain = [];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="bttom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return $.when({
+ domain: {
+ trululu: domain,
+ },
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // add a first row with a specific domain for the m2o
+ domain = [['id', 'in', [10]]]; // domain for subrecord 1
+ form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
+ form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
+
+ // add a second row with another domain for the m2o
+ domain = [['id', 'in', [5]]]; // domain for subrecord 2
+ form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
+ form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
+
+ // check again the first row to ensure that the domain hasn't change
+ domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept
+ form.$('.o_data_row:first .o_data_cell').click();
+ form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
+
+ form.destroy();
+ });
+
+ QUnit.test('updating a many2one from a many2many', function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.records[1].turtle_trululu = 1;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form string="Trululu"><field name="display_name"/></form>',
+ },
+ 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);
+ },
+ });
+
+ // Opening the modal
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_row td:contains(first record)').click();
+ form.$('.o_external_button').click();
+ assert.strictEqual($('.modal').length, 1,
+ "should have one modal in body");
+
+ // Changing the 'trululu' value
+ $('.modal input[name="display_name"]').val('test').trigger('input');
+ $('.modal button.btn-primary').click();
+
+ // Test whether the value has changed
+ assert.strictEqual($('.modal').length, 0,
+ "the modal should be closed");
+ assert.equal(form.$('.o_data_cell:contains(test)').text(), 'test',
+ "the partner name should have been updated to 'test'");
+
+ form.destroy();
+ });
+
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -3847,9 +4090,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
- assert.deepEqual(event.data.res_ids, [2],
+ assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
- assert.strictEqual(event.data.model, 'partner',
+ assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
@@ -3891,9 +4134,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
- assert.deepEqual(event.data.res_ids, [2],
+ assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
- assert.strictEqual(event.data.model, 'partner',
+ assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 53577d90e40..8c23fc4f97c 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -956,6 +956,30 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('group on date field with magic grouping method', function (assert) {
+ assert.expect(1);
+
+ this.params.fieldNames = ['foo'];
+ this.params.groupedBy = ['date:month'];
+ this.params.res_id = undefined;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.fields, ['foo', 'date'],
+ "should have correctly trimmed the magic grouping info from the field name");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ model.load(this.params);
+ model.destroy();
+ });
+
+
QUnit.test('read group when grouped by a selection field', function (assert) {
assert.expect(5);
@@ -1949,4 +1973,39 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('reload a new record', function (assert) {
+ assert.expect(6);
+
+ this.params.context = {};
+ this.params.fieldNames = ['product_id', 'category', 'product_ids'];
+ this.params.res_id = undefined;
+ this.params.type = 'record';
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (recordID) {
+ model.reload(recordID).then(function (recordID) {
+ assert.verifySteps(['default_get', 'default_get'],
+ "two default_get RPCs should have been done");
+ var record = model.get(recordID);
+ assert.strictEqual(record.data.product_id, false,
+ "m2o default value should be false");
+ assert.deepEqual(record.data.product_ids.data, [],
+ "o2m default should be []");
+ assert.deepEqual(record.data.category.data, [],
+ "m2m default should be []");
+ });
+ });
+
+ model.destroy();
+ });
+
+
});});
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 31b1babc9b3..6aa01daac5a 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -789,7 +789,7 @@ QUnit.module('Views', {
assert.strictEqual($event1Avatars.find('span').length, 0,
"should not have a span for more attendees since there is only one");
- var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars');
+ var $event2Avatars = calendar.$('.fc-event:contains(All day) .o_calendar_avatars');
assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars");
assert.strictEqual($event2Avatars.find('span').text(), '+2',
"should indicate there are 2 more attendees that we don't show");
@@ -1034,6 +1034,46 @@ QUnit.module('Views', {
"should display 5 events");
calendar.destroy();
});
+
+ QUnit.test('events starting at midnight', function (assert) {
+ assert.expect(2);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: '<calendar mode="week" date_start="start"/>',
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ // Click on Tuesday 12am
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body');
+ var top = calendar.$('.fc-axis:contains(12am)').offset().top + 5;
+ var left = calendar.$('.fc-day:eq(2)').offset().left + 5;
+ try {
+ testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
+ testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
+ } catch (e) {
+ calendar.destroy();
+ $view.remove();
+ throw new Error('The test failed to simulate a click on the screen.' +
+ 'Your screen is probably too small or your dev tools are open.');
+ }
+ assert.ok($('.modal-dialog.modal-sm').length,
+ "should open the quick create dialog");
+
+ // Creating the event
+ $('.modal-body input:first').val('new event in quick create').trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click').trigger('click');
+ assert.strictEqual(calendar.$('.fc-event:contains(new event in quick create)').length, 1,
+ "should display the new record");
+
+ calendar.destroy();
+ $view.remove();
+ });
});
});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 7eed1a2d100..26d15e33c2a 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -903,7 +903,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.strictEqual(event.data.action_data.name, "post",
"should trigger execute_action with correct method name");
- assert.deepEqual(event.data.res_ids, [2], "should have correct id in event data");
+ assert.deepEqual(event.data.env.currentID, 2, "should have correct id in event data");
event.data.on_success();
event.data.on_closed();
});
@@ -956,7 +956,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
- assert.deepEqual(event.data.res_ids, [resID],
+ assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -1006,7 +1006,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
- assert.deepEqual(event.data.res_ids, [resID],
+ assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -3597,8 +3597,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
- assert.deepEqual(e.data.action_data.context, {test: 2},
- "button context should have been evaluated and given to the action, without previous context");
+ assert.deepEqual(e.data.action_data.context, {
+ 'test': 2
+ }, "button context should have been evaluated and given to the action, with magicc without previous context");
},
},
});
@@ -3631,8 +3632,8 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
- assert.deepEqual(e.data.action_data.context, {},
- "button context should have been evaluated and given to the action, without previous context");
+ assert.deepEqual(e.data.action_data.context, {
+ }, "button context should have been evaluated and given to the action, with magic keys but without previous context");
},
},
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index f0370ac7cf8..2247a64fa3c 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -984,7 +984,7 @@ QUnit.module('Views', {
});
QUnit.test('list view, editable, without data', function (assert) {
- assert.expect(9);
+ assert.expect(11);
this.data.foo.records = [];
@@ -998,6 +998,7 @@ QUnit.module('Views', {
'<field name="date"/>' +
'<field name="m2o"/>' +
'<field name="foo"/>' +
+ '<button type="object" icon="fa-plus-square" name="method"/>' +
'</tree>',
viewOptions: {
action: {
@@ -1033,7 +1034,14 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
"the date field td should not have any content");
+ assert.strictEqual(list.$('.o_list_button button').prop('disabled'), true,
+ "buttons should be disabled while the record is not yet created");
+
list.$buttons.find('.o_list_button_save').click();
+
+ assert.strictEqual(list.$('.o_list_button button').prop('disabled'), false,
+ "buttons should not be disabled once the record is created");
+
list.destroy();
});
@@ -1163,9 +1171,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (event) {
- assert.deepEqual(event.data.res_ids, [1],
+ assert.deepEqual(event.data.env.currentID, 1,
'should call with correct id');
- assert.strictEqual(event.data.model, 'foo',
+ assert.strictEqual(event.data.env.model, 'foo',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'button_action',
"should call correct method");
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
index 06053dd1cea..6b167c59edd 100644
--- a/addons/web_editor/static/src/js/backend.js
+++ b/addons/web_editor/static/src/js/backend.js
@@ -383,7 +383,7 @@ var FieldTextHtml = AbstractField.extend({
* @override
*/
commitChanges: function () {
- if (!this.loaded) {
+ if (!this.loaded || this.mode === 'readonly') {
return;
}
// switch to WYSIWYG mode if currently in code mode to get all changes
diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js
index 283931e9b0d..41ada113cba 100644
--- a/addons/web_editor/static/tests/web_editor_tests.js
+++ b/addons/web_editor/static/tests/web_editor_tests.js
@@ -188,4 +188,34 @@ QUnit.test('field htmlsimple does not crash when commitChanges is called in mode
form.destroy();
});
+QUnit.test('html_frame does not crash when saving in readonly', function (assert) {
+ // The 'Save' action may be triggered even in readonly (e.g. when clicking
+ // on a button in the form view)
+ assert.expect(0);
+
+ var form = testUtils.createView({
+ View: FormView,
+ model: 'mass.mailing',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (_.str.startsWith(route, '/test')) {
+ // manually call the callback to simulate that the iframe has
+ // been correctly loaded
+ window.odoo[$.deparam(route).callback + '_content'].call();
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.saveRecord(); // before the fix done in this commit, it crashed here
+ form.destroy();
+});
+
});
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
index 0a1cd2e2923..234a43463dd 100644
--- a/addons/website/static/src/js/website.snippets.animation.js
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -416,8 +416,9 @@ animation.registry.media_video = animation.Class.extend({
if (!this.$target.has('> iframe').length) {
var editor = '<div class="css_editable_mode_display">&nbsp;</div>';
var size = '<div class="media_iframe_video_size">&nbsp;</div>';
- this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
+ this.$target.html(editor+size);
}
+ this.$target.html(this.$target.html()+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
return this._super.apply(this, arguments);
},
});
diff --git a/addons/website/views/website_config_settings_views.xml b/addons/website/views/website_config_settings_views.xml
index 7b11f25fe13..829b1f3e999 100644
--- a/addons/website/views/website_config_settings_views.xml
+++ b/addons/website/views/website_config_settings_views.xml
@@ -9,7 +9,7 @@
<form string="Configure Website" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
- <button string="Cancel" type="object" name="cancel" class="oe_link"/>
+ <button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div>
diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py
index 00e51627c0c..cd020a74372 100644
--- a/addons/website_event/controllers/main.py
+++ b/addons/website_event/controllers/main.py
@@ -170,6 +170,7 @@ class WebsiteEventController(http.Controller):
'event': event,
'main_object': event,
'range': range,
+ 'registrable': event._is_event_registrable()
}
return request.render("website_event.event_description_full", values)
@@ -226,7 +227,7 @@ class WebsiteEventController(http.Controller):
def registration_new(self, event, **post):
tickets = self._process_tickets_details(post)
if not tickets:
- return request.redirect("/event/%s" % slug(event))
+ return False
return request.env['ir.ui.view'].render_template("website_event.registration_attendee_details", {'tickets': tickets, 'event': event})
def _process_registration_details(self, details):
diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml
index a819d647df6..0b5da64a423 100644
--- a/addons/website_event/views/event_templates.xml
+++ b/addons/website_event/views/event_templates.xml
@@ -311,7 +311,8 @@
<template id="event_description_full">
<t t-call="website_event.event_details">
<div class="col-md-8">
- <t t-call="website_event.registration_template"/>
+ <div t-if="event.state == 'done' or not registrable" class="alert alert-info">Registration for this event is now closed</div>
+ <t t-if="event.state != 'done' and registrable" t-call="website_event.registration_template"/>
<div class="clearfix"/>
<hr/>
<div itemprop="description" t-field="event.description"></div>
diff --git a/addons/website_event_sale/controllers/main.py b/addons/website_event_sale/controllers/main.py
index 26ce63962c7..324fa1026ed 100644
--- a/addons/website_event_sale/controllers/main.py
+++ b/addons/website_event_sale/controllers/main.py
@@ -12,12 +12,7 @@ class WebsiteEventSaleController(WebsiteEventController):
@http.route(['/event/<model("event.event"):event>/register'], type='http', auth="public", website=True)
def event_register(self, event, **post):
event = event.with_context(pricelist=request.website.get_current_pricelist().id)
- values = {
- 'event': event,
- 'main_object': event,
- 'range': range,
- }
- return request.render("website_event.event_description_full", values)
+ return super(WebsiteEventSaleController, self).event_register(event, **post)
def _process_tickets_details(self, data):
ticket_post = {}
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 05abe7627b1..8f02d4a0117 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -357,7 +357,8 @@ class WebsiteSale(http.Controller):
values['suggested_products'] = _order._cart_accessories()
if post.get('type') == 'popover':
- return request.render("website_sale.cart_popover", values)
+ # force no-cache so IE11 doesn't cache this XHR
+ return request.render("website_sale.cart_popover", values, headers={'Cache-Control': 'no-cache'})
return request.render("website_sale.cart", values)
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 806b215ed02..0ffb20d9493 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1166,11 +1166,11 @@
<div class="clearfix" />
<t t-if="mode == ('new', 'billing')">
<div t-attf-class="form-group #{error.get('company_name') and 'has-error' or ''} col-md-6">
- <label class="control-label font-weight-normal" for="company_name">Company Name</label>
+ <label class="control-label font-weight-normal label-optional" for="company_name">Company Name</label>
<input type="text" name="company_name" class="form-control" t-att-value="'company_name' in checkout and checkout['company_name']" />
</div>
<div t-attf-class="form-group #{error.get('vat') and 'has-error' or ''} col-md-6 div_vat">
- <label class="control-label font-weight-normal" for="vat">TIN / VAT </label>
+ <label class="control-label font-weight-normal label-optional" for="vat">TIN / VAT </label>
<input type="text" name="vat" class="form-control" t-att-value="'vat' in checkout and checkout['vat']" />
</div>
</t>
@@ -1180,14 +1180,14 @@
<input type="text" name="street" class="form-control" t-att-value="'street' in checkout and checkout['street']" />
</div>
<div t-attf-class="form-group #{error.get('street2') and 'has-error' or ''} col-md-12 div_street2">
- <label class="control-label" for="street2">Street 2</label>
+ <label class="control-label label-optional" for="street2">Street 2</label>
<input type="text" name="street2" class="form-control" t-att-value="'street2' in checkout and checkout['street2']" />
</div>
<div class="clearfix" />
<t t-set='zip_city' t-value='country and [x for x in country.get_address_fields() if x in ["zip", "city"]] or ["city", "zip"]'/>
<t t-if="'zip' in zip_city and zip_city.index('zip') &lt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
- <label class="control-label" for="zip">Zip Code</label>
+ <label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
@@ -1197,7 +1197,7 @@
</div>
<t t-if="'zip' in zip_city and zip_city.index('zip') &gt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
- <label class="control-label" for="zip">Zip Code</label>
+ <label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
diff --git a/doc/cla/corporate/vizucom.md b/doc/cla/corporate/vizucom.md
new file mode 100644
index 00000000000..0c0b78527d2
--- /dev/null
+++ b/doc/cla/corporate/vizucom.md
@@ -0,0 +1,14 @@
+Finland, 2017-08-01
+
+Vizucom Oy agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
+
+We declare that we are authorized and able to make this agreement and sign this declaration.
+
+Signed,
+
+ Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
+
+List of contributors:
+
+ Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
+ Henri Alasentie henri.alasentie@vizucom.com https://github.com/halasentie
diff --git a/odoo/addons/base/res/res_currency.py b/odoo/addons/base/res/res_currency.py
index ebfcdc060bf..259567b7313 100644
--- a/odoo/addons/base/res/res_currency.py
+++ b/odoo/addons/base/res/res_currency.py
@@ -188,7 +188,7 @@ class CurrencyRate(models.Model):
rate = fields.Float(digits=(12, 6), help='The rate of the currency to the currency of rate 1')
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True)
company_id = fields.Many2one('res.company', string='Company',
- default=lambda self: self.env.user._get_company())
+ default=lambda self: self.env.user.company_id)
_sql_constraints = [
('unique_name_per_day', 'unique (name,currency_id,company_id)', 'Only one currency rate per day allowed!'),
diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js
index 96ae44af4b5..71dbbd8e293 100644
--- a/odoo/addons/test_main_flows/static/src/js/tour.js
+++ b/odoo/addons/test_main_flows/static/src/js/tour.js
@@ -130,7 +130,7 @@ tour.register('main_flow_tour', {
}, {
// Add second component
trigger: ".o_field_x2many_list_row_add > a",
- in_modal: false,
+ extra_trigger: "body:not(.modal-open)",
content: _t("Click here to add some lines."),
position: "bottom",
}, {
diff --git a/odoo/fields.py b/odoo/fields.py
index 84a1d83d7a1..18174f1d145 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -563,7 +563,7 @@ class Field(MetaField('DummyField', (object,), {})):
""" Traverse the fields of the related field `self` except for the last
one, and return it as a pair `(last_record, last_field)`. """
for name in self.related[:-1]:
- record = record[name][:1]
+ record = record[name][:1].with_prefetch(record._prefetch)
return record, self.related_field
def _compute_related(self, records):
@@ -1028,6 +1028,7 @@ class Field(MetaField('DummyField', (object,), {})):
self.compute_value(record)
else:
recs = record._in_cache_without(self)
+ recs = recs.with_prefetch(record._prefetch)
self.compute_value(recs)
else:
diff --git a/odoo/http.py b/odoo/http.py
index 1ea60b9194d..83cde441fb9 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -275,8 +275,9 @@ class WebRequest(object):
if self._cr:
if exc_type is None and not self._failed:
self._cr.commit()
- self.registry.signal_changes()
- else:
+ if self.registry:
+ self.registry.signal_changes()
+ elif self.registry:
self.registry.reset_changes()
self._cr.close()
# just to be sure no one tries to re-use the request
@@ -1426,6 +1427,7 @@ class Root(object):
httprequest = werkzeug.wrappers.Request(environ)
httprequest.app = self
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
+ threading.current_thread().url = httprequest.url
explicit_session = self.setup_session(httprequest)
self.setup_db(httprequest)
diff --git a/odoo/models.py b/odoo/models.py
index ca40b271748..dd55b799f0a 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -2551,10 +2551,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
else:
records &= self._in_cache_without(f)
- # prefetch at most PREFETCH_MAX records
- if len(records) > PREFETCH_MAX:
- records = records[:PREFETCH_MAX] | self
-
# fetch records with read()
assert self in records and field in fs
records = records.with_prefetch(self._prefetch)
@@ -4667,13 +4663,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return RecordCache(self)
@api.model
- def _in_cache_without(self, field):
- """ Make sure ``self`` is present in cache (for prefetching), and return
- the records of model ``self`` in cache that have no value for ``field``
- (:class:`Field` instance).
+ def _in_cache_without(self, field, limit=PREFETCH_MAX):
+ """ Return records to prefetch that have no value in cache for ``field``
+ (:class:`Field` instance), including ``self``.
+ Return at most ``limit`` records.
"""
ids = [it for it in self._prefetch[self._name] - set(self.env.cache[field]) if it]
- return self.browse(ids)
+ recs = self.browse(ids)
+ if limit and len(recs) > limit:
+ recs = self + (recs - self)[:(limit - len(self))]
+ return recs
@api.model
def refresh(self):
diff --git a/odoo/service/wsgi_server.py b/odoo/service/wsgi_server.py
index 8ef70c8ad36..ec699dc13e0 100644
--- a/odoo/service/wsgi_server.py
+++ b/odoo/service/wsgi_server.py
@@ -156,6 +156,8 @@ def application_unproxied(environ, start_response):
del threading.current_thread().uid
if hasattr(threading.current_thread(), 'dbname'):
del threading.current_thread().dbname
+ if hasattr(threading.current_thread(), 'url'):
+ del threading.current_thread().url
with odoo.api.Environment.manage():
# Try all handlers until one returns some result (i.e. not None).
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index 9619dda4076..5afa24a20c8 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -409,11 +409,17 @@ class HttpCase(TransactionCase):
t0 = int(time.time())
for thread in threading.enumerate():
if thread.name.startswith('odoo.service.http.request.'):
+ thread.join_retry_count = 10
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
- time.sleep(0.05)
+ thread.join_retry_count -= 1
+ if thread.join_retry_count < 0:
+ _logger.warning("Stop waiting for thread %s handling request for url %s",
+ thread.name, thread.url)
+ break
+ time.sleep(0.5)
t1 = int(time.time())
if t0 != t1:
_logger.info('remaining requests')
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 7abe05a5ba5..8d3854aeb6d 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -907,15 +907,17 @@ def dumpstacks(sig=None, frame=None):
# modified for python 2.5 compatibility
threads_info = {th.ident: {'name': th.name,
'uid': getattr(th, 'uid', 'n/a'),
- 'dbname': getattr(th, 'dbname', 'n/a')}
+ 'dbname': getattr(th, 'dbname', 'n/a'),
+ 'url': getattr(th, 'url', 'n/a')}
for th in threading.enumerate()}
for threadId, stack in pycompat.items(sys._current_frames()):
thread_info = threads_info.get(threadId, {})
- code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s)" %
+ code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s) (url:%s)" %
(thread_info.get('name', 'n/a'),
threadId,
thread_info.get('dbname', 'n/a'),
- thread_info.get('uid', 'n/a')))
+ thread_info.get('uid', 'n/a'),
+ thread_info.get('url', 'n/a')))
for line in extract_stack(stack):
code.append(line)