[MERGE] forward port branch saas-11.3 up to 2c495502aa
This commit is contained in:
@@ -393,18 +393,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': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (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': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (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
|
||||
@@ -533,7 +535,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
|
||||
@@ -914,7 +916,7 @@ class AccountMoveLine(models.Model):
|
||||
for line in self:
|
||||
company_ids.add(line.company_id.id)
|
||||
all_accounts.append(line.account_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.'))
|
||||
@@ -1062,6 +1064,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
|
||||
####################################################
|
||||
@@ -1495,90 +1547,91 @@ 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
|
||||
percentage_before = percentage_before_rec[move.id]
|
||||
percentage_after = move.line_ids[0]._get_matched_percentage()[move.id]
|
||||
# update the percentage before as the move can be part of
|
||||
# multiple partial reconciliations
|
||||
percentage_before_rec[move.id] = percentage_after
|
||||
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
|
||||
percentage_before = percentage_before_rec[move.id]
|
||||
percentage_after = move.line_ids[0]._get_matched_percentage()[move.id]
|
||||
# update the percentage before as the move can be part of
|
||||
# multiple partial reconciliations
|
||||
percentage_before_rec[move.id] = percentage_after
|
||||
|
||||
for line in move.line_ids:
|
||||
if not line.tax_exigible:
|
||||
#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.filtered(lambda t: t.tax_exigibility == 'on_payment'):
|
||||
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,
|
||||
for line in move.line_ids:
|
||||
if not line.tax_exigible:
|
||||
#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.filtered(lambda t: t.tax_exigibility == 'on_payment'):
|
||||
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 date.min) and newly_created_move.date != move_date:
|
||||
# The move date should be the maximum date between payment and invoice (in case
|
||||
|
||||
@@ -900,11 +900,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_decimals(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_decimals(amount_currency, other_precision)
|
||||
amount_currency = utils.round_decimals(amount_currency, other_precision);
|
||||
}
|
||||
line.balance = {
|
||||
amount: total,
|
||||
|
||||
@@ -96,7 +96,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." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
|
||||
<div class="text-muted">
|
||||
Compute tax rates based on U.S. ZIP codes
|
||||
</div>
|
||||
|
||||
@@ -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:
|
||||
@@ -331,6 +333,33 @@ class AccountVoucher(models.Model):
|
||||
self.env['account.move.line'].create(temp)
|
||||
|
||||
self.env['account.move.line'].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':
|
||||
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)
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 for="module_file" string="Select module package to import (.zip file):" colspan="4"/>
|
||||
<field name="module_file" colspan="4"/>
|
||||
|
||||
@@ -947,7 +947,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"/>
|
||||
@@ -1009,7 +1008,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="search_view_id" ref="crm.view_crm_case_opportunities_filter"/>
|
||||
|
||||
@@ -442,7 +442,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,
|
||||
|
||||
@@ -247,7 +247,10 @@ class HolidaysType(models.Model):
|
||||
if record.allocation_type != 'no':
|
||||
name = "%(name)s (%(count)s)" % {
|
||||
'name': name,
|
||||
'count': _('%g remaining out of %g') % (float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0, float_round(record.max_leaves, precision_digits=2) or 0.0)
|
||||
'count': _('%g remaining out of %g') % (
|
||||
float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0,
|
||||
float_round(record.max_leaves, precision_digits=2) or 0.0,
|
||||
)
|
||||
}
|
||||
res.append((record.id, name))
|
||||
return res
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -190,5 +194,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>
|
||||
|
||||
@@ -715,6 +715,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'})
|
||||
|
||||
def action_view_sent(self):
|
||||
|
||||
@@ -492,13 +492,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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._convert(
|
||||
|
||||
@@ -259,7 +259,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)]
|
||||
|
||||
@@ -5,18 +5,14 @@
|
||||
<t t-call="web.html_container">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="o" t-value="o.with_context({'lang':o.partner_id.lang})" />
|
||||
<t t-if="o.move_lines and o.move_lines[0].partner_id" name="partner_header">
|
||||
<t t-set="partner" t-value="o.partner_id or (o.move_lines and o.move_lines[0].partner_id) or False"/>
|
||||
<t t-if="partner" name="partner_header">
|
||||
<t t-set="address">
|
||||
<div t-field="o.move_lines[0].partner_id"
|
||||
<div t-esc="parent"
|
||||
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="not (o.move_lines and o.move_lines[0].partner_id) and o.partner_id" name="partner_header">
|
||||
<t t-set="address">
|
||||
<div t-field="o.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<div class="page">
|
||||
<h2>
|
||||
<span t-field="o.name"/>
|
||||
|
||||
@@ -247,12 +247,9 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<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')]}" 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="picking_type_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
<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="backorder_id" readonly="1" attrs="{'invisible': [('backorder_id','=',False)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
@@ -443,8 +443,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);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -243,7 +243,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 () {
|
||||
|
||||
@@ -987,6 +987,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);
|
||||
|
||||
|
||||
@@ -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');
|
||||
@@ -54,7 +56,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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
=====================
|
||||
|
||||
|
||||
@@ -369,6 +369,26 @@ class TestFields(common.TransactionCase):
|
||||
self.assertEqual(record.bar3, 'C')
|
||||
self.assertCountEqual(log, ['compute'])
|
||||
|
||||
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_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')
|
||||
@@ -771,6 +791,10 @@ class TestFields(common.TransactionCase):
|
||||
self.env['ir.property'].create({'name': 'foo', 'fields_id': field_tag_id.id,
|
||||
'value': tag0, 'type': 'many2one'})
|
||||
|
||||
# assumption: users don't have access to 'ir.property'
|
||||
accesses = self.env['ir.model.access'].search([('model_id.model', '=', 'ir.property')])
|
||||
accesses.write(dict.fromkeys(['perm_read', 'perm_write', 'perm_create', 'perm_unlink'], False))
|
||||
|
||||
# create/modify a record, and check the value for each user
|
||||
record = self.env['test_new_api.company'].create({
|
||||
'foo': 'main',
|
||||
@@ -824,6 +848,13 @@ class TestFields(common.TransactionCase):
|
||||
self.assertEqual(record.sudo(user1).foo, False)
|
||||
self.assertEqual(record.sudo(user2).foo, 'default')
|
||||
|
||||
# set field with 'force_company' in context
|
||||
record.sudo(user0).with_context(force_company=company1.id).foo = 'beta'
|
||||
record.invalidate_cache()
|
||||
self.assertEqual(record.sudo(user0).foo, 'main')
|
||||
self.assertEqual(record.sudo(user1).foo, 'beta')
|
||||
self.assertEqual(record.sudo(user2).foo, 'default')
|
||||
|
||||
# create company record and attribute
|
||||
company_record = self.env['test_new_api.company'].create({'foo': 'ABC'})
|
||||
attribute_record = self.env['test_new_api.company.attr'].create({
|
||||
@@ -843,6 +874,25 @@ class TestFields(common.TransactionCase):
|
||||
self.assertEqual(attribute_record.bar, 'DEFDEF')
|
||||
self.assertFalse(self.env.has_todo())
|
||||
|
||||
# add group on company-dependent field
|
||||
self.assertFalse(user0.has_group('base.group_system'))
|
||||
self.patch(type(record).foo, 'groups', 'base.group_system')
|
||||
with self.assertRaises(AccessError):
|
||||
record.sudo(user0).foo = 'forbidden'
|
||||
|
||||
user0.write({'groups_id': [(4, self.env.ref('base.group_system').id)]})
|
||||
record.sudo(user0).foo = 'yes we can'
|
||||
|
||||
# add ir.rule to prevent access on record
|
||||
self.assertTrue(user0.has_group('base.group_user'))
|
||||
rule = self.env['ir.rule'].create({
|
||||
'model_id': self.env['ir.model']._get_id(record._name),
|
||||
'groups': [self.env.ref('base.group_user').id],
|
||||
'domain_force': str([('id', '!=', record.id)]),
|
||||
})
|
||||
with self.assertRaises(AccessError):
|
||||
record.sudo(user0).foo = 'forbidden'
|
||||
|
||||
def test_30_read(self):
|
||||
""" test computed fields as returned by read(). """
|
||||
discussion = self.env.ref('test_new_api.discussion_0')
|
||||
|
||||
+8
-3
@@ -55,7 +55,7 @@ from weakref import WeakSet
|
||||
from decorator import decorate, decorator
|
||||
from werkzeug.local import Local, release_local
|
||||
|
||||
from odoo.tools import frozendict, classproperty, StackMap
|
||||
from odoo.tools import frozendict, classproperty, StackMap, pycompat
|
||||
from odoo.exceptions import CacheMiss
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -1036,7 +1036,7 @@ class Cache(object):
|
||||
""" Return the value of ``field`` for ``record``. """
|
||||
key = record.env.cache_key(field)
|
||||
try:
|
||||
value = self._data[key][field][record.id]
|
||||
value = self._data[key][field][record._ids[0]]
|
||||
except KeyError:
|
||||
raise CacheMiss(record, field)
|
||||
|
||||
@@ -1045,7 +1045,12 @@ class Cache(object):
|
||||
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``. """
|
||||
|
||||
+18
-4
@@ -643,13 +643,25 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
return model.env['ir.property'].get(self.name, self.model_name)
|
||||
|
||||
def _compute_company_dependent(self, records):
|
||||
Property = records.env['ir.property']
|
||||
# read property as superuser, as the current user may not have access
|
||||
context = records.env.context
|
||||
if 'force_company' not in context:
|
||||
field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name)
|
||||
company = records.env['res.company']._company_default_get(self.model_name, field_id)
|
||||
context = dict(context, force_company=company.id)
|
||||
Property = records.env(user=SUPERUSER_ID, context=context)['ir.property']
|
||||
values = Property.get_multi(self.name, self.model_name, records.ids)
|
||||
for record in records:
|
||||
record[self.name] = values.get(record.id)
|
||||
|
||||
def _inverse_company_dependent(self, records):
|
||||
Property = records.env['ir.property']
|
||||
# update property as superuser, as the current user may not have access
|
||||
context = records.env.context
|
||||
if 'force_company' not in context:
|
||||
field_id = records.env['ir.model.fields']._get_id(self.model_name, self.name)
|
||||
company = records.env['res.company']._company_default_get(self.model_name, field_id)
|
||||
context = dict(context, force_company=company.id)
|
||||
Property = records.env(user=SUPERUSER_ID, context=context)['ir.property']
|
||||
values = {
|
||||
record.id: self.convert_to_write(record[self.name], record)
|
||||
for record in records
|
||||
@@ -2129,7 +2141,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:
|
||||
@@ -2249,7 +2262,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
|
||||
|
||||
+42
-20
@@ -2767,20 +2767,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
|
||||
|
||||
@@ -2875,36 +2877,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
|
||||
@@ -3266,6 +3278,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
with self.env.protecting(protected_fields, self):
|
||||
# write stored fields with (low-level) method _write
|
||||
if store_vals or inverse_vals or inherited_vals:
|
||||
# if log_access is enabled, this updates 'write_date' and
|
||||
# 'write_uid' and check access rules, even when old_vals is
|
||||
# empty
|
||||
self._write(store_vals)
|
||||
|
||||
# update parent records (after possibly updating parent fields)
|
||||
@@ -3282,6 +3297,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
self.env[model_name].browse(parent_ids).write(parent_vals)
|
||||
|
||||
if inverse_vals:
|
||||
self.check_field_access_rights('write', list(inverse_vals))
|
||||
|
||||
self.modified(set(inverse_vals) - set(store_vals))
|
||||
|
||||
# group fields by inverse method (to call it once), and order
|
||||
@@ -4612,7 +4629,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
|
||||
@@ -4625,7 +4642,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):
|
||||
@@ -4664,9 +4682,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
|
||||
@@ -4932,7 +4954,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``.
|
||||
|
||||
Reference in New Issue
Block a user