diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 4f6685f7fc0..e292ed69818 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -521,11 +521,16 @@ class AccountJournal(models.Model):
@api.multi
def write(self, vals):
for journal in self:
+ company = journal.company_id
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its company.'))
- if self.bank_account_id:
- self.bank_account_id.company_id = vals['company_id']
+ company = self.env['res.company'].browse(vals['company_id'])
+ if self.bank_account_id.company_id and self.bank_account_id.company_id != company:
+ self.bank_account_id.write({
+ 'company_id': company.id,
+ 'partner_id': company.partner_id.id,
+ })
if ('code' in vals and journal.code != vals['code']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
@@ -541,8 +546,14 @@ class AccountJournal(models.Model):
self.default_credit_account_id.currency_id = vals['currency_id']
if self.bank_account_id:
self.bank_account_id.currency_id = vals['currency_id']
- if 'bank_account_id' in vals and not vals.get('bank_account_id'):
- raise UserError(_('You cannot remove the bank account from the journal once set.'))
+ if 'bank_account_id' in vals:
+ if not vals.get('bank_account_id'):
+ raise UserError(_('You cannot remove the bank account from the journal once set.'))
+ else:
+ bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id'])
+ if bank_account.partner_id != company.partner_id:
+ raise UserError(_("The partners of the journal's company and the related bank account mismatch."))
+
result = super(AccountJournal, self).write(vals)
# Create the bank_account_id if necessary
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index dcd4159bce7..9296124eec2 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -690,23 +690,8 @@ class AccountBankStatementLine(models.Model):
aml_dict['currency_id'] = statement_currency.id
# Create write-offs
- # When we register a payment on an invoice, the write-off line contains the amount
- # currency if all related invoices have the same currency. We apply the same logic in
- # the manual reconciliation.
- counterpart_aml = self.env['account.move.line']
- for aml_dict in counterpart_aml_dicts:
- counterpart_aml |= aml_dict.get('move_line', self.env['account.move.line'])
- new_aml_currency = False
- if counterpart_aml\
- and len(counterpart_aml.mapped('currency_id')) == 1\
- and counterpart_aml[0].currency_id\
- and counterpart_aml[0].currency_id != company_currency:
- new_aml_currency = counterpart_aml[0].currency_id
for aml_dict in new_aml_dicts:
aml_dict['payment_id'] = payment and payment.id or False
- if new_aml_currency and not aml_dict.get('currency_id'):
- aml_dict['currency_id'] = new_aml_currency.id
- aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], new_aml_currency)
aml_obj.with_context(check_move_validity=False, apply_taxes=True).create(aml_dict)
# Create counterpart move lines and reconcile them
@@ -717,9 +702,6 @@ class AccountBankStatementLine(models.Model):
aml_dict['payment_id'] = payment and payment.id or False
counterpart_move_line = aml_dict.pop('move_line')
- if counterpart_move_line.currency_id and counterpart_move_line.currency_id != company_currency and not aml_dict.get('currency_id'):
- aml_dict['currency_id'] = counterpart_move_line.currency_id.id
- aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], counterpart_move_line.currency_id)
new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict)
(new_aml | counterpart_move_line).reconcile()
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 47d7bcaf446..3f78e546e36 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -224,7 +224,7 @@ class AccountInvoice(models.Model):
@api.depends('move_id.line_ids.amount_residual')
def _compute_payments(self):
payment_lines = set()
- for line in self.move_id.line_ids:
+ for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id):
payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id'))
payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id'))
self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines))
@@ -267,7 +267,6 @@ class AccountInvoice(models.Model):
], string='Status', index=True, readonly=True, default='draft',
track_visibility='onchange', copy=False,
help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n"
- " * The 'Pro-forma' status is used when the invoice does not have an invoice number.\n"
" * The 'Open' status is used when user creates invoice, an invoice number is generated. It stays in the open status till the user pays the invoice.\n"
" * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n"
" * The 'Cancelled' status is used when user cancel invoice.")
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index f5196eb02d6..7902ec8ceca 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -472,7 +472,7 @@ class AccountMoveLine(models.Model):
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict')
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account')
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
- company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True)
+ company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True, readonly=True)
counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.")
# TODO: put the invoice link and partner_id on the account_move
@@ -525,7 +525,7 @@ class AccountMoveLine(models.Model):
raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field."))
@api.multi
- @api.constrains('amount_currency')
+ @api.constrains('amount_currency', 'debit', 'credit')
def _check_currency_amount(self):
for line in self:
if line.amount_currency:
@@ -558,7 +558,9 @@ class AccountMoveLine(models.Model):
def _get_pair_to_reconcile(self):
#field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set)
- field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
+ company_currency_id = self[0].account_id.company_id.currency_id
+ account_curreny_id = self[0].account_id.currency_id
+ field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
#reconciliation on bank accounts are special cases as we don't want to set them as reconcilable
#but we still want to reconcile entries that are reversed together in order to clear those lines
#in the bank reconciliation report.
@@ -596,8 +598,9 @@ class AccountMoveLine(models.Model):
#there is no more pair to reconcile so return what move_line are left
if not sm_credit_move or not sm_debit_move:
return self
-
- field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
+ company_currency_id = self[0].account_id.company_id.currency_id
+ account_curreny_id = self[0].account_id.currency_id
+ field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual'
if not sm_debit_move.debit and not sm_debit_move.credit:
#both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry
field = 'amount_residual_currency'
@@ -619,9 +622,20 @@ class AccountMoveLine(models.Model):
#Check for the currency and amount_currency we can set
currency = False
amount_reconcile_currency = 0
- if sm_debit_move.currency_id == sm_credit_move.currency_id and sm_debit_move.currency_id.id:
- currency = sm_credit_move.currency_id.id
- amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency)
+ if sm_debit_move.currency_id == sm_credit_move.currency_id:
+ if sm_debit_move.currency_id.id:
+ currency = sm_credit_move.currency_id.id
+ amount_reconcile_currency = min(sm_debit_move.amount_residual_currency, -sm_credit_move.amount_residual_currency)
+ else:
+ if not sm_debit_move.currency_id or not sm_credit_move.currency_id:
+ # If only one of debit_move or credit_move has a secondary currency, also record the converted amount
+ # in that secondary currency in the partial reconciliation. That allows the exchange difference entry
+ # to be created, in case it is needed.
+ company_currency = sm_debit_move.company_id.currency_id
+ currency = sm_debit_move.currency_id or sm_credit_move.currency_id
+ currency_date = sm_debit_move.currency_id and sm_credit_move.date or sm_debit_move.date
+ amount_reconcile_currency = company_currency.with_context(date=currency_date).compute(amount_reconcile, currency)
+ currency = currency.id
amount_reconcile = min(sm_debit_move.amount_residual, -sm_credit_move.amount_residual)
@@ -1061,7 +1075,7 @@ class AccountMoveLine(models.Model):
return matched_percentage_per_move
@api.model
- def compute_amount_fields(self, amount, src_currency, company_currency, invoice_currency=False):
+ def _compute_amount_fields(self, amount, src_currency, company_currency):
""" Helper function to compute value for fields debit/credit/amount_currency based on an amount and the currencies given in parameter"""
amount_currency = False
currency_id = False
@@ -1071,9 +1085,6 @@ class AccountMoveLine(models.Model):
currency_id = src_currency.id
debit = amount > 0 and amount or 0.0
credit = amount < 0 and -amount or 0.0
- if invoice_currency and invoice_currency != company_currency and not amount_currency:
- amount_currency = src_currency.with_context(self._context).compute(amount, invoice_currency)
- currency_id = invoice_currency.id
return debit, credit, amount_currency, currency_id
def _get_analytic_tag_ids(self):
@@ -1436,7 +1447,8 @@ class AccountPartialReconcile(models.Model):
total_amount_currency = 0
#make sure that all partial reconciliations share the same secondary currency otherwise it's not
#possible to compute the exchange difference entry and it has to be done manually.
- currency = self[0].currency_id
+ currency = self[0].debit_move_id.currency_id or self[0].credit_move_id.currency_id or None
+ more_than_1_currency = False
maxdate = '0000-00-00'
seen = set()
@@ -1444,9 +1456,14 @@ class AccountPartialReconcile(models.Model):
while todo:
partial_rec = todo.pop()
seen.add(partial_rec)
+ if partial_rec.debit_move_id.currency_id != currency or partial_rec.credit_move_id.currency_id != currency:
+ #There's more than 1 secondary currency involved, which means that we can use the total_debit
+ # and total_credit comparison (otherwise we cannot, as we have to rely only on the amount in
+ # secondary currency to deal with the case stated in test_partial_reconcile_currencies_02)
+ more_than_1_currency = True
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
- currency = None
+ currency = False
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
if aml.amount_residual or aml.amount_residual_currency:
@@ -1459,9 +1476,9 @@ class AccountPartialReconcile(models.Model):
total_amount_currency += aml.amount_currency
elif partial_rec.currency_id and partial_rec.currency_id == currency:
#if the aml has no secondary currency but is reconciled with other journal item(s) in secondary currency, the amount
- #currency is recorded on the partial rec and in order to check if the reconciliation is total, we need to convert the
- #aml.balance in that foreign currency
- total_amount_currency += aml.company_id.currency_id.with_context(date=aml.date).compute(aml.balance, partial_rec.currency_id)
+ #in secondary currency is recorded on the partial rec. That allows us to consider it, in order to check if the
+ # reconciliation is total
+ total_amount_currency += aml.balance > 0 and partial_rec.amount_currency or - partial_rec.amount_currency
for x in aml.matched_debit_ids | aml.matched_credit_ids:
if x not in seen:
@@ -1469,15 +1486,16 @@ class AccountPartialReconcile(models.Model):
partial_rec_ids = [x.id for x in seen]
aml_ids = aml_set.ids
- #then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
+ #if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
- if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
+ if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or \
+ ((currency is None or more_than_1_currency) and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0):
exchange_move_id = False
- if currency and aml_to_balance:
+ if aml_to_balance:
exchange_move = self.env['account.move'].create(
self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id))
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
- rate_diff_amls, rate_diff_partial_rec = self.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
+ rate_diff_amls, rate_diff_partial_rec = self.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, aml_to_balance.currency_id, exchange_move)
aml_ids += rate_diff_amls.ids
partial_rec_ids += rate_diff_partial_rec.ids
exchange_move.post()
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index ca1e13511d4..09e50feb664 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -2,6 +2,7 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
+from odoo.tools import float_compare
from itertools import groupby
@@ -544,11 +545,7 @@ class account_payment(models.Model):
Return the journal entry.
"""
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
- invoice_currency = False
- if self.invoice_ids and all([x.currency_id == self.invoice_ids[0].currency_id for x in self.invoice_ids]):
- #if all the invoices selected share the same currency, record the payment in that currency too
- invoice_currency = self.invoice_ids[0].currency_id
- debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id, invoice_currency)
+ debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
move = self.env['account.move'].create(self._get_move_vals())
@@ -561,26 +558,7 @@ class account_payment(models.Model):
#Reconcile with the invoices
if self.payment_difference_handling == 'reconcile' and self.payment_difference:
writeoff_line = self._get_shared_move_line_vals(0, 0, 0, move.id, False)
- amount_currency_wo, currency_id = aml_obj.with_context(date=self.payment_date).compute_amount_fields(self.payment_difference, self.currency_id, self.company_id.currency_id, invoice_currency)[2:]
- # the writeoff debit and credit must be computed from the invoice residual in company currency
- # minus the payment amount in company currency, and not from the payment difference in the payment currency
- # to avoid loss of precision during the currency rate computations. See revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 for a detailed example.
- total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids)
- total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id)
- if self.invoice_ids[0].type in ['in_invoice', 'out_refund']:
- amount_wo = total_payment_company_signed - total_residual_company_signed
- else:
- amount_wo = total_residual_company_signed - total_payment_company_signed
- # Align the sign of the secondary currency writeoff amount with the sign of the writeoff
- # amount in the company currency
- if amount_wo > 0:
- debit_wo = amount_wo
- credit_wo = 0.0
- amount_currency_wo = abs(amount_currency_wo)
- else:
- debit_wo = 0.0
- credit_wo = -amount_wo
- amount_currency_wo = -abs(amount_currency_wo)
+ debit_wo, credit_wo, amount_currency_wo, currency_id = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(self.payment_difference, self.currency_id, self.company_id.currency_id)
writeoff_line['name'] = self.writeoff_label
writeoff_line['account_id'] = self.writeoff_account_id.id
writeoff_line['debit'] = debit_wo
@@ -614,7 +592,7 @@ class account_payment(models.Model):
""" Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconcilable move line
"""
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
- debit, credit, amount_currency, dummy = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
+ debit, credit, amount_currency, dummy = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
amount_currency = self.destination_journal_id.currency_id and self.currency_id.with_context(date=self.payment_date).compute(amount, self.destination_journal_id.currency_id) or 0
dst_move = self.env['account.move'].create(self._get_move_vals(self.destination_journal_id))
@@ -669,6 +647,7 @@ class account_payment(models.Model):
'credit': credit,
'amount_currency': amount_currency or False,
'payment_id': self.id,
+ 'journal_id': self.journal_id.id,
}
def _get_counterpart_move_line_vals(self, invoice=False):
@@ -695,7 +674,6 @@ class account_payment(models.Model):
return {
'name': name,
'account_id': self.destination_account_id.id,
- 'journal_id': self.journal_id.id,
'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False,
}
@@ -713,7 +691,7 @@ class account_payment(models.Model):
# If the journal has a currency specified, the journal item need to be expressed in this currency
if self.journal_id.currency_id and self.currency_id != self.journal_id.currency_id:
amount = self.currency_id.with_context(date=self.payment_date).compute(amount, self.journal_id.currency_id)
- debit, credit, amount_currency, dummy = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id)
+ debit, credit, amount_currency, dummy = self.env['account.move.line'].with_context(date=self.payment_date)._compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id)
vals.update({
'amount_currency': amount_currency,
'currency_id': self.journal_id.currency_id.id,
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 93f17153fa8..d035fe594dc 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -738,6 +738,9 @@ class WizardMultiChartsAccounts(models.TransientModel):
res.setdefault('domain', {})
res['domain']['sale_tax_id'] = repr(sale_tax_domain)
res['domain']['purchase_tax_id'] = repr(purchase_tax_domain)
+ else:
+ self.sale_tax_id = False
+ self.purchase_tax_id = False
if self.chart_template_id.transfer_account_id:
self.transfer_account_id = self.chart_template_id.transfer_account_id.id
if self.chart_template_id.code_digits:
diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py
index 68d8ef3e83b..d4593130d2c 100644
--- a/addons/account/models/res_config_settings.py
+++ b/addons/account/models/res_config_settings.py
@@ -36,12 +36,12 @@ class ResConfigSettings(models.TransientModel):
module_account_budget = fields.Boolean(string='Budget Management')
module_account_payment = fields.Boolean(string='Online Payment')
module_account_reports = fields.Boolean("Dynamic Reports")
- module_account_reports_followup = fields.Boolean("Enable payment followup management")
+ module_account_reports_followup = fields.Boolean("Follow-up Levels")
module_account_check_printing = fields.Boolean("Allow check printing and deposits")
module_account_batch_deposit = fields.Boolean(string='Use batch deposit',
help='This allows you to group received checks before you deposit them to the bank.\n'
'-This installs the module account_batch_deposit.')
- module_account_sepa = fields.Boolean(string='Use SEPA payments')
+ module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)')
module_account_sepa_direct_debit = fields.Boolean(string='Use SEPA Direct Debit')
module_account_plaid = fields.Boolean(string="Plaid Connector")
module_account_yodlee = fields.Boolean("Bank Interface - Sync your bank feeds automatically")
@@ -49,7 +49,7 @@ class ResConfigSettings(models.TransientModel):
module_account_bank_statement_import_ofx = fields.Boolean("Import in .ofx format")
module_account_bank_statement_import_csv = fields.Boolean("Import in .csv format")
module_account_bank_statement_import_camt = fields.Boolean("Import in CAMT.053 format")
- module_currency_rate_live = fields.Boolean(string="Allow Currency Rate Live")
+ module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates")
module_print_docsaway = fields.Boolean(string="Docsaway")
module_product_margin = fields.Boolean(string="Allow Product Margin")
module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index 0140d489817..504a78e05e0 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -13,6 +13,10 @@ class ReportAgedPartnerBalance(models.AbstractModel):
_name = 'report.account.report_agedpartnerbalance'
def _get_partner_move_lines(self, account_type, date_from, target_move, period_length):
+ # This method can receive the context key 'include_nullified_amount' {Boolean}
+ # Do an invoice and a payment and unreconcile. The amount will be nullified
+ # By default, the partner wouldn't appear in this report.
+ # The context key allow it to appear
periods = {}
start = datetime.strptime(date_from, "%Y-%m-%d")
for i in range(5)[::-1]:
@@ -190,7 +194,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
values['name'] = _('Unknown Partner')
values['trust'] = False
- if at_least_one_amount:
+ if at_least_one_amount or self._context.get('include_nullified_amount'):
res.append(values)
return res, total, lines
diff --git a/addons/account/report/account_overdue_report.py b/addons/account/report/account_overdue_report.py
index 4d7e4b87ede..99463c482bb 100644
--- a/addons/account/report/account_overdue_report.py
+++ b/addons/account/report/account_overdue_report.py
@@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel):
"FROM account_move_line l "
"JOIN account_account_type at ON (l.user_type_id = at.id) "
"JOIN account_move m ON (l.move_id = m.id) "
- "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
+ "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),))))
for row in self.env.cr.dictfetchall():
res[row.pop('partner_id')].append(row)
return res
diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py
index e28ab7f0169..68fe77f8f40 100644
--- a/addons/account/report/account_partner_ledger.py
+++ b/addons/account/report/account_partner_ledger.py
@@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel):
full_account = []
currency = self.env['res.currency']
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
- reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
+ reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
query = """
SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code
@@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel):
return
result = 0.0
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
- reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
+ reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
query = """SELECT sum(""" + field + """)
@@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel):
AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),))
data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()]
params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
- reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
+ reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL '
query = """
SELECT DISTINCT "account_move_line".partner_id
FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am
diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py
index 3d490eedc49..af8a66df8ef 100644
--- a/addons/account/tests/test_payment.py
+++ b/addons/account/tests/test_payment.py
@@ -98,9 +98,9 @@ class TestPayment(AccountingTestCase):
self.assertEqual(len(aml_rec), 1, "Expected a move line with values : %s" % str(aml_dict))
if aml_dict.get('currency_diff'):
if aml_rec.credit:
- currency_diff_move = aml_rec.matched_debit_ids.full_reconcile_id.exchange_move_id
+ currency_diff_move = aml_rec.matched_debit_ids[0].full_reconcile_id.exchange_move_id
else:
- currency_diff_move = aml_rec.matched_credit_ids.full_reconcile_id.exchange_move_id
+ currency_diff_move = aml_rec.matched_credit_ids[0].full_reconcile_id.exchange_move_id
for currency_diff_line in currency_diff_move.line_ids:
if aml_dict.get('currency_diff') > 0:
if currency_diff_line.account_id.id == aml_rec.account_id.id:
@@ -139,6 +139,7 @@ class TestPayment(AccountingTestCase):
{'account_id': self.account_eur.id, 'debit': 300.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
{'account_id': inv_1.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 00, 'currency_id': False},
])
+ self.assertTrue(payment.move_line_ids.filtered(lambda l: l.account_id == inv_1.account_id)[0].full_reconcile_id)
liquidity_aml = payment.move_line_ids.filtered(lambda r: r.account_id == self.account_eur)
bank_statement = self.reconcile(liquidity_aml, 200, 0, False)
@@ -364,7 +365,7 @@ class TestPayment(AccountingTestCase):
{'account_id': inv_3.account_id.id, 'debit': 0.0, 'credit': 300.0, 'amount_currency': 0.0, 'currency_id': False},
])
- def test_payment_and_writeoff_in_other_currency(self):
+ def test_payment_and_writeoff_in_other_currency_1(self):
# Use case:
# Company is in EUR, create a customer invoice for 25 EUR and register payment of 25 USD.
# Mark invoice as fully paid with a write_off
@@ -413,3 +414,96 @@ class TestPayment(AccountingTestCase):
{'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 8.65, 'amount_currency': -13.22, 'currency_id': self.currency_usd_id},
{'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 7.7, 'amount_currency': -11.78, 'currency_id': self.currency_usd_id},
])
+
+ def test_payment_and_writeoff_in_other_currency_2(self):
+ # Use case:
+ # Company is in EUR, create a supplier bill of 5325.6 USD and register payment of 5325 USD, at a different rate
+ # Mark invoice as fully paid with a write_off
+ # Check that all the aml are correctly created.
+
+ # Set exchange rates 0.895@2017-11-01 and 0.88@2017-12-01
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 0.895,
+ 'name': time.strftime('%Y') + '-06-26'})
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 0.88,
+ 'name': time.strftime('%Y') + '-07-15'})
+
+ invoice = self.create_invoice(amount=5325.6, type='in_invoice', currency_id=self.currency_usd_id, partner=self.partner_agrolait.id)
+ self.check_journal_items(invoice.move_id.line_ids, [
+ {'account_id': self.account_revenue.id, 'debit': 5950.39, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 5950.39, 'amount_currency': -5325.6, 'currency_id': self.currency_usd_id},
+ ])
+ # register payment on invoice
+ payment = self.payment_model.create({'payment_type': 'outbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'supplier',
+ 'partner_id': self.partner_agrolait.id,
+ 'amount': 5325,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.account_revenue.id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'invoice_ids': [(4, invoice.id, None)]
+ })
+ payment.post()
+ self.check_journal_items(payment.move_line_ids, [
+ {'account_id': self.account_eur.id, 'debit': 0, 'credit': 6051.14, 'amount_currency': -5325.0, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 0.68, 'amount_currency': -0.6, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 6051.82, 'credit': 0.0, 'amount_currency': 5325.6, 'currency_id': self.currency_usd_id, 'currency_diff': -101.43},
+ ])
+
+ #check the invoice status
+ self.assertEqual(invoice.state, 'paid')
+
+ def test_payment_and_writeoff_in_other_currency_3(self):
+ # Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
+ # Invoice made in secondary currency for which the rate to the company currency
+ # is higher than the foreign currency decimal precision.
+ # E.g: Company currency is EUR, create a customer invoice of 247590.40 EUR and
+ # register payment of 267 USD (1 USD = 948 EUR)
+ # Mark invoice as fully paid with a write_off
+ # Check that all the aml are correctly created and that the invoice is paid
+
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 1,
+ 'name': time.strftime('%Y') + '-06-26'})
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_eur_id,
+ 'rate': 948,
+ 'name': time.strftime('%Y') + '-06-26'})
+
+ invoice = self.create_invoice(amount=247590.4, type='out_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ self.check_journal_items(invoice.move_id.line_ids, [
+ {'account_id': self.account_receivable.id, 'debit': 247590.4, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
+ {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 247590.4, 'amount_currency': 0.0, 'currency_id': False},
+ ])
+ # register payment on invoice
+ payment = self.payment_model.create({'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait.id,
+ 'amount': 267,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-06-26',
+ 'payment_difference_handling': 'reconcile',
+ 'writeoff_account_id': self.account_revenue.id,
+ 'journal_id': self.bank_journal_euro.id,
+ 'invoice_ids': [(4, invoice.id, None)],
+ 'name': 'test_payment_and_writeoff_in_other_currency_3',
+ })
+ payment.post()
+ self.check_journal_items(payment.move_line_ids, [
+ {'account_id': self.account_eur.id, 'debit': 253116.0, 'credit': 0.0, 'amount_currency': 267.0, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_revenue.id, 'debit': 0.0, 'credit': 5526.84, 'amount_currency': -5.83, 'currency_id': self.currency_usd_id},
+ {'account_id': self.account_receivable.id, 'debit': 0.0, 'credit': 247589.16, 'amount_currency': -261.17, 'currency_id': self.currency_usd_id},
+ ])
+
+ # Check the invoice status and the full reconciliation: the difference on the receivable account
+ # should have been completed by an exchange rate difference entry
+ self.assertEqual(invoice.state, 'paid')
+ self.assertTrue(invoice.move_id.line_ids.filtered(lambda l: l.account_id == self.account_receivable)[0].full_reconcile_id)
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index 1180df172c9..4840e40f41a 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -103,6 +103,7 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(move_line.currency_id.id, aml_dict[move_line.account_id.id]['currency_id'])
if 'currency_diff' in aml_dict[move_line.account_id.id]:
currency_diff_move = move_line.full_reconcile_id.exchange_move_id
+ self.assertTrue(currency_diff_move, 'The full reconciliation should have created an exchange rate journal entry')
for currency_diff_line in currency_diff_move.line_ids:
if aml_dict[move_line.account_id.id].get('currency_diff') == 0:
if currency_diff_line.account_id.id == move_line.account_id.id:
@@ -196,11 +197,11 @@ class TestReconciliation(AccountingTestCase):
customer_move_lines, supplier_move_lines = self.make_customer_and_supplier_flows(self.currency_usd_id, 50, self.bank_journal_euro, 40, 0.0, False)
self.check_results(customer_move_lines, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
- self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': -61.16, 'currency_id': self.currency_usd_id},
+ self.account_rcv.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False},
})
self.check_results(supplier_move_lines, {
self.account_euro.id: {'debit': 0.0, 'credit': 40.0, 'amount_currency': 0.0, 'currency_id': False},
- self.account_rcv.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 61.16, 'currency_id': self.currency_usd_id},
+ self.account_rcv.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
})
def test_statement_euro_invoice_usd_transaction_chf(self):
@@ -238,7 +239,7 @@ class TestReconciliation(AccountingTestCase):
'move_line': line_id,
'debit': 0.0,
'credit': 32.7,
- 'name': line_id.name,
+ 'name': 'test_statement_euro_invoice_usd_transaction_euro_full',
}], new_aml_dicts=[{
'debit': 0.0,
'credit': 7.3,
@@ -247,8 +248,8 @@ class TestReconciliation(AccountingTestCase):
}])
self.check_results(bank_stmt.move_line_ids, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
- self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': -41.97, 'currency_id': self.currency_usd_id, 'currency_diff': 0, 'amount_currency_diff': -8.03},
- self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': -9.37, 'currency_id': self.currency_usd_id},
+ self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': 0, 'currency_id': False, 'currency_diff': 0, 'amount_currency_diff': -8.03},
+ self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': 0, 'currency_id': False},
})
# The invoice should be paid, as the payments totally cover its total
@@ -480,7 +481,7 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
- def test_partial_reconcile_currencies(self):
+ def test_partial_reconcile_currencies_01(self):
# client Account (payable, rsa)
# Debit Credit
# --------------------------------------------------------
@@ -623,3 +624,86 @@ class TestReconciliation(AccountingTestCase):
# Checking if the direction of the move is correct
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
self.assertEqual(full_rec_payable.balance, 18.75)
+
+ def test_partial_reconcile_currencies_02(self):
+ ####
+ # Day 1: Invoice Cust/001 to customer (expressed in USD)
+ # Market value of USD (day 1): 1 USD = 0.5 EUR
+ # * Dr. 100 USD / 50 EUR - Accounts receivable
+ # * Cr. 100 USD / 50 EUR - Revenue
+ ####
+ account_revenue = self.env['account.account'].search(
+ [('user_type_id', '=', self.env.ref(
+ 'account.data_account_type_revenue').id)], limit=1)
+ dest_journal_id = self.env['account.journal'].search(
+ [('type', '=', 'purchase'),
+ ('company_id', '=', self.env.ref('base.main_company').id)],
+ limit=1)
+
+ # Delete any old rate - to make sure that we use the ones we need.
+ old_rates = self.env['res.currency.rate'].search(
+ [('currency_id', '=', self.currency_usd_id)])
+ old_rates.unlink()
+
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'name': time.strftime('%Y') + '-01-01',
+ 'rate': 2,
+ })
+
+ invoice_cust_1 = self.account_invoice_model.create({
+ 'partner_id': self.partner_agrolait_id,
+ 'account_id': self.account_rcv.id,
+ 'type': 'out_invoice',
+ 'currency_id': self.currency_usd_id,
+ 'date_invoice': time.strftime('%Y') + '-01-01',
+ })
+ self.account_invoice_line_model.create({
+ 'quantity': 1.0,
+ 'price_unit': 100.0,
+ 'invoice_id': invoice_cust_1.id,
+ 'name': 'product that cost 100',
+ 'account_id': account_revenue.id,
+ })
+ invoice_cust_1.action_invoice_open()
+ self.assertEqual(invoice_cust_1.residual_company_signed, 50.0)
+ aml = invoice_cust_1.move_id.mapped('line_ids').filtered(
+ lambda x: x.account_id == account_revenue)
+ self.assertEqual(aml.credit, 50.0)
+ #####
+ # Day 2: Receive payment for half invoice Cust/1 (in USD)
+ # -------------------------------------------------------
+ # Market value of USD (day 2): 1 USD = 1 EUR
+
+ # Payment transaction:
+ # * Dr. 50 USD / 50 EUR - EUR Bank (valued at market price
+ # at the time of receiving the money)
+ # * Cr. 50 USD / 50 EUR - Accounts Receivable
+ #####
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'name': time.strftime('%Y') + '-01-02',
+ 'rate': 1,
+ })
+ # register payment on invoice
+ payment = self.env['account.payment'].create(
+ {'payment_type': 'inbound',
+ 'payment_method_id': self.env.ref(
+ 'account.account_payment_method_manual_in').id,
+ 'partner_type': 'customer',
+ 'partner_id': self.partner_agrolait_id,
+ 'amount': 50,
+ 'currency_id': self.currency_usd_id,
+ 'payment_date': time.strftime('%Y') + '-01-02',
+ 'journal_id': dest_journal_id.id,
+ })
+ payment.post()
+ payment_move_line = False
+ for l in payment.move_line_ids:
+ if l.account_id == invoice_cust_1.account_id:
+ payment_move_line = l
+ invoice_cust_1.register_payment(payment_move_line)
+ # We expect at this point that the invoice should still be open,
+ # because they owe us still 50 CC.
+ self.assertEqual(invoice_cust_1.state, 'open',
+ 'Invoice is in status %s' % invoice_cust_1.state)
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 30b9382e607..c3af025c5e6 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -256,7 +256,7 @@
-
+
@@ -303,7 +303,7 @@
-
+
@@ -401,7 +401,7 @@
-
+
@@ -504,7 +504,7 @@
-
+
diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml
index 179699fa726..e02640721fe 100644
--- a/addons/account/views/res_config_settings_views.xml
+++ b/addons/account/views/res_config_settings_views.xml
@@ -176,7 +176,7 @@
-
+
Update exchange rates automatically
@@ -190,7 +190,7 @@
-
+
Use follow-up levels and schedule actions
@@ -256,7 +256,7 @@
-
+
Pay your bills in one-click using Euro SEPA service
diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py
index 36636e05731..73cbff6aa52 100644
--- a/addons/account_voucher/models/account_voucher.py
+++ b/addons/account_voucher/models/account_voucher.py
@@ -139,9 +139,16 @@ class AccountVoucher(models.Model):
@api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self):
+ pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
if self.partner_id:
self.account_id = self.partner_id.property_account_receivable_id \
if self.voucher_type == 'sale' else self.partner_id.property_account_payable_id
+ else:
+ if self.voucher_type == 'purchase':
+ pay_journal_domain.append(('outbound_payment_method_ids', '!=', False))
+ else:
+ pay_journal_domain.append(('inbound_payment_method_ids', '!=', False))
+ return {'domain': {'payment_journal_id': pay_journal_domain}}
@api.multi
def proforma_voucher(self):
diff --git a/addons/auth_crypt/models/res_users.py b/addons/auth_crypt/models/res_users.py
index ad66d0c7025..ff7e983bc64 100644
--- a/addons/auth_crypt/models/res_users.py
+++ b/addons/auth_crypt/models/res_users.py
@@ -96,3 +96,6 @@ class ResUsers(models.Model):
internally
"""
return default_crypt_context
+
+ def _get_session_token_fields(self):
+ return super(ResUsers, self)._get_session_token_fields() | {'password_crypt'}
diff --git a/addons/auth_oauth/models/res_users.py b/addons/auth_oauth/models/res_users.py
index fa052e41a1b..3657885c3ac 100644
--- a/addons/auth_oauth/models/res_users.py
+++ b/addons/auth_oauth/models/res_users.py
@@ -117,3 +117,6 @@ class ResUsers(models.Model):
res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)])
if not res:
raise
+
+ def _get_session_token_fields(self):
+ return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'}
diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py
index dff5de888c7..c35430d1f92 100644
--- a/addons/base_address_city/models/res_partner.py
+++ b/addons/base_address_city/models/res_partner.py
@@ -4,6 +4,8 @@
from lxml import etree
from odoo import api, models, fields
+from odoo.tools.translate import _
+
class Partner(models.Model):
_inherit = 'res.partner'
@@ -24,14 +26,15 @@ class Partner(models.Model):
doc = etree.fromstring(arch)
if doc.xpath("//field[@name='city_id']"):
return arch
+ label = _('City')
for city_node in doc.xpath("//field[@name='city']"):
replacement_xml = """
-
-
+
+
- """
+ """ % (label, label, label)
city_id_node = etree.fromstring(replacement_xml)
city_node.getparent().replace(city_node, city_id_node)
diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py
index 063ebf6a829..a781a314686 100644
--- a/addons/base_setup/models/res_config_settings.py
+++ b/addons/base_setup/models/res_config_settings.py
@@ -85,6 +85,8 @@ class ResConfigSettings(models.TransientModel):
@api.multi
def edit_external_header(self):
+ if not self.external_report_layout:
+ return False
return self._prepare_report_view_action('web.external_layout_' + self.external_report_layout)
@api.multi
diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml
index 24625320191..e798d5fd1eb 100644
--- a/addons/crm/report/crm_opportunity_report_views.xml
+++ b/addons/crm/report/crm_opportunity_report_views.xml
@@ -55,14 +55,14 @@
crm.opportunity.report[('probability', '=', 100)]
- {'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count']}
+ {'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count__']}Won by Channelcrm.opportunity.report[('probability', '=', 100)]
- {'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected revenue']}
+ {'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected_revenue']}Won by Country
@@ -75,7 +75,7 @@
Expected Revenue by Channelcrm.opportunity.report
- {'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count']}
+ {'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count__']}
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index 9a64a61fe68..ddaa57c01f6 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -251,8 +251,6 @@
-
-
@@ -274,6 +272,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/event_sale/wizard/event_edit_registration.xml b/addons/event_sale/wizard/event_edit_registration.xml
index 47bb964b0f2..5adc5d50bae 100644
--- a/addons/event_sale/wizard/event_edit_registration.xml
+++ b/addons/event_sale/wizard/event_edit_registration.xml
@@ -11,7 +11,7 @@
-
+
diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py
index 20370a4a32d..60fa38e7b14 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr.py
@@ -127,7 +127,8 @@ class Employee(models.Model):
], groups="hr.group_hr_user", default="male")
marital = fields.Selection([
('single', 'Single'),
- ('married', 'Married (or similar)'),
+ ('married', 'Married'),
+ ('cohabitant', 'Legal Cohabitant'),
('widower', 'Widower'),
('divorced', 'Divorced')
], string='Marital Status', groups="hr.group_hr_user", default='single')
diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js
index c2c425d2612..69599fa39e3 100644
--- a/addons/hr_org_chart/static/src/js/hr_org_chart.js
+++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js
@@ -13,6 +13,7 @@ var FieldOrgChart = AbstractField.extend({
events: {
"click .o_employee_redirect": "_onEmployeeRedirect",
+ "click .o_employee_sub_redirect": "_onEmployeeSubRedirect",
},
/**
* @constructor
diff --git a/addons/hr_payroll/models/hr_salary_rule.py b/addons/hr_payroll/models/hr_salary_rule.py
index 95c06cbcb56..74052e7b290 100644
--- a/addons/hr_payroll/models/hr_salary_rule.py
+++ b/addons/hr_payroll/models/hr_salary_rule.py
@@ -77,7 +77,7 @@ class HrSalaryRuleCategory(models.Model):
_name = 'hr.salary.rule.category'
_description = 'Salary Rule Category'
- name = fields.Char(required=True)
+ name = fields.Char(required=True, translate=True)
code = fields.Char(required=True)
parent_id = fields.Many2one('hr.salary.rule.category', string='Parent',
help="Linking a salary category to its parent is used only for the reporting purpose.")
@@ -90,7 +90,7 @@ class HrSalaryRuleCategory(models.Model):
class HrSalaryRule(models.Model):
_name = 'hr.salary.rule'
- name = fields.Char(required=True)
+ name = fields.Char(required=True, translate=True)
code = fields.Char(required=True,
help="The code of salary rules can be used as reference in computation of other rules. "
"In that case, it is case sensitive.")
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index ddfd68e0d56..e37f3139e57 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -18,7 +18,7 @@ class AccountAnalyticLine(models.Model):
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
employee_id = fields.Many2one('hr.employee', "Employee")
- department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True)
+ department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True)
@api.onchange('project_id')
def onchange_project_id(self):
@@ -28,6 +28,11 @@ class AccountAnalyticLine(models.Model):
def _onchange_employee_id(self):
self.user_id = self.employee_id.user_id
+ @api.depends('employee_id')
+ def _compute_department_id(self):
+ for line in self:
+ line.department_id = line.employee_id.department_id
+
@api.model
def create(self, vals):
vals = self._timesheet_preprocess(vals)
diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py
index 9cd8776eaf7..86a01497b46 100644
--- a/addons/hw_scanner/controllers/main.py
+++ b/addons/hw_scanner/controllers/main.py
@@ -4,7 +4,7 @@
import logging
import time
from os import listdir
-from os.path import join
+from os.path import join, isdir
try:
from queue import Queue, Empty
except ImportError:
@@ -124,7 +124,7 @@ class Scanner(Thread):
if not evdev:
return []
- if not os.path.isdir(self.input_dir):
+ if not isdir(self.input_dir):
return []
new_devices = [device for device in listdir(self.input_dir)
diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py
index 0e140f9de7f..a4200047315 100644
--- a/addons/im_livechat/models/mail_channel.py
+++ b/addons/im_livechat/models/mail_channel.py
@@ -101,12 +101,6 @@ class MailChannel(models.Model):
empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()]
self.browse(empty_channel_ids).unlink()
- @api.model
- def get_empty_list_help(self, help):
- if help:
- help
- return super(MailChannel, self).get_empty_list_help(help)
-
def _define_command_history(self):
return {
'channel_types': ['livechat'],
diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml
index 0c8d8ec72bb..cc4133b46ab 100644
--- a/addons/im_livechat/views/im_livechat_channel_views.xml
+++ b/addons/im_livechat/views/im_livechat_channel_views.xml
@@ -106,10 +106,11 @@
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 166a0a44506..2df63cf1e66 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -119,6 +119,7 @@ class ProductTemplate(models.Model):
active = fields.Boolean('Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.")
color = fields.Integer('Color Index')
+ is_product_variant = fields.Boolean(string='Is a product variant', compute='_compute_is_product_variant')
attribute_line_ids = fields.One2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes')
product_variant_ids = fields.One2many('product.product', 'product_tmpl_id', 'Products', required=True)
# performance: product_variant_id provides prefetching on the first product variant only
@@ -236,6 +237,13 @@ class ProductTemplate(models.Model):
for template in (self - unique_variants):
template.weight = 0.0
+ def _compute_is_product_variant(self):
+ for template in self:
+ if template._name == 'product.template':
+ template.is_product_variant = False
+ else:
+ template.is_product_variant = True
+
@api.model
def _get_weight_uom_id_from_ir_config_parameter(self):
""" Get the unit of measure to interpret the `weight` field. By default, we considerer
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index d4ffa71f15f..84c383620af 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -16,6 +16,7 @@
+
diff --git a/addons/sale_margin/__init__.py b/addons/sale_margin/__init__.py
index c76a7ba77c4..0d226707e04 100644
--- a/addons/sale_margin/__init__.py
+++ b/addons/sale_margin/__init__.py
@@ -1,20 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from functools import partial
-import odoo
-from odoo import api, SUPERUSER_ID
-
from . import models # noqa
from . import report # noqa
-
-
-def uninstall_hook(cr, registry):
- def recreate_view(dbname):
- db_registry = odoo.modules.registry.Registry.new(dbname)
- with api.Environment.manage(), db_registry.cursor() as cr:
- env = api.Environment(cr, SUPERUSER_ID, {})
- if 'sale.report' in env:
- env['sale.report'].init()
-
- cr.after("commit", partial(recreate_view, cr.dbname))
diff --git a/addons/sale_margin/__manifest__.py b/addons/sale_margin/__manifest__.py
index 7ead9ff4c02..0da258e4932 100644
--- a/addons/sale_margin/__manifest__.py
+++ b/addons/sale_margin/__manifest__.py
@@ -15,5 +15,4 @@ Price and Cost Price.
'depends':['sale_management'],
'demo':['data/sale_margin_demo.xml'],
'data':['security/ir.model.access.csv','views/sale_margin_view.xml'],
- 'uninstall_hook': "uninstall_hook",
}
diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py
index ebb1ee59d9c..cef63155645 100644
--- a/addons/sale_stock/models/stock.py
+++ b/addons/sale_stock/models/stock.py
@@ -50,17 +50,6 @@ class StockPicking(models.Model):
sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True)
- @api.multi
- def _create_backorder(self, backorder_moves=[]):
- res = super(StockPicking, self)._create_backorder(backorder_moves)
- for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'):
- backorder = picking.search([('backorder_id', '=', picking.id)])
- if backorder.sale_id:
- backorder.message_post_with_view(
- 'mail.message_origin_link',
- values={'self': backorder, 'origin': backorder.sale_id},
- subtype_id=self.env.ref('mail.mt_note').id)
- return res
def _log_less_quantities_than_expected(self, moves):
""" Log an activity on sale order that are linked to moves. The
@@ -102,4 +91,4 @@ class StockPicking(models.Model):
documents = self._log_activity_get_documents(moves, 'sale_line_id', 'DOWN', _keys_in_sorted, _keys_in_groupby)
self._log_activity(_render_note_exception_quantity, documents)
- return super(StockPicking, self)._log_less_quantities_than_expected(moves)
+ return super(StockPicking, self)._log_less_quantities_than_expected(moves)
\ No newline at end of file
diff --git a/addons/stock/data/stock_data.xml b/addons/stock/data/stock_data.xml
index 70fe78291f1..bc37a9bb572 100644
--- a/addons/stock/data/stock_data.xml
+++ b/addons/stock/data/stock_data.xml
@@ -120,7 +120,6 @@
- WH
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 92f3a9d1351..379ae43a456 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1073,18 +1073,8 @@ class StockMove(models.Model):
return moves_todo
if picking:
- moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
- if moves_to_backorder:
- backorder_picking = picking.copy({
- 'name': '/',
- 'move_lines': [],
- 'move_line_ids': [],
- 'backorder_id': picking.id
- })
- picking.message_post(body=_('The backorder %s has been created.') % (backorder_picking.id, backorder_picking.name))
- moves_to_backorder.write({'picking_id': backorder_picking.id})
- moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
- moves_to_backorder._action_assign()
+ picking._create_backorder()
+
return moves_todo
def unlink(self):
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index f812df8ea35..fa6f4fae341 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -378,9 +378,9 @@ class Picking(models.Model):
@api.depends('move_lines.date_expected')
def _compute_scheduled_date(self):
if self.move_type == 'direct':
- self.scheduled_date = min(self.move_lines.mapped('date_expected') or [False])
+ self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
else:
- self.scheduled_date = max(self.move_lines.mapped('date_expected') or [False])
+ self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
@api.one
def _set_scheduled_date(self):
@@ -806,31 +806,25 @@ class Picking(models.Model):
@api.multi
def _create_backorder(self, backorder_moves=[]):
- """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
+ """ Move all non-done lines into a new backorder picking.
"""
- # TDE note: o2o conversion, todo multi
backorders = self.env['stock.picking']
for picking in self:
- backorder_moves = backorder_moves or picking.move_lines
- if self._context.get('do_only_split'):
- not_done_bo_moves = backorder_moves.filtered(lambda move: move.id not in self._context.get('split', []))
- else:
- not_done_bo_moves = backorder_moves.filtered(lambda move: move.state not in ('done', 'cancel'))
- if not not_done_bo_moves:
- continue
- backorder_picking = picking.copy({
- 'name': '/',
- 'move_lines': [],
- 'move_line_ids': [],
- 'backorder_id': picking.id
- })
- picking.message_post(body=_("Back order %screated.") % (backorder_picking.name))
- not_done_bo_moves.write({'picking_id': backorder_picking.id})
- if not picking.date_done:
- picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
- backorder_picking.action_confirm()
- backorder_picking.action_assign()
- backorders |= backorder_picking
+ moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
+ if moves_to_backorder:
+ backorder_picking = picking.copy({
+ 'name': '/',
+ 'move_lines': [],
+ 'move_line_ids': [],
+ 'backorder_id': picking.id
+ })
+ picking.message_post(
+ body=_('The backorder %s has been created.') % (
+ backorder_picking.id, backorder_picking.name))
+ moves_to_backorder.write({'picking_id': backorder_picking.id})
+ moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
+ backorder_picking.action_assign()
+ backorders |= backorder_picking
return backorders
def _log_activity_get_documents(self, orig_obj_changes, stream_field, stream, sorted_method=False, groupby_method=False):
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 005b89be02d..661c764d972 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -21,7 +21,7 @@ class Warehouse(models.Model):
# namedtuple used in helper methods generating values for routes
Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type'])
- name = fields.Char('Warehouse Name', index=True, required=True)
+ name = fields.Char('Warehouse Name', index=True, required=True, default=lambda self: self.env['res.company']._company_default_get('stock.inventory').name)
active = fields.Boolean('Active', default=True)
company_id = fields.Many2one(
'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.inventory'),
@@ -684,11 +684,11 @@ class Warehouse(models.Model):
def _get_sequence_values(self):
return {
- 'in_type_id': {'name': self.name + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
- 'out_type_id': {'name': self.name + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
- 'pack_type_id': {'name': self.name + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
- 'pick_type_id': {'name': self.name + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
- 'int_type_id': {'name': self.name + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
+ 'in_type_id': {'name': self.name + ' ' + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
+ 'out_type_id': {'name': self.name + ' ' + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
+ 'pack_type_id': {'name': self.name + ' ' + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
+ 'pick_type_id': {'name': self.name + ' ' + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
+ 'int_type_id': {'name': self.name + ' ' + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
}
def _format_rulename(self, from_loc, dest_loc, suffix):
diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml
index 60627d9f596..a4020c61b99 100644
--- a/addons/stock/report/report_deliveryslip.xml
+++ b/addons/stock/report/report_deliveryslip.xml
@@ -53,20 +53,19 @@
Product
-
Ordered Quantity
+
Quantity
-
+
-
@@ -75,10 +74,7 @@
Lot/Serial Number
-
Ordered Quantity
-
- Delivered Quantity
-
+
Quantity
@@ -112,13 +108,6 @@
-
-
-
-
-
-
-
@@ -129,7 +118,7 @@
This shipment is a backorder of .
-
+
All items couldn't be shipped, the remaining ones will be shipped as soon as they become available.
diff --git a/doc/cla/corporate/dajmi5.md b/doc/cla/corporate/dajmi5.md
new file mode 100644
index 00000000000..e26ab73ef0d
--- /dev/null
+++ b/doc/cla/corporate/dajmi5.md
@@ -0,0 +1,15 @@
+Croatia, 2017-10-31
+
+DAJ MI 5 agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Davor Bojkić bole@dajmi5.com https://github.com/dajmi5
+
+List of contributors:
+
+Davor Bojkić bole@dajmi5.com https://github.com/badbole
\ No newline at end of file
diff --git a/doc/cla/corporate/kazacube.md b/doc/cla/corporate/kazacube.md
new file mode 100644
index 00000000000..ae4d2868080
--- /dev/null
+++ b/doc/cla/corporate/kazacube.md
@@ -0,0 +1,16 @@
+Morocco, 2017-10-11
+
+KAZACUBE SARL agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Damien DELANGLE ddelangle@kazacube.com https://github.com/kazacube-damien
+
+List of contributors:
+
+ddelangle@kazacube.com https://github.com/kazacube-damien
+cloudmaster@kazacube.com https://github.com/sharecodekzc
diff --git a/doc/cla/corporate/odoo_house.md b/doc/cla/corporate/odoo_house.md
new file mode 100644
index 00000000000..34306354122
--- /dev/null
+++ b/doc/cla/corporate/odoo_house.md
@@ -0,0 +1,15 @@
+Denmark, 2018-03-18
+
+Odoo House ApS agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Henning Dinsen henning@odoohouse.dk https://github.com/odoohouse
+
+List of contributors:
+
+Henning Dinsen henning@odoohouse.dk https://github.com/odoohouse
diff --git a/doc/cla/individual/SimoRubi.md b/doc/cla/individual/SimoRubi.md
new file mode 100644
index 00000000000..07a69054170
--- /dev/null
+++ b/doc/cla/individual/SimoRubi.md
@@ -0,0 +1,11 @@
+Italy, 2018-02-05
+
+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,
+
+Simone Rubino simone.rubino@agilebg.com https://github.com/SimoRubi
diff --git a/doc/cla/individual/agente2012.md b/doc/cla/individual/agente2012.md
new file mode 100644
index 00000000000..d01ad0893f9
--- /dev/null
+++ b/doc/cla/individual/agente2012.md
@@ -0,0 +1,12 @@
+
+Portugal, 2018-03-14
+
+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,
+
+João Santos joaopns05@gmail.com https://github.com/agente2012
diff --git a/doc/cla/individual/ernesto-telematel.md b/doc/cla/individual/ernesto-telematel.md
new file mode 100644
index 00000000000..c7f093485af
--- /dev/null
+++ b/doc/cla/individual/ernesto-telematel.md
@@ -0,0 +1,11 @@
+México, 2017-12-12
+
+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,
+
+Luis Ernesto García Medina ernesto.garcia@telematel.com https://github.com/ernesto-telematel
\ No newline at end of file
diff --git a/doc/cla/individual/kurkop.md b/doc/cla/individual/kurkop.md
index ead18fbc4cb..99527b55009 100644
--- a/doc/cla/individual/kurkop.md
+++ b/doc/cla/individual/kurkop.md
@@ -9,3 +9,4 @@ declaration.
Signed,
Juan Pablo Arias jpa@devco.co https://github.com/kurkop
+Juan Pablo Arias j@kurlabs.com https://github.com/kurkop
diff --git a/doc/cla/individual/mginshe.md b/doc/cla/individual/mginshe.md
new file mode 100644
index 00000000000..23e87975a32
--- /dev/null
+++ b/doc/cla/individual/mginshe.md
@@ -0,0 +1,9 @@
+New Zealand, 2018-03-15
+
+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,
+
+Ryan COLE mginshe@gmail.com https://github.com/mginshe
diff --git a/doc/cla/individual/mpanarin.md b/doc/cla/individual/mpanarin.md
new file mode 100644
index 00000000000..ea0a988978e
--- /dev/null
+++ b/doc/cla/individual/mpanarin.md
@@ -0,0 +1,10 @@
+Ukraine, 2018-03-16
+
+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,
+
+Panarin Mykhailo Ihorevich m.panarin@mobilunity.com https://github.com/mpanarin
+
diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst
index ec31f63af53..7eef640326d 100644
--- a/doc/howtos/backend.rst
+++ b/doc/howtos/backend.rst
@@ -258,6 +258,13 @@ be declared in the ``'data'`` list (always loaded) or in the ``'demo'`` list
.. patch::
+.. tip:: The content of the data files is only loaded when a module is
+ installed or updated.
+
+ After making some changes, do not forget to use
+ :ref:`odoo-bin -u openacademy ` to save the changes
+ to your database.
+
Actions and Menus
-----------------
@@ -1036,9 +1043,9 @@ their most common attributes are:
``date_start``
record's field holding the start date/time for the event
``date_stop`` (optional)
- record's field holding the end date/time for the event
-
-field (to define the label for each calendar event)
+ record's field holding the end date/time for the event
+``string``
+ record's field to define the label for each calendar event
.. code-block:: xml
diff --git a/doc/reference/guidelines.rst b/doc/reference/guidelines.rst
index f04050b011e..a965c3a689b 100644
--- a/doc/reference/guidelines.rst
+++ b/doc/reference/guidelines.rst
@@ -6,17 +6,26 @@
Odoo Guidelines
===============
-This page introduces the new Odoo Coding Guidelines. Those aim to improve the quality of the code (e.g. better readability of source) and Odoo Apps. Indeed, proper code eases maintenance, aids debugging, lowers complexity and promotes reliability.
-
-These guidelines should be applied to every new module, and new development.
+This page introduces the Odoo Coding Guidelines. Those aim to improve the
+quality of Odoo Apps code. Indeed proper code improves readability, eases
+maintenance, helps debugging, lowers complexity and promotes reliability.
+These guidelines should be applied to every new module and to all new development.
.. warning::
- These guidelines are written with new modules and new files in mind. When
- modifying existing files, the original style of the file strictly supersedes
- any other style guidelines. In other words, never modify existing files in
- order to apply these guidelines, to avoid disrupting the revision history of
- each line. For more details, see our `pull request guide `_.
+ When modifying existing files in **stable version** the original file style
+ strictly supersedes any other style guidelines. In other words please never
+ modify existing files in order to apply these guidelines. It avoids disrupting
+ the revision history of code lines. Diff should be kept minimal. For more
+ details, see our `pull request guide `_.
+
+.. warning::
+
+ When modifying existing files in **master (development) version** apply those
+ guidelines to existing code only for modified code or if most of the file is
+ under revision. In other words modify existing files structure only if it is
+ going under major changes. In that case first do a **move** commit then apply
+ the changes related to the feature.
Module structure
================
@@ -913,50 +922,138 @@ Javascript and CSS
Git
===
-Commit message
---------------
+Configure your git
+------------------
-Prefix your commit with
+Based on ancestral experience and oral tradition, the following things go a long
+way towards making your commits more helpful:
-- **[IMP]** for improvements
-- **[FIX]** for bug fixes
-- **[REF]** for refactoring
-- **[ADD]** for adding new resources
-- **[REM]** for removing of resources
-- **[MOV]** for moving files (Do not change content of moved file, otherwise
- Git will loose track, and the history will be lost !), or simply moving code
- from a file to another one.
-- **[MERGE]** for merge commits (only for forward/back-port)
-- **[CLA]** for signing the Odoo Individual Contributor License
+- Be sure to define both the user.email and user.name in your local git config
-Then, in the message itself, specify the part of the code impacted by
-your changes (module name, lib, transversal object, ...) and a description
-of the changes.
+ .. code-block:: text
-- Always include a meaningful commit message: it should be self explanatory
- (long enough) including the name of the module that has been changed and the
- reason behind the change. Do not use single words like "bugfix" or
- "improvements".
-- Avoid commits which simultaneously impact multiple modules. Try to
- split into different commits where impacted modules are different
- (It will be helpful if we need to revert a module separately).
+ git config --global
+
+- Be sure to add your full name to your Github profile here. Please feel fancy
+ and add your team, avatar, your favorite quote, and whatnot ;-)
+
+Commit message structure
+------------------------
+
+Commit message has four parts: tag, module, short description and full
+description. Try to follow the preferred structure for your commit messages
.. code-block:: text
- [FIX] website, website_mail: remove unused alert div, fixes look of input-group-btn
+ [TAG] module: describe your change in a short sentence (ideally < 50 chars)
- Bootstrap's CSS depends on the input-group-btn
- element being the first/last child of its parent.
- This was not the case because of the invisible
- and useless alert.
+ Long version of the change description, including the rationale for the change,
+ or a summary of the feature being introduced.
- [IMP] fields: reduce memory footprint of list/set field attributes
+ Please spend a lot more time describing WHY the change is being done rather
+ than WHAT is being changed. This is usually easy to grasp by actually reading
+ the diff. WHAT should be explained only if there are technical choices
+ or decision involved. In that case explain WHY this decision was taken.
- [REF] web: add module system to the web client
+ End the message with references, such as task or bug numbers, PR numbers, and
+ OPW tickets, following the suggested format:
+ Related to task #taskId
+ Fixes #12345 (link and close issue on Github)
+ Closes #7865 (link and close PR on Github)
+ OPW-112233
- This commit introduces a new module system for the javascript code.
- Instead of using global ...
+Tag and module name
+-------------------
+Tags are used to prefix your commit. They should be one of the following
-.. note:: Use the long description to explain the *why* not the
- *what*, the *what* can be seen in the diff
+- **[FIX]** for bug fixes: mostly used in stable version but also valid if you
+ are fixing a recent bug in development version;
+- **[REF]** for refactoring: when a feature is heavily rewritten;
+- **[ADD]** for adding new modules;
+- **[REM]** for removing resources: removing dead code, removing views,
+ removing modules, ...;
+- **[REV]** for reverting commits: if a commit causes issues or is not wanted
+ reverting it is done using this tag;
+- **[MOV]** for moving files: use git move and do not change content of moved file
+ otherwise Git may loose track and history of the file; also used when moving
+ code from one file to another;
+- **[REL]** for release commits: new major or minor stable versions;
+- **[IMP]** for improvements: most of the changes done in development version
+ are incremental improvements not related to another tag;
+- **[MERGE]** for merge commits: used in forward port of bug fixes but also as
+ main commit for feature involving several separated commits;
+- **[CLA]** for signing the Odoo Individual Contributor License;
+- **[I18N]** for changes in translation files;
+
+After tag comes the modified module name. Use the technical name as functional
+name may change with time. If several modules are modified, list them or use
+various to tell it is cross-modules. Unless really required or easier avoid
+modifying code across several modules in the same commit. Understanding module
+history may become difficult.
+
+Commit message header
+---------------------
+
+After tag and module name comes a meaningful commit message header. It should be
+self explanatory and include the reason behind the change. Do not use single words
+like "bugfix" or "improvements". Try to limit the header length to about 50 characters
+for readability.
+
+Commit message header should make a valid sentence once concatenated with
+``if applied, this commit will ``. For example ``[IMP] base: prevent to
+archive users linked to active partners`` is correct as it makes a valid sentence
+``if applied, this commit will prevent users to archive...``.
+
+Commit message full description
+-------------------------------
+
+In the message description specify the part of the code impacted by your changes
+(module name, lib, transversal object, ...) and a description of the changes.
+
+First explain WHY you are modifying code. What is important if someone goes back
+to your commit in about 4 decades (or 3 days) is why you did it. It is the
+purpose of the change.
+
+What you did can be found in the commit itself. If there was some technical choices
+involved it is a good idea to explain it also in the commit message after the why.
+For Odoo R&D developers "PO team asked me to do it" is not a valid why, by the way.
+
+Please avoid commits which simultaneously impact multiple modules. Try to split
+into different commits where impacted modules are different. It will be helpful
+if we need to revert changes in a given module separately.
+
+Don't hesitate to be a bit verbose. Most people will only see your commit message
+and judge everything you did in your life just based on those few sentences.
+No pressure at all.
+
+**You spend several hours, days or weeks working on meaningful features. Take
+some time to calm down and write clear and understandable commit messages.**
+
+If you are an Odoo R&D developer the WHY should be the purpose of the task you
+are working on. Full specifications make the core of the commit message.
+**If you are working on a task that lacks purpose and specifications please
+consider making them clear before continuing.**
+
+Finally here are some examples of correct commit messages :
+
+.. code-block:: text
+
+ [REF] models: use `parent_path` to implement parent_store
+
+ This replaces the former modified preorder tree traversal (MPTT) with the
+ fields `parent_left`/`parent_right`[...]
+
+ [FIX] account: remove frenglish
+
+ [...]
+
+ Closes #22793
+ Fixes #22769
+
+ [FIX] website: remove unused alert div, fixes look of input-group-btn
+
+ Bootstrap's CSS depends on the input-group-btn
+ element being the first/last child of its parent.
+ This was not the case because of the invisible
+ and useless alert.
diff --git a/doc/reference/mobile.rst b/doc/reference/mobile.rst
index e645d03a404..355164e82c9 100644
--- a/doc/reference/mobile.rst
+++ b/doc/reference/mobile.rst
@@ -21,7 +21,7 @@ Vibration, Notification and Toast through Odoo Web (via JavaScript). For this, y
do not need to be a mobile developer, if you know Odoo JavaScript API you can
access all available mobile features.
-.. warning:: This features works with **Odoo Enterprise 10.0+** only
+.. warning:: These features work with **Odoo Enterprise 10.0+** only
How does it work?
=================
diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst
index 00b284f9cb2..9ce03c8709a 100644
--- a/doc/reference/orm.rst
+++ b/doc/reference/orm.rst
@@ -476,6 +476,11 @@ added.
``onchange`` methods work on virtual records assignment on these records
is not written to the database, just used to know which value to send back
to the client
+
+.. warning::
+
+ It is not possible for a ``one2many`` or ``many2many`` field to modify
+ itself via onchange. This is a webclient limitation - see `#2693 `_.
Low-level SQL
-------------
@@ -630,6 +635,9 @@ Model Reference
If set to ``False``, override :meth:`.init` to create the database
table
+
+ .. tip:: To create a model without any table, inherit
+ from ``odoo.models.AbstractModel``
.. attribute:: _table
diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml
index b0c001d7aad..fdbb6fff9f2 100644
--- a/odoo/addons/base/data/res_country_data.xml
+++ b/odoo/addons/base/data/res_country_data.xml
@@ -1237,6 +1237,7 @@
+ Peru
diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml
index 2d32fedb9c5..6e2d8cf5dc8 100644
--- a/odoo/addons/base/data/res_currency_data.xml
+++ b/odoo/addons/base/data/res_currency_data.xml
@@ -218,7 +218,7 @@
JPY¥
- 0.01
+ 1.00YenCen
@@ -999,7 +999,7 @@
MAD
- د.م.
+ DH0.01Dirham
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 0bc80b6998a..330cdc67a0f 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -172,6 +172,7 @@ xpath_utils['hasclass'] = _hasclass
TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS))
WRONGCLASS = re.compile(r"(@class\s*=|=\s*@class|contains\(@class)")
+READONLY = re.compile(r"\breadonly\b")
class View(models.Model):
@@ -786,12 +787,16 @@ actual arch.
attrs = {}
field = Model._fields.get(node.get('name'))
if field:
+ editable = self.env.context.get('view_is_editable', True) and self._field_is_editable(field, node)
children = False
views = {}
for f in node:
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(f)
- xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(field.comodel_name, f, view_id)
+ xarch, xfields = self.with_context(
+ base_model_name=model,
+ view_is_editable=editable,
+ ).postprocess_and_fields(field.comodel_name, f, view_id)
views[str(f.tag)] = {
'arch': xarch,
'fields': xfields,
@@ -866,6 +871,17 @@ actual arch.
return arch
+ def _view_is_editable(self, node):
+ """ Return whether the node is an editable view. """
+ return node.tag == 'form' or node.tag == 'tree' and node.get('editable')
+
+ def _field_is_editable(self, field, node):
+ """ Return whether a field is editable (not always readonly). """
+ return (
+ (not field.readonly or READONLY.search(str(field.states or ""))) and
+ (node.get('readonly') != "1" or READONLY.search(node.get('attrs') or ""))
+ )
+
def get_attrs_symbols(self):
""" Return a set of predefined symbols for evaluating attrs. """
return {
@@ -885,10 +901,11 @@ actual arch.
'current_date',
}
- def get_attrs_field_names(self, arch):
+ def get_attrs_field_names(self, arch, model, editable):
""" Retrieve the field names appearing in context, domain and attrs, and
return a list of triples ``(field_name, attr_name, attr_value)``.
"""
+ VIEW_TYPES = {item[0] for item in type(self).type.selection}
symbols = self.get_attrs_symbols() | {None}
result = []
@@ -918,8 +935,17 @@ actual arch.
if isinstance(arg, (tuple, list)):
process_expr(str(arg[0]), get, key, expr)
- def process(node, get=get_name):
+ def process(node, model, editable, get=get_name):
""" traverse `node` and collect triples """
+ if node.tag in VIEW_TYPES:
+ # determine whether this view is editable
+ editable = editable and self._view_is_editable(node)
+ elif node.tag == 'field':
+ # determine whether the field is editable
+ field = model._fields.get(node.get('name'))
+ if field:
+ editable = editable and self._field_is_editable(field, node)
+
for key, val in node.items():
if not val:
continue
@@ -927,13 +953,21 @@ actual arch.
process_expr(val, get, key, val)
elif key == 'attrs':
process_attrs(val, get, key, val)
- if node.tag == 'field':
- # retrieve subfields of 'parent'
- get = partial(get_subname, get)
- for child in node:
- process(child, get)
- process(arch)
+ if node.tag == 'field' and field and field.relational:
+ if editable and not node.get('domain'):
+ domain = field._description_domain(self.env)
+ # process the field's domain as if it was in the view
+ if isinstance(domain, pycompat.string_types):
+ process_expr(domain, get, 'domain', domain)
+ # retrieve subfields of 'parent'
+ model = self.env[field.comodel_name]
+ get = partial(get_subname, get)
+
+ for child in node:
+ process(child, model, editable, get)
+
+ process(arch, model, editable)
return result
@api.model
@@ -974,7 +1008,8 @@ actual arch.
attrs_fields = []
if self.env.context.get('check_field_names'):
- attrs_fields = self.get_attrs_field_names(node)
+ editable = self.env.context.get('view_is_editable', True)
+ attrs_fields = self.get_attrs_field_names(node, Model, editable)
fields_def = self.postprocess(model, node, view_id, False, fields)
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py
index 60f42b16c1c..421da4a899c 100644
--- a/odoo/addons/base/models/res_config.py
+++ b/odoo/addons/base/models/res_config.py
@@ -370,9 +370,8 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
``execute``` will save its value in an ir.config.parameter (global setting for the
database).
- * For the other fields, the method ``execute`` invokes all methods with a name
- that starts with 'set_'; such methods can be defined to implement the effect
- of those fields.
+ * For the other fields, the method ``execute`` invokes `set_values`.
+ Override it to implement the effect of those fields.
The method ``default_get`` retrieves values that reflect the current status of the
fields like 'default_XXX', 'group_XXX', 'module_XXX' and config_XXX.
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index abb25a4bc2e..efcf203edf0 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -4,11 +4,13 @@ import pytz
import datetime
import itertools
import logging
+import hmac
from collections import defaultdict
from itertools import chain, repeat
from lxml import etree
from lxml.builder import E
+from hashlib import sha256
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationError
@@ -374,6 +376,8 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
+ if any(key in values for key in self._get_session_token_fields()):
+ self._invalidate_session_cache()
return res
@@ -384,6 +388,7 @@ class Users(models.Model):
db = self._cr.dbname
for id in self.ids:
self.__uid_cache[db].pop(id, None)
+ self._invalidate_session_cache()
return super(Users, self).unlink()
@api.model
@@ -509,6 +514,34 @@ class Users(models.Model):
finally:
cr.close()
+ def _get_session_token_fields(self):
+ return {'id', 'login', 'password', 'active'}
+
+ @tools.ormcache('sid')
+ def _compute_session_token(self, sid):
+ """ Compute a session token given a session id and a user id """
+ # retrieve the fields used to generate the session token
+ session_fields = ', '.join(sorted(self._get_session_token_fields()))
+ self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret')
+ FROM res_users
+ WHERE id=%%s""" % (session_fields), (self.id,))
+ if self.env.cr.rowcount != 1:
+ self._invalidate_session_cache()
+ return False
+ data_fields = self.env.cr.fetchone()
+ # generate hmac key
+ key = (u'%s' % (data_fields,)).encode('utf-8')
+ # hmac the session id
+ data = sid.encode('utf-8')
+ h = hmac.new(key, data, sha256)
+ # keep in the cache the token
+ return h.hexdigest()
+
+ @api.multi
+ def _invalidate_session_cache(self):
+ """ Clear the sessions cache """
+ self._compute_session_token.clear_cache(self)
+
@api.model
def change_password(self, old_passwd, new_passwd):
"""Change current user password. Old password must be provided explicitly
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index 7a1a5bfb617..567df2b352a 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -1107,6 +1107,183 @@ class TestViews(ViewCase):
})
@mute_logger('odoo.addons.base.models.ir_ui_view')
+ def test_domain_on_field_in_view(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % '',
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % '',
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_field_in_subview(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', model)]")
+
+ arch = """
+
+ """
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', ''),
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', ''),
+ })
+ with self.assertRaises(ValidationError):
+ self.View.create({
+ 'name': 'valid domain',
+ 'model': 'ir.ui.view',
+ 'arch': arch % ('', ''),
+ })
+
+ @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ def test_domain_on_field_in_subview_with_parent(self):
+ field = self.env['ir.ui.view']._fields['inherit_id']
+ self.patch(field, 'domain', "[('model', '=', parent.model)]")
+
+ arch = """
+
diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js
index 7b08672385c..48417b619f1 100644
--- a/odoo/addons/test_main_flows/static/src/js/tour.js
+++ b/odoo/addons/test_main_flows/static/src/js/tour.js
@@ -56,6 +56,22 @@ tour.register('main_flow_tour', {
trigger: '.o_field_widget[name=route_ids] .o_checkbox + label:contains("Make To Order")',
content: _t('Uncheck Make To Order'),
position: 'right',
+}, {
+ trigger: '.o_notebook a:contains("General Information")',
+ content: _t('Go to main tab'),
+ position: 'top',
+}, {
+ trigger: ".o_field_widget[name=taxes_id] input",
+ content: _t("Focus on customer taxes field."),
+ run: function(actions) {
+ actions.click();
+ var $e = $(".ui-menu-item.ui-state-focus:not(.o_m2o_dropdown_option)");
+ if ($e.length) {
+ actions.click($e);
+ } else {
+ actions.click(); // close dropdown
+ }
+ },
}, {
trigger: '.o_form_button_save',
content: _t('Save this product and the modifications you\'ve made to it.'),
@@ -207,6 +223,18 @@ tour.register('main_flow_tour', {
content: _t('Set to service'),
position: 'left',
run: 'text "service"',
+}, {
+ trigger: ".o_field_widget[name=taxes_id] input",
+ content: _t("Focus on customer taxes field."),
+ run: function(actions) {
+ actions.click();
+ var $e = $(".ui-menu-item.ui-state-focus:not(.o_m2o_dropdown_option)");
+ if ($e.length) {
+ actions.click($e);
+ } else {
+ actions.click(); // close dropdown
+ }
+ },
}, {
trigger: '.o_notebook a:contains("Sales")',
content: _t('Go to sales tab'),
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 bd33123af89..a7d0f977968 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -278,6 +278,14 @@ class TestFields(common.TransactionCase):
self.assertEqual(ewan.parent, cath)
self.assertEqual(ewan.name, "Erwan")
+ # write on non-stored inverse field on severals records
+ foo1 = Category.create({'name': 'Foo'})
+ foo2 = Category.create({'name': 'Foo'})
+ (foo1 + foo2).write({'display_name': 'Bar'})
+ self.assertEqual(foo1.name, 'Bar')
+ self.assertEqual(foo2.name, 'Bar')
+
+
# create/write on 'foo' should only invoke the compute method
log = []
model = self.env['test_new_api.compute.inverse'].with_context(log=log)
diff --git a/odoo/api.py b/odoo/api.py
index 0e9975f25c9..3f55669023e 100644
--- a/odoo/api.py
+++ b/odoo/api.py
@@ -1021,8 +1021,10 @@ class Cache(object):
for field, field_cache in self._data.items():
src_key = field.cache_key(src)
dst_key = field.cache_key(dst)
- for record_id, record_cache in field_cache.items():
- if src_key in record_cache:
+ for record_cache in field_cache.values():
+ if src_key in record_cache and not isinstance(record_cache[src_key], SpecialValue):
+ # But not if it's a SpecialValue, which often is an access error
+ # because the other environment (eg. sudo()) is well expected to have access.
record_cache[dst_key] = record_cache[src_key]
def invalidate(self, spec=None):
diff --git a/odoo/fields.py b/odoo/fields.py
index 46898e3fcd4..8980fae3e29 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -1553,6 +1553,9 @@ class Date(Field):
""" Convert a :class:`date` value into the format expected by the ORM. """
return value.strftime(DATE_FORMAT) if value else False
+ def convert_to_column(self, value, record, values=None, validate=True):
+ return super(Date, self).convert_to_column(value or None, record, values, validate)
+
def convert_to_cache(self, value, record, validate=True):
if not value:
return False
@@ -1623,6 +1626,9 @@ class Datetime(Field):
""" Convert a :class:`datetime` value into the format expected by the ORM. """
return value.strftime(DATETIME_FORMAT) if value else False
+ def convert_to_column(self, value, record, values=None, validate=True):
+ return super(Datetime, self).convert_to_column(value or None, record, values, validate)
+
def convert_to_cache(self, value, record, validate=True):
if not value:
return False
diff --git a/odoo/http.py b/odoo/http.py
index 4cfe76a76a7..9c6c0092e25 100644
--- a/odoo/http.py
+++ b/odoo/http.py
@@ -1035,7 +1035,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.db = db
self.uid = uid
self.login = login
- self.password = password
+ self.session_token = uid and security.compute_session_token(self)
request.uid = uid
request.disable_db = False
@@ -1050,7 +1050,10 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
"""
if not self.db or not self.uid:
raise SessionExpiredException("Session expired")
- security.check(self.db, self.uid, self.password)
+
+ # here we check if the session is still valid
+ if not security.check_session(self):
+ raise SessionExpiredException("Session expired")
def logout(self, keep_db=False):
for k in list(self):
@@ -1063,7 +1066,7 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
self.setdefault("db", None)
self.setdefault("uid", None)
self.setdefault("login", None)
- self.setdefault("password", None)
+ self.setdefault("session_token", None)
self.setdefault("context", {})
def get_context(self):
diff --git a/odoo/models.py b/odoo/models.py
index 6834dca5b6a..4aa616ed5dc 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -3025,17 +3025,27 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self.env[model_name].browse(parent_ids).write(parent_vals)
if inverse_vals:
- # put the values of inverse fields in cache, and inverse them
self.modified(set(inverse_vals) - set(store_vals))
- for record in self:
- record._cache.update(record._convert_to_cache(inverse_vals, update=True))
# in case several fields use the same inverse method, call it once
for _inv, fields in groupby(inverse_fields, attrgetter('inverse')):
- fields[0].determine_inverse(self)
+ # If a field is not stored, its inverse method will probably
+ # write on its dependencies, which will invalidate the field
+ # on all records. We therefore inverse the field one record
+ # at a time.
+ batches = [self] if all(f.store for f in fields) else list(self)
+ # put the values of fields in cache, and inverse them
+ inv_vals = {f.name: inverse_vals[f.name] for f in fields}
+ for records in batches:
+ for record in records:
+ record._cache.update(
+ record._convert_to_cache(inv_vals, update=True)
+ )
+ fields[0].determine_inverse(records)
self.modified(set(inverse_vals) - set(store_vals))
+
# check Python constraints for inversed fields
self._validate_fields(set(inverse_vals) - set(store_vals))
diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py
index 083fda4203b..4dda572fb13 100644
--- a/odoo/modules/loading.py
+++ b/odoo/modules/loading.py
@@ -400,7 +400,12 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
cr.commit()
_logger.info('Reloading registry once more after uninstalling modules')
api.Environment.reset()
- return odoo.modules.registry.Registry.new(cr.dbname, force_demo, status, update_module)
+ registry = odoo.modules.registry.Registry.new(
+ cr.dbname, force_demo, status, update_module
+ )
+ registry.check_tables_exist(cr)
+ cr.commit()
+ return registry
# STEP 6: verify custom views on every model
if update_module:
diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py
index fb568185d9a..4352b0583f9 100644
--- a/odoo/modules/registry.py
+++ b/odoo/modules/registry.py
@@ -303,8 +303,16 @@ class Registry(Mapping):
models[0].recompute()
# make sure all tables are present
+ self.check_tables_exist(cr)
+
+ def check_tables_exist(self, cr):
+ """
+ Verify that all tables are present and try to initialize those that are missing.
+ """
+ env = odoo.api.Environment(cr, SUPERUSER_ID, {})
table2model = {model._table: name for name, model in env.items() if not model._abstract}
missing_tables = set(table2model).difference(existing_tables(cr, table2model))
+
if missing_tables:
missing = {table2model[table] for table in missing_tables}
_logger.warning("Models have no table: %s.", ", ".join(missing))
diff --git a/odoo/service/security.py b/odoo/service/security.py
index 264e88d42bd..229e4b84077 100644
--- a/odoo/service/security.py
+++ b/odoo/service/security.py
@@ -11,3 +11,16 @@ def login(db, login, password):
def check(db, uid, passwd):
res_users = odoo.registry(db)['res.users']
return res_users.check(db, uid, passwd)
+
+def compute_session_token(session):
+ with odoo.registry(session.db).cursor() as cr:
+ self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
+ return self._compute_session_token(session.sid)
+
+def check_session(session):
+ with odoo.registry(session.db).cursor() as cr:
+ self = odoo.api.Environment(cr, session.uid, {})['res.users'].browse(session.uid)
+ if odoo.tools.misc.consteq(self._compute_session_token(session.sid), session.session_token):
+ return True
+ self._invalidate_session_cache()
+ return False
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index df4178ee0e9..7e974a96656 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -44,6 +44,8 @@ except ImportError:
import odoo
from odoo import api
+from odoo.service import security
+
_logger = logging.getLogger(__name__)
@@ -361,7 +363,7 @@ class HttpCase(TransactionCase):
session.db = db
session.uid = uid
session.login = user
- session.password = password
+ session.session_token = uid and security.compute_session_token(session)
session.context = env['res.users'].context_get() or {}
session.context['uid'] = uid
session._fix_lang(session.context)
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index e6dc561188b..5edf8df21bc 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -460,7 +460,7 @@ def quote(s):
.replace('\n', '\\n"\n"')
re_escaped_char = re.compile(r"(\\.)")
-re_escaped_replacements = {'n': '\n', }
+re_escaped_replacements = {'n': '\n', 't': '\t',}
def _sub_replacement(match_obj):
return re_escaped_replacements.get(match_obj.group(1)[1], match_obj.group(1)[1])