diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-02-20 13:40:48 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-02-20 13:40:48 +0100 |
| commit | f1ff55bca1242af86cd5feedbf7c4793b96174a1 (patch) | |
| tree | 164453a9cf678e004c2ae82819c51fbd57c0b6ae | |
| parent | 716a272480e649db721c081605368e1bccf8dc91 (diff) | |
| parent | de8cefcef4d456398e5028c52e3bd9db807e5f8a (diff) | |
[MERGE] forward port branch 11.0 up to de8cefcef4d
34 files changed, 459 insertions, 199 deletions
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 66019eaa59e..d61b2aa60f2 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -237,18 +237,20 @@ class AccountMove(models.Model): @api.multi def _reverse_move(self, date=None, journal_id=None, auto=False): self.ensure_one() - reversed_move = self.copy(default={ - 'date': date, - 'journal_id': journal_id.id if journal_id else self.journal_id.id, - 'ref': _('%sreversal of: ') % (_('Automatic ') if auto else '') + self.name, - 'auto_reverse': False}) - for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): - acm_line.write({ - 'debit': acm_line.credit, - 'credit': acm_line.debit, - 'amount_currency': -acm_line.amount_currency - }) - self.reverse_entry_id = reversed_move + with self.env.norecompute(): + reversed_move = self.copy(default={ + 'date': date, + 'journal_id': journal_id.id if journal_id else self.journal_id.id, + 'ref': _('%sreversal of: ') % (_('Automatic ') if auto else '') + self.name, + 'auto_reverse': False}) + for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): + acm_line.write({ + 'debit': acm_line.credit, + 'credit': acm_line.debit, + 'amount_currency': -acm_line.amount_currency + }) + self.reverse_entry_id = reversed_move + self.recompute() return reversed_move @api.multi @@ -373,7 +375,7 @@ class AccountMoveLine(models.Model): #computing the `reconciled` field. reconciled = False digits_rounding_precision = line.company_id.currency_id.rounding - if float_is_zero(amount, precision_rounding=digits_rounding_precision): + if (line.matched_debit_ids or line.matched_credit_ids) and float_is_zero(amount, precision_rounding=digits_rounding_precision): if line.currency_id and line.amount_currency: if float_is_zero(amount_residual_currency, precision_rounding=line.currency_id.rounding): reconciled = True @@ -684,7 +686,7 @@ class AccountMoveLine(models.Model): all_accounts.append(line.account_id) if (line.account_id.internal_type in ('receivable', 'payable')): partners.add(line.partner_id.id) - if line.reconciled: + if (line.matched_debit_ids or line.matched_credit_ids) and line.reconciled: raise UserError(_('You are trying to reconcile some entries that are already reconciled!')) if len(company_ids) > 1: raise UserError(_('To reconcile the entries company should be the same for all entries!')) @@ -858,6 +860,56 @@ class AccountMoveLine(models.Model): ) return rec_move_ids.unlink() + def _apply_taxes(self, vals, amount): + tax_lines_vals = [] + # Get ids from triplets : https://www.odoo.com/documentation/10.0/reference/orm.html#odoo.models.Model.write + tax_ids = [tax['id'] for tax in self.resolve_2many_commands('tax_ids', vals['tax_ids']) if tax.get('id')] + # Since create() receives ids instead of recordset, let's just use the old-api bridge + taxes = self.env['account.tax'].browse(tax_ids) + currency = self.env['res.currency'].browse(vals.get('currency_id')) + partner = self.env['res.partner'].browse(vals.get('partner_id')) + ctx = dict(self._context) + ctx['round'] = ctx.get('round', True) + res = taxes.with_context(ctx).compute_all(amount, + currency, 1, vals.get('product_id'), partner) + # Adjust line amount if any tax is price_include + if abs(res['total_excluded']) < abs(amount): + if vals['debit'] != 0.0: vals['debit'] = res['total_excluded'] + if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded'] + if vals.get('amount_currency'): + vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (res['total_excluded']/amount)) + # Create tax lines + for tax_vals in res['taxes']: + if tax_vals['amount']: + tax = self.env['account.tax'].browse([tax_vals['id']]) + account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id']) + if not account_id: account_id = vals['account_id'] + temp = { + 'account_id': account_id, + 'name': vals['name'] + ' ' + tax_vals['name'], + 'tax_line_id': tax_vals['id'], + 'move_id': vals['move_id'], + 'partner_id': vals.get('partner_id'), + 'statement_id': vals.get('statement_id'), + 'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0, + 'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0, + 'analytic_account_id': vals.get('analytic_account_id') if tax.analytic else False, + } + bank = self.env["account.bank.statement.line"].browse(vals.get('statement_line_id')).statement_id + if bank.currency_id != bank.company_id.currency_id: + ctx = {} + if 'date' in vals: + ctx['date'] = vals['date'] + elif 'date_maturity' in vals: + ctx['date'] = vals['date_maturity'] + temp['currency_id'] = bank.currency_id.id + temp['amount_currency'] = bank.company_id.currency_id.with_context(ctx).compute(tax_vals['amount'], bank.currency_id, round=True) + if vals.get('tax_exigible'): + temp['tax_exigible'] = True + temp['account_id'] = tax.cash_basis_account.id or account_id + tax_lines_vals.append(temp) + return tax_lines_vals + #################################################### # CRUD methods #################################################### @@ -925,50 +977,7 @@ class AccountMoveLine(models.Model): # Create tax lines tax_lines_vals = [] if context.get('apply_taxes') and vals.get('tax_ids'): - # Get ids from triplets : https://www.odoo.com/documentation/10.0/reference/orm.html#odoo.models.Model.write - tax_ids = [tax['id'] for tax in self.resolve_2many_commands('tax_ids', vals['tax_ids']) if tax.get('id')] - # Since create() receives ids instead of recordset, let's just use the old-api bridge - taxes = self.env['account.tax'].browse(tax_ids) - currency = self.env['res.currency'].browse(vals.get('currency_id')) - partner = self.env['res.partner'].browse(vals.get('partner_id')) - res = taxes.with_context(dict(self._context, round=True)).compute_all(amount, - currency, 1, vals.get('product_id'), partner) - # Adjust line amount if any tax is price_include - if abs(res['total_excluded']) < abs(amount): - if vals['debit'] != 0.0: vals['debit'] = res['total_excluded'] - if vals['credit'] != 0.0: vals['credit'] = -res['total_excluded'] - if vals.get('amount_currency'): - vals['amount_currency'] = self.env['res.currency'].browse(vals['currency_id']).round(vals['amount_currency'] * (res['total_excluded']/amount)) - # Create tax lines - for tax_vals in res['taxes']: - if tax_vals['amount']: - tax = self.env['account.tax'].browse([tax_vals['id']]) - account_id = (amount > 0 and tax_vals['account_id'] or tax_vals['refund_account_id']) - if not account_id: account_id = vals['account_id'] - temp = { - 'account_id': account_id, - 'name': vals['name'] + ' ' + tax_vals['name'], - 'tax_line_id': tax_vals['id'], - 'move_id': vals['move_id'], - 'partner_id': vals.get('partner_id'), - 'statement_id': vals.get('statement_id'), - 'debit': tax_vals['amount'] > 0 and tax_vals['amount'] or 0.0, - 'credit': tax_vals['amount'] < 0 and -tax_vals['amount'] or 0.0, - 'analytic_account_id': vals.get('analytic_account_id') if tax.analytic else False, - } - bank = self.env["account.bank.statement.line"].browse(vals.get('statement_line_id')).statement_id - if bank.currency_id != bank.company_id.currency_id: - ctx = {} - if 'date' in vals: - ctx['date'] = vals['date'] - elif 'date_maturity' in vals: - ctx['date'] = vals['date_maturity'] - temp['currency_id'] = bank.currency_id.id - temp['amount_currency'] = bank.company_id.currency_id.with_context(ctx).compute(tax_vals['amount'], bank.currency_id, round=True) - if vals.get('tax_exigible'): - temp['tax_exigible'] = True - temp['account_id'] = tax.cash_basis_account.id or account_id - tax_lines_vals.append(temp) + tax_lines_vals = self._apply_taxes(vals, amount) #Toggle the 'tax_exigible' field to False in case it is not yet given and the tax in 'tax_line_id' or one of #the 'tax_ids' is a cash based tax. @@ -1377,87 +1386,89 @@ class AccountPartialReconcile(models.Model): self.ensure_one() move_date = self.debit_move_id.date newly_created_move = self.env['account.move'] - for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): - #move_date is the max of the 2 reconciled items - if move_date < move.date: - move_date = move.date - for line in move.line_ids: - #TOCHECK: normal and cash basis taxes shouldn't be mixed together (on the same invoice line for example) as it will - # create reporting issues. Not sure of the behavior to implement in that case, though. - if not line.tax_exigible: - percentage_before = percentage_before_rec[move.id] - percentage_after = line._get_matched_percentage()[move.id] - #amount is the current cash_basis amount minus the one before the reconciliation - amount = line.balance * percentage_after - line.balance * percentage_before - rounded_amt = self._get_amount_tax_cash_basis(amount, line) - if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): - continue - if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment': - if not newly_created_move: - newly_created_move = self._create_tax_basis_move() - #create cash basis entry for the tax line - to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.move_id.name, - 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, - 'credit': rounded_amt if rounded_amt > 0 else 0.0, - 'account_id': line.account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'tax_exigible': True, - 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, - 'currency_id': line.currency_id.id, - 'move_id': newly_created_move.id, - 'partner_id': line.partner_id.id, - }) - # Group by cash basis account and tax - self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.name, - 'debit': rounded_amt if rounded_amt > 0 else 0.0, - 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, - 'account_id': line.tax_line_id.cash_basis_account_id.id, - 'analytic_account_id': line.analytic_account_id.id, - 'analytic_tag_ids': line.analytic_tag_ids.ids, - 'tax_line_id': line.tax_line_id.id, - 'tax_exigible': True, - 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, - 'currency_id': line.currency_id.id, - 'move_id': newly_created_move.id, - 'partner_id': line.partner_id.id, - }) - if line.account_id.reconcile: - #setting the account to allow reconciliation will help to fix rounding errors - to_clear_aml |= line - to_clear_aml.reconcile() - - if any([tax.tax_exigibility == 'on_payment' for tax in line.tax_ids]): - if not newly_created_move: - newly_created_move = self._create_tax_basis_move() - #create cash basis entry for the base - for tax in line.tax_ids: - account_id = self._get_tax_cash_basis_base_account(line, tax) - self.env['account.move.line'].with_context(check_move_validity=False).create({ - 'name': line.name, - 'debit': rounded_amt > 0 and rounded_amt or 0.0, - 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': account_id.id, + with self.env.norecompute(): + for move in (self.debit_move_id.move_id, self.credit_move_id.move_id): + #move_date is the max of the 2 reconciled items + if move_date < move.date: + move_date = move.date + for line in move.line_ids: + #TOCHECK: normal and cash basis taxes shouldn't be mixed together (on the same invoice line for example) as it will + # create reporting issues. Not sure of the behavior to implement in that case, though. + if not line.tax_exigible: + percentage_before = percentage_before_rec[move.id] + percentage_after = line._get_matched_percentage()[move.id] + #amount is the current cash_basis amount minus the one before the reconciliation + amount = line.balance * percentage_after - line.balance * percentage_before + rounded_amt = self._get_amount_tax_cash_basis(amount, line) + if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): + continue + if line.tax_line_id and line.tax_line_id.tax_exigibility == 'on_payment': + if not newly_created_move: + newly_created_move = self._create_tax_basis_move() + #create cash basis entry for the tax line + to_clear_aml = self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.move_id.name, + 'debit': abs(rounded_amt) if rounded_amt < 0 else 0.0, + 'credit': rounded_amt if rounded_amt > 0 else 0.0, + 'account_id': line.account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, 'tax_exigible': True, - 'tax_ids': [(6, 0, [tax.id])], - 'move_id': newly_created_move.id, + 'amount_currency': line.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, - 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, + 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, }) + # Group by cash basis account and tax self.env['account.move.line'].with_context(check_move_validity=False).create({ 'name': line.name, - 'credit': rounded_amt > 0 and rounded_amt or 0.0, - 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': account_id.id, + 'debit': rounded_amt if rounded_amt > 0 else 0.0, + 'credit': abs(rounded_amt) if rounded_amt < 0 else 0.0, + 'account_id': line.tax_line_id.cash_basis_account_id.id, + 'analytic_account_id': line.analytic_account_id.id, + 'analytic_tag_ids': line.analytic_tag_ids.ids, + 'tax_line_id': line.tax_line_id.id, 'tax_exigible': True, - 'move_id': newly_created_move.id, + 'amount_currency': line.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, 'currency_id': line.currency_id.id, - 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, + 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, }) + if line.account_id.reconcile: + #setting the account to allow reconciliation will help to fix rounding errors + to_clear_aml |= line + to_clear_aml.reconcile() + + if any([tax.tax_exigibility == 'on_payment' for tax in line.tax_ids]): + if not newly_created_move: + newly_created_move = self._create_tax_basis_move() + #create cash basis entry for the base + for tax in line.tax_ids: + account_id = self._get_tax_cash_basis_base_account(line, tax) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.name, + 'debit': rounded_amt > 0 and rounded_amt or 0.0, + 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, + 'account_id': account_id.id, + 'tax_exigible': True, + 'tax_ids': [(6, 0, [tax.id])], + 'move_id': newly_created_move.id, + 'currency_id': line.currency_id.id, + 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, + 'partner_id': line.partner_id.id, + }) + self.env['account.move.line'].with_context(check_move_validity=False).create({ + 'name': line.name, + 'credit': rounded_amt > 0 and rounded_amt or 0.0, + 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, + 'account_id': account_id.id, + 'tax_exigible': True, + 'move_id': newly_created_move.id, + 'currency_id': line.currency_id.id, + 'amount_currency': self.amount_currency and line.currency_id.round(-line.amount_currency * amount / line.balance) or 0.0, + 'partner_id': line.partner_id.id, + }) + self.recompute() if newly_created_move: if move_date > (self.company_id.period_lock_date or '0000-00-00') and newly_created_move.date != move_date: # The move date should be the maximum date between payment and invoice (in case diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 0ed5377c0b8..e6a958f81d6 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -885,11 +885,11 @@ var StatementModel = BasicModel.extend({ }); var company_currency = session.get_currency(line.st_line.currency_id); var company_precision = company_currency && company_currency.digits[1] || 2; - total = utils.round_precision(total*1000, company_precision)/1000 || 0; + total = utils.round_decimals(total, company_precision) || 0; if(isOtherCurrencyId){ var other_currency = session.get_currency(isOtherCurrencyId); var other_precision = other_currency && other_currency.digits[1] || 2; - amount_currency = utils.round_precision(amount_currency, other_precision) + amount_currency = utils.round_decimals(amount_currency, other_precision); } line.balance = { amount: total, diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 2c23fdf635b..cad63fd8c97 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -97,7 +97,6 @@ </div> <div class="o_setting_right_pane" name="account_taxcloud_right_pane"> <label for="module_account_taxcloud" string="TaxCloud"/> - <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/> <div class="text-muted"> Compute tax rates based on U.S. ZIP codes </div> diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index b2477217e73..2acc3ba3ec7 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -274,6 +274,8 @@ class AccountVoucher(models.Model): :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method) :rtype: tuple(float, list of int) ''' + tax_calculation_rounding_method = self.env.user.company_id.tax_calculation_rounding_method + tax_lines_vals = [] for line in self.line_ids: #create one move line per voucher line where amount is not 0.0 if not line.price_subtotal: @@ -302,7 +304,36 @@ class AccountVoucher(models.Model): 'currency_id': company_currency != current_currency and current_currency or False, 'payment_id': self._context.get('payment_id'), } - self.env['account.move.line'].with_context(apply_taxes=True).create(move_line) + # When global rounding is activated, we must wait until all tax lines are computed to + # merge them. + if tax_calculation_rounding_method == 'round_globally': + self.env['account.move.line'].create(move_line) + tax_lines_vals += self.env['account.move.line'].with_context(round=False)._apply_taxes( + move_line, + move_line.get('debit', 0.0) - move_line.get('credit', 0.0) + ) + else: + self.env['account.move.line'].with_context(apply_taxes=True).create(move_line) + + # When round globally is set, we merge the tax lines + if tax_calculation_rounding_method == 'round_globally': + tax_lines_vals_merged = {} + for tax_line_vals in tax_lines_vals: + key = ( + tax_line_vals['tax_line_id'], + tax_line_vals['account_id'], + tax_line_vals['analytic_account_id'], + ) + if key not in tax_lines_vals_merged: + tax_lines_vals_merged[key] = tax_line_vals + else: + tax_lines_vals_merged[key]['debit'] += tax_line_vals['debit'] + tax_lines_vals_merged[key]['credit'] += tax_line_vals['credit'] + currency = self.env['res.currency'].browse(company_currency) + for vals in tax_lines_vals_merged.values(): + vals['debit'] = currency.round(vals['debit']) + vals['credit'] = currency.round(vals['credit']) + self.env['account.move.line'].create(vals) return line_total @api.multi diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index 67309d1ad23..03df5bd89ce 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -28,14 +28,14 @@ class ResPartner(models.Model): signup_token = fields.Char(copy=False, groups="base.group_erp_manager") signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager") signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager") - signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid') + signup_valid = fields.Boolean(compute='_compute_signup_valid', compute_sudo=True, string='Signup Token is Valid') signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL') @api.multi @api.depends('signup_token', 'signup_expiration') def _compute_signup_valid(self): dt = now() - for partner in self.sudo(): + for partner in self: partner.signup_valid = bool(partner.signup_token) and \ (not partner.signup_expiration or dt <= partner.signup_expiration) diff --git a/addons/base_automation/views/base_automation_view.xml b/addons/base_automation/views/base_automation_view.xml index 7f1dfb861cd..33cef6b7ff1 100644 --- a/addons/base_automation/views/base_automation_view.xml +++ b/addons/base_automation/views/base_automation_view.xml @@ -11,9 +11,6 @@ <xpath expr="//button[@name='create_action']" position="replace"> </xpath> <xpath expr="//button[@name='unlink_action']" position="replace"> - <button name="unlink_action_for_menu" string="Remove Action" type="object" - attrs="{'invisible':[('binding_model_id','=',False)]}" - help="Remove the contextual action related to this server action"/> </xpath> <div class="oe_title" position="before"> <div class="oe_button_box" name="button_box"> diff --git a/addons/base_import_module/views/base_import_module_view.xml b/addons/base_import_module/views/base_import_module_view.xml index 60af0d3fc61..b132c7f3a83 100644 --- a/addons/base_import_module/views/base_import_module_view.xml +++ b/addons/base_import_module/views/base_import_module_view.xml @@ -8,6 +8,7 @@ <form string="Import module"> <field name="state" invisible="1"/> <separator string="Import Module" colspan="4"/> + <p class="alert alert-warning">Note: you can only import data modules (.xml files and static assets)</p> <group states="init" col="4"> <label string="Select module package to import (.zip file):" colspan="4"/> <field name="module_file" colspan="4"/> diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 1502a3b51bc..6e32490a5de 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -865,7 +865,6 @@ for rec in records: <field name="domain">[('type','=','opportunity')]</field> <field name="context">{ 'default_type': 'opportunity', - 'default_user_id': uid } </field> <field name="search_view_id" ref="crm.view_crm_case_opportunities_filter"/> @@ -906,7 +905,6 @@ for rec in records: <field name="domain">[('type','=','opportunity')]</field> <field name="context">{ 'default_type': 'opportunity', - 'default_user_id': uid, 'search_default_assigned_to_me': 1, }</field> <field name="view_ids" diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index a422438778c..b15d4b6d9ef 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -441,7 +441,9 @@ class GoogleCalendar(models.AbstractModel): if google_attendee.get('found'): continue - attendee = ResPartner.search([('email', '=', google_attendee['email'])], limit=1) + attendee = ResPartner.search([('email', '=ilike', google_attendee['email']), ('user_ids', '!=', False)], limit=1) + if not attendee: + attendee = ResPartner.search([('email', '=ilike', google_attendee['email'])], limit=1) if not attendee: data = { 'email': partner_email, diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 35286c1fc9d..8be2923a03c 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -9,7 +9,7 @@ from datetime import timedelta from odoo import api, fields, models from odoo.exceptions import AccessError, UserError, ValidationError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.tools.translate import _ from odoo.addons.resource.models.resource import HOURS_PER_DAY diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index bf94fbfac27..564afa082c3 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -176,7 +176,10 @@ class HolidaysType(models.Model): if not record.limit: name = "%(name)s (%(count)s)" % { 'name': name, - 'count': _('%g remaining out of %g') % (record.virtual_remaining_leaves or 0.0, record.max_leaves or 0.0) + 'count': _('%g remaining out of %g') % ( + float_round(record.virtual_remaining_leaves or 0.0, precision_digits=2) + 0.0, + record.max_leaves or 0.0 + ) } res.append((record.id, name)) return res diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 783fbd417ef..bbf64b138f8 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -177,6 +177,7 @@ class ImLivechatChannel(models.Model): 'public': 'private', 'email_send': False, }) + mail_channel._broadcast([operator_partner_id]) return mail_channel.sudo().with_context(im_livechat_operator_partner_id=operator_partner_id).channel_info()[0] @api.model diff --git a/addons/l10n_it/__manifest__.py b/addons/l10n_it/__manifest__.py index f78432b06b3..fd621d872fd 100644 --- a/addons/l10n_it/__manifest__.py +++ b/addons/l10n_it/__manifest__.py @@ -17,7 +17,7 @@ Piano dei conti italiano di un'impresa generica. Italian accounting chart and localization. """, 'category': 'Localization', - 'website': 'http://www.openerp-italia.org/', + 'website': 'http://www.odoo.com/', 'data': [ 'data/l10n_it_chart_data.xml', 'data/account.account.template.csv', diff --git a/addons/l10n_mx/data/account_data.xml b/addons/l10n_mx/data/account_data.xml index 782597ee8ea..8176c32b5f3 100644 --- a/addons/l10n_mx/data/account_data.xml +++ b/addons/l10n_mx/data/account_data.xml @@ -11,6 +11,10 @@ <field name="name">IVA 16% </field> </record> + <record id="tax_group_iva_8" model="account.tax.group"> + <field name="name">IVA 8%</field> + </record> + <record id="tax_group_iva_ret_4" model="account.tax.group"> <field name="name">IVA Retencion 4%</field> </record> diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index b961639b36a..e182fe60763 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -30,6 +30,10 @@ <field name="name">DIOT: 0%</field> <field name="applicability">taxes</field> </record> + <record id="tag_diot_8" model="account.account.tag"> + <field name="name">DIOT: 8%</field> + <field name="applicability">taxes</field> + </record> <record id="tag_diot_ret" model="account.account.tag"> <field name="name">DIOT: Retención</field> <field name="applicability">taxes</field> @@ -182,5 +186,35 @@ <field name="cash_basis_account_id" ref="cuenta118_01"/> <field name="tag_ids" eval="[(6,0,[ref('tag_diot_16')])]"/> </record> + + <record id="tax16" model="account.tax.template"> + <field name="chart_template_id" ref="mx_coa"/> + <field name="name">IVA(8%) COMPRAS</field> + <field name="description">IVA(8%)</field> + <field name="amount">8</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="account_id" ref="cuenta119_01"/> + <field name="refund_account_id" ref="cuenta119_01"/> + <field name="tax_group_id" ref="tax_group_iva_8"/> + <field name="tax_exigibility">on_payment</field> + <field name="cash_basis_account" ref="cuenta118_01"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_diot_8')])]"/> + </record> + + <record id="tax17" model="account.tax.template"> + <field name="chart_template_id" ref="mx_coa"/> + <field name="name">IVA(8%) VENTAS</field> + <field name="description">IVA(8%)</field> + <field name="amount">8</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="account_id" ref="cuenta209_01"/> + <field name="refund_account_id" ref="cuenta209_01"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> + <field name="tax_group_id" ref="tax_group_iva_8"/> + <field name="tax_exigibility">on_payment</field> + <field name="cash_basis_account" ref="cuenta208_01"/> + </record> </data> </odoo> diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index ed6262e865f..87fdba30447 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -610,6 +610,12 @@ class MassMailing(models.Model): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() failed_mails.sudo().unlink() + res_ids = self.get_recipients() + except_mailed = self.env['mail.mail.statistics'].search([ + ('model', '=', self.mailing_model_real), + ('res_id', 'in', res_ids), + ('exception', '!=', False), + ('mass_mailing_id', '=', self.id)]).unlink() self.write({'state': 'in_queue'}) #------------------------------------------------------ diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 1c93a0f816f..a2c6b27df62 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -487,13 +487,11 @@ class PosOrder(models.Model): aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids aml = aml.filtered(lambda r: not r.reconciled and r.account_id.internal_type == 'receivable' and r.partner_id == order.partner_id.commercial_partner_id) - # Reconcile returns first - # to avoid mixing up the credit of a payment and the credit of a return - # in the receivable account - aml_returns = aml.filtered(lambda l: (l.journal_id.type == 'sale' and l.credit) or (l.journal_id.type != 'sale' and l.debit)) try: - aml_returns.reconcile() - (aml - aml_returns).reconcile() + # Cash returns will be well reconciled + # Whereas freight returns won't be + # "c'est la vie..." + aml.reconcile() except Exception: # There might be unexpected situations where the automatic reconciliation won't # work. We don't want the user to be blocked because of this, since the automatic diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index 7abb931d68e..b5cff2f233a 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -437,7 +437,7 @@ var Gui = core.Class.extend({ numpad_input: function(buffer, input, options) { var newbuf = buffer.slice(0); options = options || {}; - var newbuf_float = field_utils.parse.float(newbuf); + var newbuf_float = newbuf === '-' ? newbuf : field_utils.parse.float(newbuf); var decimal_point = _t.database.parameters.decimal_point; if (input === decimal_point) { @@ -457,7 +457,9 @@ var Gui = core.Class.extend({ newbuf = newbuf.substring(1,newbuf.length); } } else if (input === '-') { - if ( newbuf[0] === '-' ) { + if (options.firstinput) { + newbuf = '-'; + } else if ( newbuf[0] === '-' ) { newbuf = newbuf.substring(1,newbuf.length); } else { newbuf = '-' + newbuf; diff --git a/addons/sale_crm/models/crm_lead.py b/addons/sale_crm/models/crm_lead.py index b4eeb43f565..2e62e818905 100644 --- a/addons/sale_crm/models/crm_lead.py +++ b/addons/sale_crm/models/crm_lead.py @@ -20,7 +20,7 @@ class CrmLead(models.Model): nbr = 0 company_currency = lead.company_currency or self.env.user.company_id.currency_id for order in lead.order_ids: - if order.state in ('draft', 'sent', 'sale'): + if order.state in ('draft', 'sent'): nbr += 1 if order.state not in ('draft', 'sent', 'cancel'): total += order.currency_id.compute(order.amount_untaxed, company_currency) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 87ec0a10f2c..cc98e4da640 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -121,7 +121,7 @@ class AccountAnalyticLine(models.Model): total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue')) # compute (new) revenue of current timesheet line values['timesheet_revenue'] = min( - analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-so_line.discount)), + analytic_account.currency_id.round(unit_amount * sale_price * (1-so_line.discount/100)), total_revenue_so - total_revenue_invoiced ) values['timesheet_invoice_type'] = 'billable_fixed' diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 2ee2bc1ce90..1dcd88e5cad 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -257,7 +257,9 @@ class Product(models.Model): # TODO: Still optimization possible when searching virtual quantities ids = [] - for product in self.with_context(prefetch_fields=False).search([]): + # Order the search on `id` to prevent the default order on the product name which slows + # down the search because of the join on the translation table to get the translated names. + for product in self.with_context(prefetch_fields=False).search([], order='id'): if OPERATORS[operator](product[field], value): ids.append(product.id) return [('id', 'in', ids)] diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index 240d9d7523b..4c82165eeba 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -11,12 +11,9 @@ <div> <span><strong>Customer Address:</strong></span> </div> - <div t-if="o.move_lines and o.move_lines[0].partner_id" name="partner_header"> - <div t-field="o.move_lines[0].partner_id" - t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> - </div> - <div t-if="not (o.move_lines and o.move_lines[0].partner_id) and o.partner_id" name="partner_header"> - <div t-field="o.partner_id" + <t t-set="partner" t-value="o.partner_id or (o.move_lines and o.move_lines[0].partner_id) or False"/> + <div t-if="partner" name="partner_header"> + <div t-esc="partner" t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/> </div> </div> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 234b3b16b4e..640c95d3580 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -240,11 +240,8 @@ <group> <group> <field name="partner_id" attrs="{'invisible': [('picking_type_code', '=', 'internal')]}"/> - <button name="action_assign_partner" string="Assign Partner" type="object" help="Assign the partner to the Operations. Use only if the partner is manually changed!" - attrs="{'invisible': ['|', '|', ('move_line_exist', '=', False), ('state', 'not in', ('draft', 'assigned', 'confirmed')), ('picking_type_code', '=', 'internal')]}" - class="oe_link oe_edit_only"/> - <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/> - <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/> + <field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> + <field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}" domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"/> <field name="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/> </group> <group> diff --git a/addons/web/static/src/js/chrome/search_filters.js b/addons/web/static/src/js/chrome/search_filters.js index 4628578f9fe..05de554da2f 100644 --- a/addons/web/static/src/js/chrome/search_filters.js +++ b/addons/web/static/src/js/chrome/search_filters.js @@ -238,7 +238,7 @@ var DateTime = Field.extend({ _create_new_widget: function (name) { this[name] = new (this._get_widget_class())(this); return this[name].appendTo(this.$el).then((function () { - this[name].setValue(moment(new Date())); + this[name].setValue(moment()); }).bind(this)); }, _get_widget_class: function () { diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index c4590c1430b..4978e6ae920 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -294,8 +294,10 @@ var PivotModel = AbstractModel.extend({ self._updateMainGroupBys(old_row, self.data.main_row); } - self._updateTree(old_col.root, self.data.main_col.root); - self._updateMainGroupBys(old_col, self.data.main_col); + if (!('pivot_column_groupby' in (params.context || {}))) { + self._updateTree(old_col.root, self.data.main_col.root); + self._updateMainGroupBys(old_col, self.data.main_col); + } }); }, /** diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 904c517e5de..c32435a9b43 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -905,6 +905,95 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('Unload Filter, reset display, load another filter', function (assert) { + assert.expect(18); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot>' + + '<field name="foo" type="measure"/>' + + '</pivot>', + viewOptions: { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }, + }); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + // Equivalent to unload the filter + var reloadParams = { + context: {}, + }; + pivot.reload(reloadParams); + // collapse all headers + pivot.$('.o_pivot_header_cell_opened').click(); + pivot.$('.o_pivot_header_cell_opened').click(); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_closed').length, 1, + 'The column should not be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 0, + 'There should not be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 0, + 'There should not be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_closed').length, 1, + 'The row should not be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 0, + 'There should not be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 0, + 'There should not be a row "xpad"'); + + // Equivalent to load another filter + reloadParams = { + context: { + pivot_measures: ['foo'], + pivot_column_groupby: ['customer'], + pivot_row_groupby: ['product_id'], + }, + }; + pivot.reload(reloadParams); + + // Check Columns + assert.strictEqual(pivot.$('thead .o_pivot_header_cell_opened').length, 1, + 'The column should be grouped'); + assert.strictEqual(pivot.$('thead tr:contains("First")').length, 1, + 'There should be a column "First"'); + assert.strictEqual(pivot.$('thead tr:contains("Second")').length, 1, + 'There should be a column "Second"'); + + // Check Rows + assert.strictEqual(pivot.$('tbody .o_pivot_header_cell_opened').length, 1, + 'The row should be grouped'); + assert.strictEqual(pivot.$('tbody tr:contains("xphone")').length, 1, + 'There should be a row "xphone"'); + assert.strictEqual(pivot.$('tbody tr:contains("xpad")').length, 1, + 'There should be a row "xpad"'); + + pivot.destroy(); + }); + QUnit.test('correctly uses pivot_ keys from the context', function (assert) { assert.expect(7); diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index 79c70becf98..6d7aa37fc78 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -1,10 +1,12 @@ -'use strict'; odoo.define('website_sale_delivery.checkout', function (require) { + 'use strict'; require('web.dom_ready'); var ajax = require('web.ajax'); var core = require('web.core'); var _t = core._t; + var concurrency = require('web.concurrency'); + var dp = new concurrency.DropPrevious(); /* Handle interactive carrier choice + cart update */ var $pay_button = $('#o_payment_form_pay'); @@ -51,7 +53,7 @@ odoo.define('website_sale_delivery.checkout', function (require) { $pay_button.prop('disabled', true); var carrier_id = $(ev.currentTarget).val(); var values = {'carrier_id': carrier_id}; - ajax.jsonRpc('/shop/update_carrier', 'call', values) + dp.add(ajax.jsonRpc('/shop/update_carrier', 'call', values)) .then(_onCarrierUpdateAnswer); }; diff --git a/doc/cla/corporate/invitu.md b/doc/cla/corporate/invitu.md index 3b15373c298..19207d3de2e 100644 --- a/doc/cla/corporate/invitu.md +++ b/doc/cla/corporate/invitu.md @@ -11,5 +11,6 @@ Signed, Cyril Vinh-Tung contact@invitu.com https://github.com/invitu List of contributors: -Cyril Vinh-Tung contact@invitu.com https://github.com/invitu +Cyril Vinh-Tung cyril@invitu.com https://github.com/cvinh +Tymofii Glukhov tima@invitu.com https://github.com/tglukhov diff --git a/doc/cla/individual/naglis.md b/doc/cla/individual/naglis.md new file mode 100644 index 00000000000..dde081dfaa1 --- /dev/null +++ b/doc/cla/individual/naglis.md @@ -0,0 +1,11 @@ +Lithuania, 2019-02-09 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Naglis Jonaitis naglis@mailbox.org https://github.com/naglis diff --git a/doc/reference/reports.rst b/doc/reference/reports.rst index ed408a0d817..9b7d2e365e3 100644 --- a/doc/reference/reports.rst +++ b/doc/reference/reports.rst @@ -301,6 +301,38 @@ the function ``render_html`` and pass objects in the ``docargs`` dictionary: } return report_obj.render('module.report_name', docargs) +.. _reference/reports/custom_fonts: + +Custom fonts +============ +If you want to use custom fonts you will need to add your custom font and the related less/CSS to the ``web.reports_assets_common`` assets bundle. +Adding your custom font(s) to ``web.assets_common`` or ``web.assets_backend`` will not make your font available in QWeb reports. + +Example:: + + <template id="report_assets_common_custom_fonts" name="Custom QWeb fonts" inherit_id="web.report_assets_common"> + <xpath expr="." position="inside"> + <link href="/your_module/static/src/less/fonts.less" rel="stylesheet" type="text/less"/> + </xpath> + </template> + +You will need to define your ``@font-face`` within this less file, even if you've used in another assets bundle (other than ``web.reports_assets_common``). + +Example:: + + @font-face { + font-family: 'MonixBold'; + src: local('MonixBold'), local('MonixBold'), url(/your_module/static/src/fonts/MonixBold-Regular.otf) format('opentype'); + } + + .h1-title-big { + font-family: MonixBold; + font-size: 60px; + color: #3399cc; + } + +After you've added the less into your assets bundle you can use the classes - in this example ``h1-title-big`` - in your custom QWeb report. + Reports are web pages ===================== diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index e409e6e310d..d778f8166dc 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -374,6 +374,16 @@ class TestFields(common.TransactionCase): with self.assertRaises(AccessError): foo.sudo(user).display_name = 'Forbidden' + def test_13_inverse_access(self): + """ test access rights on inverse fields """ + foo = self.env['test_new_api.category'].create({'name': 'Foo'}) + user = self.env['res.users'].create({'name': 'Foo', 'login': 'foo'}) + self.assertFalse(user.has_group('base.group_system')) + # add group on non-stored inverse field + self.patch(type(foo).display_name, 'groups', 'base.group_system') + with self.assertRaises(AccessError): + foo.sudo(user).display_name = 'Forbidden' + def test_14_search(self): """ test search on computed fields """ discussion = self.env.ref('test_new_api.discussion_0') diff --git a/odoo/api.py b/odoo/api.py index 9b904c497f3..f8332a8c762 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -55,7 +55,7 @@ from weakref import WeakSet from decorator import decorator from werkzeug.local import Local, release_local -from odoo.tools import frozendict, classproperty, StackMap +from odoo.tools import frozendict, classproperty, StackMap, pycompat _logger = logging.getLogger(__name__) @@ -965,13 +965,18 @@ class Cache(object): def get(self, record, field): """ Return the value of ``field`` for ``record``. """ key = record.env.cache_key(field) - value = self._data[key][field][record.id] + value = self._data[key][field][record._ids[0]] return value.get() if isinstance(value, SpecialValue) else value def set(self, record, field, value): """ Set the value of ``field`` for ``record``. """ key = record.env.cache_key(field) - self._data[key][field][record.id] = value + self._data[key][field][record._ids[0]] = value + + def update(self, records, field, values): + """ Set the values of ``field`` for several ``records``. """ + key = records.env.cache_key(field) + self._data[key][field].update(pycompat.izip(records._ids, values)) def remove(self, record, field): """ Remove the value of ``field`` for ``record``. """ diff --git a/odoo/fields.py b/odoo/fields.py index 4a41b4c4915..35b2892d447 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2025,7 +2025,8 @@ class Many2one(_Relational): return () def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): if use_name_get and value: @@ -2141,7 +2142,8 @@ class _RelationalMulti(_Relational): raise ValueError("Wrong value for %s: %s" % (self, value)) def convert_to_record(self, value, record): - return record.env[self.comodel_name]._browse(value, record.env, record._prefetch) + # use registry to avoid creating a recordset for the model + return record.env.registry[self.comodel_name]._browse(value, record.env, record._prefetch) def convert_to_read(self, value, record, use_name_get=True): return value.ids @@ -2575,9 +2577,15 @@ class Id(Field): def __get__(self, record, owner): if record is None: return self # the field is accessed through the class owner - if not record: + + # the code below is written to make record.id as quick as possible + ids = record._ids + size = len(ids) + if size is 0: return False - return record.ensure_one()._ids[0] + elif size is 1: + return ids[0] + raise ValueError("Expected singleton: %s" % record) def __set__(self, record, value): raise TypeError("field 'id' cannot be assigned") diff --git a/odoo/models.py b/odoo/models.py index 538984a9be6..c9d89c175ed 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2611,20 +2611,22 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # retrieve results from records; this takes values from the cache and # computes remaining fields self = self.with_prefetch(self._prefetch.copy()) - data = {record: {'id': record.id} for record in self} - missing = set() + data = [(record, {'id': record._ids[0]}) for record in self] use_name_get = (load == '_classic_read') for name in (stored + inherited + computed): convert = self._fields[name].convert_to_read # restrict the prefetching of self's model to self; this avoids # computing fields on a larger recordset than self self._prefetch[self._name] = set(self._ids) - for record in self: + for record, vals in data: + # missing records have their vals empty + if not vals: + continue try: - data[record][name] = convert(record[name], record, use_name_get) + vals[name] = convert(record[name], record, use_name_get) except MissingError: - missing.add(record) - result = [data[record] for record in self if record not in missing] + vals.clear() + result = [vals for record, vals in data if vals] return result @@ -2719,36 +2721,46 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): res = 'pg_size_pretty(length(%s)::bigint)' % res return '%s as "%s"' % (res, col) + # selected fields are: 'id' followed by fields_pre qual_names = [qualify(name) for name in [self._fields['id']] + fields_pre] # determine the actual query to execute from_clause, where_clause, params = query.get_sql() query_str = "SELECT %s FROM %s WHERE %s" % (",".join(qual_names), from_clause, where_clause) - result = [] + # fetch one list of record values per field + field_values_list = [[] for name in qual_names] param_pos = params.index(param_ids) for sub_ids in cr.split_for_in_conditions(self.ids): params[param_pos] = tuple(sub_ids) cr.execute(query_str, params) - result.extend(cr.dictfetchall()) + for row in cr.fetchall(): + for values, val in pycompat.izip(field_values_list, row): + values.append(val) - ids = [vals['id'] for vals in result] + ids = field_values_list.pop(0) fetched = self.browse(ids) if ids: # translate the fields if necessary if context.get('lang'): - for field in fields_pre: + for field, values in pycompat.izip(fields_pre, field_values_list): if not field.inherited and callable(field.translate): name = field.name translate = field.get_trans_func(fetched) - for vals in result: - vals[name] = translate(vals['id'], vals[name]) + for index in range(len(ids)): + values[index] = translate(ids[index], values[index]) # store result in cache - for vals in result: - record = self.browse(vals.pop('id'), self._prefetch) - record._cache.update(record._convert_to_cache(vals, validate=False)) + target = self.browse([], self._prefetch) + for field, values in pycompat.izip(fields_pre, field_values_list): + convert = field.convert_to_cache + # Note that the target record passed to convert below is empty. + # This does not harm in practice, as it is only used in Monetary + # fields for rounding the value. As the value comes straight + # from the database, it is expected to be rounded already. + values = [convert(value, target, validate=False) for value in values] + self.env.cache.update(fetched, field, values) # determine the fields that must be processed now; # for the sake of simplicity, we ignore inherited fields @@ -4290,7 +4302,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # @classmethod - def _browse(cls, ids, env, prefetch=None): + def _browse(cls, ids, env, prefetch=None, add_prefetch=True): """ Create a recordset instance. :param ids: a tuple of record ids @@ -4303,7 +4315,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if prefetch is None: prefetch = defaultdict(set) # {model_name: set(ids)} records._prefetch = prefetch - prefetch[cls._name].update(ids) + if add_prefetch: + prefetch[cls._name].update(ids) return records def browse(self, arg=None, prefetch=None): @@ -4342,9 +4355,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ Verifies that the current recorset holds a single record. Raises an exception otherwise. """ - if len(self) == 1: + try: + # unpack to ensure there is only one value is faster than len when true and + # has a significant impact as this check is largely called + _id, = self._ids return self - raise ValueError("Expected singleton: %s" % self) + except ValueError: + raise ValueError("Expected singleton: %s" % self) def with_env(self, env): """ Returns a new version of this recordset attached to the provided @@ -4610,7 +4627,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): def __iter__(self): """ Return an iterator over ``self``. """ for id in self._ids: - yield self._browse((id,), self.env, self._prefetch) + yield self._browse((id,), self.env, self._prefetch, False) def __contains__(self, item): """ Test whether ``item`` (record or field name) is an element of ``self``. |
