diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index baa0aed666e..cde55f97bf4 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -412,7 +412,7 @@ class AccountJournal(models.Model):
belongs_to_company = fields.Boolean('Belong to the user\'s current company', compute="_belong_to_company", search="_search_company_journals",)
# Bank journals fields
- bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False, domain="[('partner_id','=', company_id)]")
+ bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False)
bank_statements_source = fields.Selection([('undefined', 'Undefined Yet'),('manual', 'Record Manually')], string='Bank Feeds', default='undefined')
bank_acc_number = fields.Char(related='bank_account_id.acc_number')
bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id')
@@ -522,11 +522,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.'))
@@ -542,8 +547,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_invoice.py b/addons/account/models/account_invoice.py
index e05b02ae058..bca650dcc65 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 df18aa8fca9..5f9a2cf26ba 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:
@@ -936,7 +936,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.
@@ -974,8 +976,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'
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 d280b6cb850..4de3fedbbd4 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 dbfd933c99a..a41803f7062 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 2da6c42abe7..852b2a14e00 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/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index 50124617526..6fdc285a97b 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -252,7 +252,7 @@
-
+
@@ -299,7 +299,7 @@
-
+
@@ -397,7 +397,7 @@
-
+
@@ -500,7 +500,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 c652267ad5d..ddc2d48ce76 100644
--- a/addons/account_voucher/models/account_voucher.py
+++ b/addons/account_voucher/models/account_voucher.py
@@ -154,6 +154,7 @@ class AccountVoucher(models.Model):
@api.onchange('partner_id', 'pay_now')
def onchange_partner_id(self):
+ pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
if self.pay_now != 'pay_now':
if self.partner_id:
self.account_id = self.partner_id.property_account_receivable_id \
@@ -163,6 +164,12 @@ class AccountVoucher(models.Model):
domain = [('deprecated', '=', False), ('internal_type', '=', account_type)]
self.account_id = self.env['account.account'].search(domain, limit=1)
+ 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 2710b693ccd..258e6ca8af8 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -251,8 +251,6 @@
-
-
@@ -267,6 +265,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 49d8a9e0859..21d302a63c5 100644
--- a/addons/hr/models/hr.py
+++ b/addons/hr/models/hr.py
@@ -126,7 +126,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 5d6c49a0bf7..af9d6c342a5 100644
--- a/addons/hr_payroll/models/hr_salary_rule.py
+++ b/addons/hr_payroll/models/hr_salary_rule.py
@@ -76,7 +76,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.")
@@ -89,7 +89,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/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 0e0a8a0c103..ffa74c60037 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 6532067bb57..006711c5d39 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 a8b54e88b39..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 openerp
-from openerp import api, SUPERUSER_ID
-
from . import models # noqa
from . import report # noqa
-
-
-def uninstall_hook(cr, registry):
- def recreate_view(dbname):
- db_registry = openerp.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 3dbea80703b..1bf167d5731 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1068,18 +1068,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 1147c9e6a96..f09d9ca1160 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):
@@ -813,31 +813,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 73368db6ad4..5711400941e 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 5a53839b628..05ba34c4abe 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/static/src/js/res_config_settings.js b/odoo/addons/base/static/src/js/res_config_settings.js
index c6cef6362af..28f5ac17c9b 100644
--- a/odoo/addons/base/static/src/js/res_config_settings.js
+++ b/odoo/addons/base/static/src/js/res_config_settings.js
@@ -31,17 +31,6 @@ var BaseSettingRenderer = FormRenderer.extend({
}
},
- /**
- * enable case insensitive search in jQuery contains function
- *
- * @private
- */
- _activeCaseInsensitiveSearch: function () {
- $.expr[':'].contains = function (a, i, m) {
- return $(a).text().toUpperCase()
- .indexOf(m[3].toUpperCase()) >= 0;
- };
- },
/**
* initialize modules list.
* remove module that restricted in groups
@@ -253,7 +242,6 @@ var BaseSettingRenderer = FormRenderer.extend({
var res = this._super.apply(this, arguments);
if (!this.modules) {
this._initModules();
- this._activeCaseInsensitiveSearch();
}
this._renderLeftPanel();
this._initSearch();
@@ -289,7 +277,7 @@ var BaseSettingRenderer = FormRenderer.extend({
module.settingView.find('h2').addClass('o_hidden');
module.settingView.find('.settingSearchHeader').addClass('o_hidden');
module.settingView.find('.o_settings_container').removeClass('mt16');
- var resultSetting = module.settingView.find("label:contains('" + self.searchText + "')");
+ var resultSetting = module.settingView.find("label:containsLike('" + self.searchText + "')");
if (resultSetting.length > 0) {
resultSetting.each(function () {
var settingBox = $(this).closest('.o_setting_box');
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 = """
+