diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-08-10 16:22:36 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-08-10 16:22:36 +0200 |
| commit | 85571bb78c9acc4e867cc3b480be9236a756fc22 (patch) | |
| tree | 11e77c15caeeb6cb01e09835892bc75351e0c6ba | |
| parent | deba788b4cdc22bb50e0c19adb5ed35ed33c0a01 (diff) | |
| parent | dde62073baf5a0cc45678e4504fe2f54ccffcf18 (diff) | |
[MERGE] forward port branch saas-15 up to dde62073baf
49 files changed, 397 insertions, 109 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 56d3c59fd9f..a4573fbee55 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 dceb556547d..0a52dd72bfc 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -778,7 +778,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, @@ -790,8 +789,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 d44212ee148..15f6b72c2b6 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -530,6 +530,7 @@ class AccountMoveLine(models.Model): {2} WHERE a.reconcile IS TRUE + AND l.full_reconcile_id is NULL {3} {4} {5} @@ -1545,6 +1546,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 @@ -1647,30 +1692,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 @@ -1678,7 +1709,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 @@ -1686,7 +1717,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 @@ -1704,16 +1735,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], @@ -1721,6 +1757,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 @@ -1766,3 +1821,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 ad316e2b0a2..c0430d720c9 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -473,4 +473,148 @@ class TestReconciliation(AccountingTestCase): self.assertEquals(round(aml.debit, 2), line['debit']) 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'])
\ No newline at end of file + 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/res_config_view.xml b/addons/account/views/res_config_view.xml index c3626626056..a58960c2b4e 100644 --- a/addons/account/views/res_config_view.xml +++ b/addons/account/views/res_config_view.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 4d6e3590037..cafa2077299 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/base_setup/views/res_config_views.xml b/addons/base_setup/views/res_config_views.xml index 86dac4a307b..c23322531b0 100644 --- a/addons/base_setup/views/res_config_views.xml +++ b/addons/base_setup/views/res_config_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 46c243286ef..7529c7be91f 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -323,6 +323,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 6fadf6ce756..a2051407e83 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/hr_attendance/views/res_config_view.xml b/addons/hr_attendance/views/res_config_view.xml index b8c11252fbf..cf72f9f2220 100644 --- a/addons/hr_attendance/views/res_config_view.xml +++ b/addons/hr_attendance/views/res_config_view.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 efc1433b629..491938b60c1 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -384,7 +384,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_views.xml b/addons/hr_expense/views/hr_expense_views.xml index baa339238a2..a8f4f045661 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_expense/views/res_config_views.xml b/addons/hr_expense/views/res_config_views.xml index bb550bd7caf..9d95f3cd9bd 100644 --- a/addons/hr_expense/views/res_config_views.xml +++ b/addons/hr_expense/views/res_config_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_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/mail/models/mail_template.py b/addons/mail/models/mail_template.py index 2a7d191cbfc..f54e6827a69 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -576,6 +576,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 072a76da4c8..4f7e89ae26b 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1725,6 +1725,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/res_config_views.xml b/addons/mass_mailing/views/res_config_views.xml index d7164d017d7..7c9dd4e0382 100644 --- a/addons/mass_mailing/views/res_config_views.xml +++ b/addons/mass_mailing/views/res_config_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 c645f1362b4..860e34318fa 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/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 7338313e245..77570e9b3ce 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, so_token: so_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 adc633fccc0..8e41fb3ef38 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -836,7 +836,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 1624115b567..fc212067d0d 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 @@ -396,12 +396,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/res_config_view.xml b/addons/point_of_sale/views/res_config_view.xml index 1a16886e973..562b37319bc 100644 --- a/addons/point_of_sale/views/res_config_view.xml +++ b/addons/point_of_sale/views/res_config_view.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 2c1d3ee7263..27c86e5be53 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 e6c402bc95a..faa5521b51f 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -111,8 +111,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 44ac59adc76..427ad89ab47 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -173,7 +173,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) @@ -289,7 +291,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/res_config_views.xml b/addons/project/views/res_config_views.xml index c56bf1bf247..d0cbfa1b26d 100644 --- a/addons/project/views/res_config_views.xml +++ b/addons/project/views/res_config_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>Team Collaboration</h2> diff --git a/addons/purchase/views/res_config_views.xml b/addons/purchase/views/res_config_views.xml index 57f7f97d080..cc24594ae45 100644 --- a/addons/purchase/views/res_config_views.xml +++ b/addons/purchase/views/res_config_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/res_config_view.xml b/addons/sales_team/views/res_config_view.xml index 628f217904c..f2f6badd327 100644 --- a/addons/sales_team/views/res_config_view.xml +++ b/addons/sales_team/views/res_config_view.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 540bd6f1a06..6482c1733f7 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 d1ef1ce1444..d2a4f16a38c 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 a34ff7f7bdf..a5fad884d37 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -680,7 +680,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) @@ -691,7 +691,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) @@ -701,7 +701,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) @@ -719,7 +719,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) @@ -729,7 +729,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) @@ -738,7 +738,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/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"> </div>'; var size = '<div class="media_iframe_video_size"> </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/res_config_views.xml b/addons/website/views/res_config_views.xml index a98cfd92a5b..854436faf0a 100644 --- a/addons/website/views/res_config_views.xml +++ b/addons/website/views/res_config_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 9a9ef605118..ab9ad2376b4 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -169,6 +169,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) @@ -225,7 +226,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 25fab128a24..543f4d95a1d 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 edb473d90e0..33f1775ba42 100644 --- a/addons/website_event_sale/controllers/main.py +++ b/addons/website_event_sale/controllers/main.py @@ -11,12 +11,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 4cbee712302..1ca79edeb01 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -364,7 +364,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 8197691fae3..8210ebf90fb 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1205,11 +1205,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> @@ -1219,14 +1219,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') < 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> @@ -1236,7 +1236,7 @@ </div> <t t-if="'zip' in zip_city and zip_city.index('zip') > 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/fields.py b/odoo/fields.py index e200d7b32dc..e7058891c49 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -558,7 +558,7 @@ class Field(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): @@ -1023,6 +1023,7 @@ class Field(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 626f43bbea4..fad2a17a124 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -1435,6 +1435,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 6949a5b9d45..bff10e28b38 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2554,10 +2554,6 @@ class BaseModel(object): 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) @@ -4682,13 +4678,16 @@ class BaseModel(object): 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 = filter(None, self._prefetch[self._name] - set(self.env.cache[field])) - 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 afcac829856..7dc5c0464e6 100644 --- a/odoo/service/wsgi_server.py +++ b/odoo/service/wsgi_server.py @@ -161,6 +161,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 a65f60343e1..b3e993781df 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -398,11 +398,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 448ffdfed43..ff45b8d8978 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -939,15 +939,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 sys._current_frames().items(): 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) |
