diff --git a/README.md b/README.md
index 4432013ba16..f8bfd4cf38f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](http://runbot.odoo.com/runbot)
-[](http://www.odoo.com/documentation/master)
-[](https://www.odoo.com/forum/help-1)
-[](http://nightly.odoo.com/)
+[](http://www.odoo.com/documentation/master)
+[](https://www.odoo.com/forum/help-1)
+[](http://nightly.odoo.com/)
Odoo
----
@@ -27,14 +27,14 @@ a full-featured Open Source ERP when you inst
Getting started with Odoo
-------------------------
-For a standard installation please follow the Setup instructions
+For a standard installation please follow the Setup instructions
from the documentation.
If you are a developer you may type the following command at your terminal:
- wget -O- https://raw.githubusercontent.com/odoo/odoo/10.0/setup/setup_dev.py | python
+ wget -O- https://raw.githubusercontent.com/odoo/odoo/master/setup/setup_dev.py | python
-Then follow the developer tutorials
+Then follow the developer tutorials
For Odoo employees
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 0271cad8aa5..eec37901431 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -731,6 +731,9 @@ class AccountTax(models.Model):
else:
total_included += tax_amount
+ # Keep base amount used for the current tax
+ tax_base = base
+
if tax.include_base_amount:
base += tax_amount
@@ -738,6 +741,7 @@ class AccountTax(models.Model):
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
'amount': tax_amount,
+ 'base': tax_base,
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index a23e1d12b5c..204f0161a85 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -250,13 +250,13 @@ class AccountBankStatement(models.Model):
context = dict(self._context or {})
context['journal_id'] = self.journal_id.id
return {
- 'name': _('Journal Items'),
+ 'name': _('Journal Entries'),
'view_type': 'form',
'view_mode': 'tree,form',
- 'res_model': 'account.move.line',
+ 'res_model': 'account.move',
'view_id': False,
'type': 'ir.actions.act_window',
- 'domain': [('statement_id', 'in', self.ids)],
+ 'domain': [('id', 'in', self.mapped('move_line_ids').mapped('move_id').ids)],
'context': context,
}
@@ -366,6 +366,7 @@ class AccountBankStatementLine(models.Model):
journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True)
amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.")
currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.")
+ state = fields.Selection(related='statement_id.state' , string='Status', readonly=True)
move_name = fields.Char(string='Journal Entry Name', readonly=True,
default=False, copy=False,
help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.")
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 04488a6ea8e..d7bec0cb579 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -12,6 +12,9 @@ from odoo.tools.misc import formatLang
from odoo.exceptions import UserError, RedirectWarning, ValidationError
import odoo.addons.decimal_precision as dp
+import logging
+
+_logger = logging.getLogger(__name__)
# mapping invoice type to journal type
TYPE2JOURNAL = {
@@ -592,6 +595,25 @@ class AccountInvoice(models.Model):
else:
return self.env.ref('account.invoice_form').id
+ def _prepare_tax_line_vals(self, line, tax):
+ """ Prepare values to create an account.invoice.tax line
+
+ The line parameter is an account.invoice.line, and the
+ tax parameter is the output of account.tax.compute_all().
+ """
+ vals = {
+ 'invoice_id': self.id,
+ 'name': tax['name'],
+ 'tax_id': tax['id'],
+ 'amount': tax['amount'],
+ 'base': tax['base'],
+ 'manual': False,
+ 'sequence': tax['sequence'],
+ 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
+ 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
+ }
+ return vals
+
@api.multi
def get_taxes_values(self):
tax_grouped = {}
@@ -599,16 +621,7 @@ class AccountInvoice(models.Model):
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes']
for tax in taxes:
- val = {
- 'invoice_id': self.id,
- 'name': tax['name'],
- 'tax_id': tax['id'],
- 'amount': tax['amount'],
- 'manual': False,
- 'sequence': tax['sequence'],
- 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
- 'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
- }
+ val = self._prepare_tax_line_vals(line, tax)
# If the taxes generate moves on the same financial account as the invoice line,
# propagate the analytic account from the invoice line to the tax line.
@@ -623,6 +636,7 @@ class AccountInvoice(models.Model):
tax_grouped[key] = val
else:
tax_grouped[key]['amount'] += val['amount']
+ tax_grouped[key]['base'] += val['base']
return tax_grouped
@api.multi
@@ -1336,15 +1350,19 @@ class AccountInvoiceTax(models.Model):
@api.depends('invoice_id.invoice_line_ids')
def _compute_base_amount(self):
+ tax_grouped = {}
+ for invoice in self.mapped('invoice_id'):
+ tax_grouped[invoice.id] = invoice.get_taxes_values()
for tax in self:
- base = 0.0
- for line in tax.invoice_id.invoice_line_ids:
- if tax.tax_id in line.invoice_line_tax_ids:
- price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
- base += (line.invoice_line_tax_ids - tax.tax_id).compute_all(
- price_unit, line.invoice_id.currency_id, line.quantity, line.product_id, line.invoice_id.partner_id
- )['base']
- tax.base = base
+ key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({
+ 'tax_id': tax.tax_id.id,
+ 'account_id': tax.account_id.id,
+ 'account_analytic_id': tax.account_analytic_id.id,
+ })
+ if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]:
+ tax.base = tax_grouped[tax.invoice_id.id][key]['base']
+ else:
+ _logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name)
invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True)
name = fields.Char(string='Tax Description', required=True)
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index b9d80ea21cc..5ff889f6025 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -1037,10 +1037,13 @@ class AccountMoveLine(models.Model):
# the provided values were not already multi-currency
if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
vals['currency_id'] = account.currency_id.id
- ctx = {}
- if 'date' in vals:
- ctx['date'] = vals['date']
- vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id)
+ if self._context.get('skip_full_reconcile_check') == 'amount_currency_excluded':
+ vals['amount_currency'] = 0.0
+ else:
+ ctx = {}
+ if 'date' in vals:
+ ctx['date'] = vals['date']
+ vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id)
if not ok:
raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 0bf832c133d..a7841f307c4 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -39,7 +39,7 @@ def migrate_tags_on_taxes(cr, registry):
('description', '=', tax_template.description)
])
if len(tax_id.ids) == 1:
- tax_id.sudo().write({'tag_ids': [(6, 0, [tax_template.tag_ids.ids])]})
+ tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]})
# ---------------------------------------------------------------
# Account Templates: Account, Tax, Tax Code and chart. + Wizard
@@ -399,7 +399,9 @@ class AccountChartTemplate(models.Model):
:rtype: dict
"""
self.ensure_one()
- account_reconcile_models = self.env['account.reconcile.model.template'].search([])
+ account_reconcile_models = self.env['account.reconcile.model.template'].search([
+ ('account_id.chart_template_id', '=', self.id)
+ ])
for account_reconcile_model in account_reconcile_models:
vals = {
'name': account_reconcile_model.name,
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index 7272a77345e..3ca9f8f10ee 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -445,5 +445,5 @@ class ResPartner(models.Model):
'''
action = self.env.ref('account.action_invoice_refund_out_tree')
result = action.read()[0]
- result['domain'] = "[('id','in',[" + ','.join(map(str, self.ids)) + "])]"
+ result['domain'] = [('partner_id', 'in', self.ids)]
return result
diff --git a/addons/account/tests/test_account_supplier_invoice.py b/addons/account/tests/test_account_supplier_invoice.py
index 671cfb2dcb0..232bd39ddc7 100644
--- a/addons/account/tests/test_account_supplier_invoice.py
+++ b/addons/account/tests/test_account_supplier_invoice.py
@@ -41,3 +41,65 @@ class TestAccountSupplierInvoice(AccountingTestCase):
#I cancel the account move which is in posted state and verifies that it gives warning message
with self.assertRaises(Warning):
invoice.move_id.button_cancel()
+
+ def test_supplier_invoice2(self):
+ tax_fixed = self.env['account.tax'].create({
+ 'sequence': 10,
+ 'name': 'Tax 10.0 (Fixed)',
+ 'amount': 10.0,
+ 'amount_type': 'fixed',
+ 'include_base_amount': True,
+ })
+ tax_percent_included_base_incl = self.env['account.tax'].create({
+ 'sequence': 20,
+ 'name': 'Tax 50.0% (Percentage of Price Tax Included)',
+ 'amount': 50.0,
+ 'amount_type': 'division',
+ 'include_base_amount': True,
+ })
+ tax_percentage = self.env['account.tax'].create({
+ 'sequence': 30,
+ 'name': 'Tax 20.0% (Percentage of Price)',
+ 'amount': 20.0,
+ 'amount_type': 'percent',
+ 'include_base_amount': False,
+ })
+ analytic_account = self.env['account.analytic.account'].create({
+ 'name': 'test account',
+ })
+
+ # Should be changed by automatic on_change later
+ invoice_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1).id
+ invoice_line_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1).id
+
+ invoice = self.env['account.invoice'].create({'partner_id': self.env.ref('base.res_partner_2').id,
+ 'account_id': invoice_account,
+ 'type': 'in_invoice',
+ })
+
+ invoice_line = self.env['account.invoice.line'].create({'product_id': self.env.ref('product.product_product_4').id,
+ 'quantity': 5.0,
+ 'price_unit': 100.0,
+ 'invoice_id': invoice.id,
+ 'name': 'product that cost 100',
+ 'account_id': invoice_line_account,
+ 'invoice_line_tax_ids': [(6, 0, [tax_fixed.id, tax_percent_included_base_incl.id, tax_percentage.id])],
+ 'account_analytic_id': analytic_account.id,
+ })
+ invoice.compute_taxes()
+
+ # check that Initially supplier bill state is "Draft"
+ self.assertTrue((invoice.state == 'draft'), "Initially vendor bill state is Draft")
+
+ #change the state of invoice to open by clicking Validate button
+ invoice.signal_workflow('invoice_open')
+
+ # Check if amount and corresponded base is correct for all tax scenarios given on a computational base
+ # Keep in mind that tax amount can be changed by the user at any time before validating (based on the invoice and tax laws applicable)
+ invoice_tax = invoice.tax_line_ids.sorted(key=lambda r: r.sequence)
+ self.assertEquals(invoice_tax.mapped('amount'), [50.0, 550.0, 220.0])
+ self.assertEquals(invoice_tax.mapped('base'), [500.0, 550.0, 1100.0])
+
+ #I cancel the account move which is in posted state and verifies that it gives warning message
+ with self.assertRaises(Warning):
+ invoice.move_id.button_cancel()
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 23021226ace..033394aa7e7 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -556,7 +556,7 @@
Manage time & material contracts or fixed-price recurring subscriptions.
-
Install Now
diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py
index 7c5585b174b..942c54dfdf1 100644
--- a/addons/account_asset/models/account_asset.py
+++ b/addons/account_asset/models/account_asset.py
@@ -153,7 +153,7 @@ class AccountAssetAsset(models.Model):
else:
if self.method == 'linear':
amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
- if self.prorata and self.category_id.type == 'purchase':
+ if self.prorata:
amount = amount_to_depr / self.method_number
if sequence == 1:
days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
@@ -174,7 +174,7 @@ class AccountAssetAsset(models.Model):
while depreciation_date <= end_date:
depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period)
undone_dotation_number += 1
- if self.prorata and self.category_id.type == 'purchase':
+ if self.prorata:
undone_dotation_number += 1
return undone_dotation_number
@@ -378,7 +378,7 @@ class AccountAssetAsset(models.Model):
@api.multi
def _compute_entries(self, date, group_entries=False):
- depreciation_ids = self.env['account.asset.depreciation.line'].with_context(depreciation_date=date).search([
+ depreciation_ids = self.env['account.asset.depreciation.line'].search([
('asset_id', 'in', self.ids), ('depreciation_date', '<=', date),
('move_check', '=', False)])
if group_entries:
diff --git a/addons/account_cancel/views/account_views.xml b/addons/account_cancel/views/account_views.xml
index 41e8371efe1..54478d06960 100644
--- a/addons/account_cancel/views/account_views.xml
+++ b/addons/account_cancel/views/account_views.xml
@@ -53,7 +53,8 @@
{'reload_on_button': true}
-
+
+
diff --git a/addons/barcodes/static/src/js/form_view_barcode_handler.js b/addons/barcodes/static/src/js/form_view_barcode_handler.js
index b00bce51c5e..7f2614f0ef1 100644
--- a/addons/barcodes/static/src/js/form_view_barcode_handler.js
+++ b/addons/barcodes/static/src/js/form_view_barcode_handler.js
@@ -7,6 +7,7 @@ var common = require('web.form_common');
var BarcodeEvents = require('barcodes.BarcodeEvents');
var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
var KanbanRecord = require('web_kanban.Record');
+var Dialog = require('web.Dialog');
var _t = core._t;
@@ -77,22 +78,38 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
} else {
var field = this.form_view.fields[this.m2x_field];
var view = field.viewmanager.active_view;
+ var $content = $('
').append($('', {type: 'text', class: 'o_set_qty_input'}));
if (this.last_scanned_barcode) {
- var new_qty = window.prompt(_t('Set quantity'), character) || "0";
- new_qty = new_qty.replace(',', '.');
- var record = this._get_records(field).find(function(record) {
- return record.get('product_barcode') === self.last_scanned_barcode;
+ this.dialog = new Dialog(this, {
+ title: _t('Set quantity'),
+ buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () {
+ var new_qty = this.$content.find('.o_set_qty_input').val();
+ var record = _.find(self._get_records(field), function (record) {
+ return record.get('product_barcode') === self.last_scanned_barcode;
+ });
+ if (record) {
+ var values = {};
+ values[self.quantity_field] = parseFloat(new_qty);
+ field.data_update(record.get('id'), values).then(function () {
+ view.controller.reload_record(record);
+ });
+ } else {
+ self._display_no_last_scanned_warning();
+ }
+ }}, {text: _t('Discard'), close: true}],
+ $content: $content,
+ }).open();
+ // This line set the value of the key which triggered the _set_quantity in the input
+ this.dialog.$content.find('.o_set_qty_input').focus().val(character);
+
+ var $selectBtn = this.dialog.$footer.find('.btn-primary');
+ core.bus.on('keypress', this.dialog, function(event){
+ if (event.which === 13) {
+ event.preventDefault();
+ $selectBtn.click();
+ }
});
- if (record) {
- var values = {};
- values[this.quantity_field] = parseFloat(new_qty);
- field.data_update(record.get('id'), values).then(function () {
- view.controller.reload_record(record);
- });
- } else {
- this._display_no_last_scanned_warning();
- }
} else {
this._display_no_last_scanned_warning();
}
@@ -159,8 +176,13 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
},
_get_records: function(field) {
- return field.viewmanager.active_view.controller.records || // tree view
- field.viewmanager.active_view.controller.widgets; // kanban view
+ var active_view = field.viewmanager.active_view;
+ if (active_view.type === "kanban") {
+ return active_view.controller.widgets;
+ } else {
+ // tree view case
+ return active_view.controller.records.records;
+ }
},
});
diff --git a/addons/base_action_rule/models/base_action_rule.py b/addons/base_action_rule/models/base_action_rule.py
index 9c0151fba53..7f13bf9f3ef 100644
--- a/addons/base_action_rule/models/base_action_rule.py
+++ b/addons/base_action_rule/models/base_action_rule.py
@@ -198,7 +198,14 @@ class BaseActionRule(models.Model):
# subscribe followers
if self.act_followers and hasattr(records, 'message_subscribe'):
- records.message_subscribe(self.act_followers.ids)
+ followers = self.env['mail.followers'].sudo().search(
+ [('res_model', '=', records._name),
+ ('res_id', 'in', records.ids),
+ ('partner_id', 'in', self.act_followers.ids),
+ ]
+ )
+ if not len(followers) == len(self.act_followers):
+ records.message_subscribe(self.act_followers.ids)
# execute server actions
if self.server_action_ids:
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 32dfcbd0cf9..2a497106735 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -14,6 +14,7 @@ import re
from odoo import api, fields, models
from odoo.tools.translate import _
from odoo.tools.mimetypes import guess_mimetype
+from odoo.tools.misc import ustr
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
try:
@@ -346,29 +347,37 @@ class Import(models.TransientModel):
if not date_patterns:
date_patterns = [pattern.replace('r', sep) for sep in separator for pattern in date_format]
date_patterns.extend([p.replace('Y', 'y') for p in date_patterns])
+ datetime_patterns = [options['datetime_format']] if options.get('datetime_format') else []
+ if not datetime_patterns:
+ datetime_patterns = [pattern + ' %H:%M:%S' for pattern in date_patterns]
+
current_date_pattern = False
- for date_pattern in date_patterns:
- date_ok = True
- datetime_ok = False
- for val in preview_values:
- if val == '':
- continue
- try:
- dt.strptime(val, date_pattern)
- except ValueError:
- date_ok = False
+ current_datetime_pattern = False
+
+ def check_patterns(patterns, preview_values):
+ for pattern in patterns:
+ match = True
+ for val in preview_values:
+ if not val:
+ continue
try:
- dt.strptime(val, date_pattern+' %H:%M:%S')
- datetime_ok = True
+ dt.strptime(val, pattern)
except ValueError:
- datetime_ok = False
+ match = False
break
- if date_ok or datetime_ok:
- current_date_pattern = date_pattern
- break
+ if match:
+ return pattern
+ return False
+
+ current_date_pattern = check_patterns(date_patterns, preview_values)
if current_date_pattern:
options['date_format'] = current_date_pattern
- return ['date'] if date_ok else ['datetime']
+ return ['date']
+
+ current_datetime_pattern = check_patterns(datetime_patterns, preview_values)
+ if current_datetime_pattern:
+ options['datetime_format'] = current_datetime_pattern
+ return ['datetime']
return ['text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html']
@@ -598,14 +607,19 @@ class Import(models.TransientModel):
# Parse date
index = import_fields.index(name)
dt = datetime.datetime
- field_date_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT
- if options.get('date_format', field_date_format) != field_date_format:
- for line in data:
+ server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT
+
+ if options.get('%s_format' % field['type'], server_format) != server_format:
+ user_format = ustr(options.get('%s_format' % field['type'])).encode('utf-8')
+ for num, line in enumerate(data):
if line[index]:
try:
- line[index] = dt.strftime(dt.strptime(line[index], options['date_format']), field_date_format)
- except ValueError:
- raise ValueError(_("Column %s contains incorrect values (value: %s does not match date format" % (name, line[index])))
+ line[index] = dt.strftime(dt.strptime(ustr(line[index]).encode('utf-8'), user_format), server_format)
+ except ValueError, e:
+ raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, ustr(e.message)))
+ except Exception, e:
+ raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, ustr(e.message)))
+
elif field['type'] in ('float', 'monetary') and name in import_fields:
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
# We should be able to manage both case
diff --git a/addons/base_import/static/src/js/base_import.js b/addons/base_import/static/src/js/base_import.js
index 1901a2c180e..b3b905b36a5 100644
--- a/addons/base_import/static/src/js/base_import.js
+++ b/addons/base_import/static/src/js/base_import.js
@@ -119,11 +119,12 @@ var DataImport = Widget.extend(ControlPanelMixin, {
opts: [
{name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'},
{name: 'separator', label: _lt("Separator:"), value: ','},
- {name: 'quoting', label: _lt("Quoting:"), value: '"'}
+ {name: 'quoting', label: _lt("Text Delimiter:"), value: '"'}
],
parse_opts: [
{name: 'date_format', label: _lt("Date Format:"), value: ''},
- {name: 'float_thousand_separator', label: _lt("Thousand Separator:"), value: ','},
+ {name: 'datetime_format', label: _lt("Datetime Format:"), value: ''},
+ {name: 'float_thousand_separator', label: _lt("Thousands Separator:"), value: ','},
{name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'}
],
events: {
@@ -295,8 +296,8 @@ var DataImport = Widget.extend(ControlPanelMixin, {
self.$('input.oe_import_' + opt.name).val();
});
_(this.parse_opts).each(function (opt) {
- if (opt.name === 'date_format') {
- options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_date_format').val());
+ if (opt.name === 'date_format' || opt.name === 'datetime_format') {
+ options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_' + opt.name).val());
}
else {
options[opt.name] = self.$('input.oe_import_' + opt.name).val();
@@ -320,6 +321,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
.prop('disabled', true);
if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); }
this.$('.oe_import_date_format').val('');
+ this.$('.oe_import_datetime_format').val('');
this.$el.removeClass('oe_import_preview oe_import_error');
var import_toggle = false;
@@ -375,6 +377,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
}
this.$('.oe_import_date_format').val(time.strftime_to_moment_format(result.options.date_format));
+ this.$('.oe_import_datetime_format').val(time.strftime_to_moment_format(result.options.datetime_format));
this.$('.oe_import_float_thousand_separator').val(result.options.float_thousand_separator).change();
this.$('.oe_import_float_decimal_separator').val(result.options.float_decimal_separator).change();
if (result.debug === false){
diff --git a/addons/base_import/tests/test_base_import.py b/addons/base_import/tests/test_base_import.py
index 9fcadd27e5a..68a562971c4 100644
--- a/addons/base_import/tests/test_base_import.py
+++ b/addons/base_import/tests/test_base_import.py
@@ -379,6 +379,31 @@ class test_convert_import_data(TransactionCase):
['qux', '5', '6'],
])
+ def test_date_fields(self):
+ import_wizard = self.env['base_import.import'].create({
+ 'res_model': 'res.partner',
+ 'file': 'name,date,create_date\n'
+ '"foo","2013年07月18日","2016-10-12 06:06"\n',
+ 'file_type': 'text/csv'
+
+ })
+
+ results = import_wizard.do(
+ ['name', 'date', 'create_date'],
+ {
+ 'date_format': '%Y年%m月%d日',
+ 'datetime_format': '%Y-%m-%d %H:%M',
+ 'quoting': '"',
+ 'separator': ',',
+ 'headers': True
+ }
+ )
+
+ # if results empty, no errors
+ self.assertItemsEqual(results, [])
+
+
+
def test_filtered(self):
""" If ``False`` is provided as field mapping for a column,
that column should be removed from importable data
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 01b5a2c714d..24b0aa71b33 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -1372,7 +1372,7 @@ class Meeting(models.Model):
if not self._context.get('dont_notify'):
if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'):
partners_to_notify = meeting.partner_ids.ids
- event_attendees_changes = attendees_create and attendees_create[real_ids[0]]
+ event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]]
if event_attendees_changes:
partners_to_notify.append(event_attendees_changes['removed_partners'].ids)
self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify)
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index 2abb85b6ea8..f3071eb0a6c 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -526,7 +526,7 @@
-
+
diff --git a/addons/delivery/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml
index 271b86e1bae..7758b26a914 100644
--- a/addons/delivery/views/delivery_view.xml
+++ b/addons/delivery/views/delivery_view.xml
@@ -202,7 +202,7 @@
-
+
diff --git a/addons/hr_attendance/static/src/js/greeting_message.js b/addons/hr_attendance/static/src/js/greeting_message.js
index 113012dbb18..c0f120983ad 100644
--- a/addons/hr_attendance/static/src/js/greeting_message.js
+++ b/addons/hr_attendance/static/src/js/greeting_message.js
@@ -1,6 +1,8 @@
odoo.define('hr_attendance.greeting_message', function (require) {
"use strict";
+var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
+
var core = require('web.core');
var Model = require('web.Model');
var Widget = require('web.Widget');
@@ -8,27 +10,39 @@ var Widget = require('web.Widget');
var _t = core._t;
-var GreetingMessage = Widget.extend({
+var GreetingMessage = Widget.extend(BarcodeHandlerMixin, {
template: 'HrAttendanceGreetingMessage',
events: {
- "click .o_hr_attendance_button_dismiss": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
+ "click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
},
init: function(parent, action) {
- this._super.apply(this, arguments);
var self = this;
- var default_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_my_attendances', params: {} };
- if(!action.attendance){
- self.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group){
- if(has_group){
- self.next_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_kiosk_mode', target: 'fullscreen', params: {} };
+ this._super.apply(this, arguments);
+ BarcodeHandlerMixin.init.apply(this, arguments);
+
+ // if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return
+ // to the (likely) appropriate menu, according to the user access rights
+ if(!action.attendance) {
+ this.stop_listening();
+ this.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) {
+ if(has_group) {
+ self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
+ } else {
+ self.next_action = 'hr_attendance.hr_attendance_action_my_attendances';
}
});
return;
}
- this.next_action = action.next_action || default_action;
+
+ this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances';
+ // no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action)
+ if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') {
+ this.stop_listening();
+ }
this.attendance = action.attendance;
+ // check in/out times displayed in the greeting message template.
this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
this.previous_attendance_change_date = action.previous_attendance_change_date;
@@ -36,48 +50,42 @@ var GreetingMessage = Widget.extend({
},
start: function() {
- if (!this.attendance){
- return;
- }
- if (this.attendance.check_out) {
- this.farewell_message();
- } else {
- this.welcome_message();
+ if (this.attendance) {
+ this.attendance.check_out ? this.farewell_message() : this.welcome_message();
}
},
welcome_message: function() {
var self = this;
var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
- self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
+ this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
- if(now.getHours()<5){
- self.$('.o_hr_attendance_message_message').append(_t("Good night"));
- } else if(now.getHours() < 12){
- if(now.getHours() < 8 && Math.random() < .3){
- if(Math.random()<.75){
- self.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
+ if (now.getHours() < 5) {
+ this.$('.o_hr_attendance_message_message').append(_t("Good night"));
+ } else if (now.getHours() < 12) {
+ if (now.getHours() < 8 && Math.random() < 0.3) {
+ if (Math.random() < 0.75) {
+ this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
} else {
- self.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
+ this.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
}
-
} else {
- self.$('.o_hr_attendance_message_message').append(_t("Good morning"));
+ this.$('.o_hr_attendance_message_message').append(_t("Good morning"));
}
- } else if(now.getHours()<17){
- self.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
- } else if(now.getHours()<23){
- self.$('.o_hr_attendance_message_message').append(_t("Good evening"));
+ } else if (now.getHours() < 17){
+ this.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
+ } else if (now.getHours() < 23){
+ this.$('.o_hr_attendance_message_message').append(_t("Good evening"));
} else {
- self.$('.o_hr_attendance_message_message').append(_t("Good night"));
+ this.$('.o_hr_attendance_message_message').append(_t("Good night"));
}
if(this.previous_attendance_change_date){
var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){
- self.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
+ this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
} else {
- if(Math.random() < .02){
- self.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
+ if(Math.random() < 0.02){
+ this.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
}
}
}
@@ -86,38 +94,55 @@ var GreetingMessage = Widget.extend({
farewell_message: function() {
var self = this;
var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
- self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
+ this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
if(this.previous_attendance_change_date){
var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){
- self.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago. If this isn't right, please contact Human Resources."));
- clearTimeout(self.return_to_main_menu);
+ this.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago. If this isn't right, please contact Human Resources."));
+ clearTimeout(this.return_to_main_menu);
+ this.stop_listening();
} else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){
- self.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
+ this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
}
}
- if(now.getHours()<12){
- self.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
- } else if(now.getHours()<14){
- self.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
- if(Math.random() < 0.05){
- self.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
- } else if(Math.random() < 0.06){
- self.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
+ if (now.getHours() < 12) {
+ this.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
+ } else if (now.getHours() < 14) {
+ this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
+ if (Math.random() < 0.05) {
+ this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
+ } else if (Math.random() < 0.06) {
+ this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
}
- } else if(now.getHours()<17){
- self.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
+ } else if (now.getHours() < 17) {
+ this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
} else {
- if(now.getHours()<18 && Math.random()<0.2){
- self.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
+ if (now.getHours() < 18 && Math.random() < 0.2) {
+ this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
} else {
- self.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
+ this.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
}
}
},
+ on_barcode_scanned: function(barcode) {
+ var self = this;
+ if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
+ clearTimeout(this.return_to_main_menu);
+ }
+ var hr_employee = new Model('hr.employee');
+ hr_employee.call('attendance_scan', [barcode, ])
+ .then(function (result) {
+ if (result.action) {
+ self.do_action(result.action);
+ } else if (result.warning) {
+ self.do_warn(result.warning);
+ }
+ });
+ },
+
destroy: function () {
clearTimeout(this.return_to_main_menu);
this._super.apply(this, arguments);
diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js
index 60e75b73328..f8d113d27d6 100644
--- a/addons/hr_attendance/static/src/js/kiosk_confirm.js
+++ b/addons/hr_attendance/static/src/js/kiosk_confirm.js
@@ -55,7 +55,7 @@ var KioskConfirm = Widget.extend({
init: function (parent, action) {
this._super.apply(this, arguments);
- this.next_action = { type: "ir.actions.client", name: "Attendances", tag: 'hr_attendance_kiosk_mode', target: "fullscreen", params: {} };
+ this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
this.employee_id = action.employee_id;
this.employee_name = action.employee_name;
this.employee_state = action.employee_state;
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index f8dc372f39e..fa4c6498366 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -136,6 +136,7 @@ class HrExpense(models.Model):
'product_id': line.get('product_id'),
'product_uom_id': line.get('uom_id'),
'analytic_account_id': line.get('analytic_account_id'),
+ 'payment_id': line.get('payment_id'),
}
@api.multi
@@ -185,7 +186,10 @@ class HrExpense(models.Model):
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
'date': acc_date,
- 'ref': ref
+ 'ref': ref,
+ # force the name to the default value, to avoid an eventual 'default_name' in the context
+ # to set it to '' which cause no number to be given to the account.move when posted.
+ 'name': '/',
})
for expense in expense_list:
company_currency = expense.company_id.currency_id
@@ -194,11 +198,29 @@ class HrExpense(models.Model):
move_lines = expense._move_line_get()
#create one more move line, a counterline for the total on payable account
+ payment_id = False
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date)
if expense.payment_mode == 'company_account':
if not expense.sheet_id.bank_journal_id.default_credit_account_id:
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id
+ journal = expense.bank_journal_id
+ #create payment
+ payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids
+ journal_currency = journal.currency_id or journal.company_id.currency_id
+ payment = self.env['account.payment'].create({
+ 'payment_method_id': payment_methods and payment_methods[0].id or False,
+ 'payment_type': total < 0 and 'outbound' or 'inbound',
+ 'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
+ 'partner_type': 'supplier',
+ 'journal_id': journal.id,
+ 'payment_date': expense.date,
+ 'state': 'reconciled',
+ 'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id,
+ 'amount': diff_currency_p and abs(total_currency) or abs(total),
+ 'name': expense.name,
+ })
+ payment_id = payment.id
else:
if not expense.employee_id.address_home_id:
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
@@ -212,6 +234,7 @@ class HrExpense(models.Model):
'date_maturity': acc_date,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and expense.currency_id.id or False,
+ 'payment_id': payment_id,
})
#convert eml into an osv-valid format
diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py
index fdec5c0ad5e..fe52be16430 100644
--- a/addons/hr_holidays/models/hr.py
+++ b/addons/hr_holidays/models/hr.py
@@ -213,5 +213,5 @@ class Employee(models.Model):
('date_from', '<=', today_end),
('date_to', '>=', today_start),
('type', '=', 'remove')
- ], ['employee_id'])
+ ])
return [('id', 'in', holidays.mapped('employee_id').ids)]
diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py
index 6caad2b3aaf..fbb9b58f876 100644
--- a/addons/hw_escpos/controllers/main.py
+++ b/addons/hw_escpos/controllers/main.py
@@ -77,10 +77,15 @@ class EscposDriver(Thread):
printers = usb.core.find(find_all=True, idVendor=0x0519)
for printer in printers:
+ try:
+ description = usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
+ except Exception as e:
+ _logger.error("Can not get printer description: %s" % (e.message or repr(e)))
+ description = 'Unknown printer'
connected.append({
'vendor': printer.idVendor,
'product': printer.idProduct,
- 'name': usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
+ 'name': description
})
return connected
diff --git a/addons/l10n_in/data/account_tax_template_data.xml b/addons/l10n_in/data/account_tax_template_data.xml
index 43251bc2ad8..85fb8c4c6d6 100644
--- a/addons/l10n_in/data/account_tax_template_data.xml
+++ b/addons/l10n_in/data/account_tax_template_data.xml
@@ -462,7 +462,7 @@
-
+ (standard)Input Sales Tax @ 12%
@@ -474,7 +474,7 @@
-
+ (standard)Input Sales Tax @ 4%
@@ -486,7 +486,7 @@
-
+ (standard)Additional Input VAT @ 1%
@@ -605,7 +605,7 @@
-
+ (standard)Input Education Cess @ 2% On Service Tax
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 599cec0493c..a733f2f329b 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -1109,37 +1109,23 @@ class MailThread(models.AbstractModel):
reply_match = False
if reply_match:
- compat_mode = False
msg_references = tools.mail_header_msgid_re.findall(thread_references)
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
- # message is a reply to an existing thread (6.1 compatibility)
- # do not match forwarded emails from another OpenERP system (thread_id collision!)
- if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env:
- mail_messages = MailMessage.search([
- ('message_id', '=', False),
- ('model', '=', reply_model),
- ('res_id', '=', reply_thread_id)])
- compat_mode = True
-
if mail_messages:
model, thread_id = mail_messages.model, mail_messages.res_id
if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior
dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)], limit=1)
- # TDE Note: compat mode = without context key, why ? because
route = self.message_route_verify(
message, message_dict,
(model, thread_id, custom_values, self._uid, dest_aliases),
update_author=True, assert_model=reply_private, create_fallback=True,
allow_private=reply_private, drop_alias=True)
if route:
- # TDE Note: compat mode: parent is invalid for a compat-reply
- # message_dict.pop('parent_id', None)
- # TDE note: add compat mode for compat mode in debug
_logger.info(
- 'Routing mail from %s to %s with Message-Id %s%s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
- email_from, email_to, message_id, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid)
+ 'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
return [route]
elif route is False:
return []
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py
index 707e316bca5..b729a105b98 100644
--- a/addons/mail/tests/test_mail_gateway.py
+++ b/addons/mail/tests/test_mail_gateway.py
@@ -580,25 +580,29 @@ class TestMailgateway(TestMail):
msg_id='<1198923581.41972151344608186802.JavaMail.diff1@agrolait.com>')
# when 6.1 messages are present, compat mode is available
+ # Odoo 10 update: compat mode has been removed and should not work anymore
self.fake_email.write({'message_id': False})
# Do: compat mode accepts partial-matching emails
- frog_groups = self.format_and_process(
+ self.assertRaises(
+ ValueError,
+ self.format_and_process,
MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.2.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam',
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@%s>' % (self.group_public.id, socket.gethostname()))
# 3''. 6.1 compat mode should not work if hostname does not match!
+ # Odoo 10 update: compat mode has been removed and should not work anymore and does not depend from hostname
self.assertRaises(ValueError,
self.format_and_process,
MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.3.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam',
- extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % frog_groups.id)
+ extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % self.group_public.id)
# Test created messages
- self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain 6 messages')
- self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0, 'message_process: msg1 should not have children')
+ self.assertEqual(len(self.group_public.message_ids), 1)
+ self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0)
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_message_process_duplicate(self):
diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js
index 62bf848aa44..c2b3fad52f0 100644
--- a/addons/point_of_sale/static/src/js/db.js
+++ b/addons/point_of_sale/static/src/js/db.js
@@ -410,6 +410,10 @@ var PosDB = core.Class.extend({
}
}
+ // Only necessary when we store a new, validated order. Orders
+ // that where already stored should already have been removed.
+ this.remove_unpaid_order(order);
+
orders.push({id: order_id, data: order});
this.save('orders',orders);
return order_id;
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 68476c9f5e8..164fd5f7040 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1895,7 +1895,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
}
// if the change is too large, it's probably an input error, make the user confirm.
- if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
+ if (!force_validation && order.get_total_with_tax() > 0 && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
this.gui.show_popup('confirm',{
title: _t('Please Confirm Large Amount'),
body: _t('Are you sure that the customer wants to pay') +
diff --git a/addons/pos_cache/models/pos_cache.py b/addons/pos_cache/models/pos_cache.py
index c082a8cfe94..fb0fb20641e 100644
--- a/addons/pos_cache/models/pos_cache.py
+++ b/addons/pos_cache/models/pos_cache.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
+import base64
from ast import literal_eval
from odoo import models, fields, api
@@ -29,7 +29,7 @@ class pos_cache(models.Model):
prod_ctx = prod_ctx.sudo(self.compute_user_id.id)
res = prod_ctx.read(self.get_product_fields())
datas = {
- 'cache': cPickle.dumps(res),
+ 'cache': base64.encodestring(cPickle.dumps(res)),
}
self.write(datas)
@@ -49,7 +49,8 @@ class pos_cache(models.Model):
self.product_fields = str(fields)
self.refresh_cache()
- return cPickle.loads(self.cache)
+ cache = base64.decodestring(self.cache)
+ return cPickle.loads(cache)
class pos_config(models.Model):
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index fdfdf7c49e1..dd351d2d564 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -30,6 +30,35 @@ class AccountInvoice(models.Model):
]}
return result
+ def _prepare_invoice_line_from_po_line(self, line):
+ if line.product_id.purchase_method == 'purchase':
+ qty = line.product_qty - line.qty_invoiced
+ else:
+ qty = line.qty_received - line.qty_invoiced
+ if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
+ qty = 0.0
+ taxes = line.taxes_id
+ invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
+ invoice_line = self.env['account.invoice.line']
+ data = {
+ 'purchase_line_id': line.id,
+ 'name': self.purchase_id.name+': '+line.name,
+ 'origin': self.purchase_id.origin,
+ 'uom_id': line.product_uom.id,
+ 'product_id': line.product_id.id,
+ 'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
+ 'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
+ 'quantity': qty,
+ 'discount': 0.0,
+ 'account_analytic_id': line.account_analytic_id.id,
+ 'analytic_tag_ids': line.analytic_tag_ids.ids,
+ 'invoice_line_tax_ids': invoice_line_tax_ids.ids
+ }
+ account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
+ if account:
+ data['account_id'] = account.id
+ return data
+
# Load all unsold PO lines
@api.onchange('purchase_id')
def purchase_order_change(self):
@@ -43,31 +72,7 @@ class AccountInvoice(models.Model):
# Load a PO line only once
if line in self.invoice_line_ids.mapped('purchase_line_id'):
continue
- if line.product_id.purchase_method == 'purchase':
- qty = line.product_qty - line.qty_invoiced
- else:
- qty = line.qty_received - line.qty_invoiced
- if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
- qty = 0.0
- taxes = line.taxes_id
- invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
- data = {
- 'purchase_line_id': line.id,
- 'name': self.purchase_id.name+': '+line.name,
- 'origin': self.purchase_id.origin,
- 'uom_id': line.product_uom.id,
- 'product_id': line.product_id.id,
- 'account_id': self.env['account.invoice.line'].with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
- 'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
- 'quantity': qty,
- 'discount': 0.0,
- 'account_analytic_id': line.account_analytic_id.id,
- 'analytic_tag_ids': line.analytic_tag_ids.ids,
- 'invoice_line_tax_ids': invoice_line_tax_ids.ids
- }
- account = new_lines.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
- if account:
- data['account_id'] = account.id
+ data = self._prepare_invoice_line_from_po_line(line)
new_line = new_lines.new(data)
new_line._set_additional_fields(self)
new_lines += new_line
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
index 0731b4456d5..86ad0cc1ca4 100644
--- a/addons/purchase_requisition/models/purchase_requisition.py
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -194,16 +194,16 @@ class PurchaseOrder(models.Model):
taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id).ids
# Compute quantity and price_unit
- if requisition.type_id.quantity_copy != 'copy':
- product_qty = 0
- price_unit = line.price_unit
- elif line.product_uom_id != line.product_id.uom_po_id:
+ if line.product_uom_id != line.product_id.uom_po_id:
product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id)
price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id)
else:
product_qty = line.product_qty
price_unit = line.price_unit
+ if requisition.type_id.quantity_copy != 'copy':
+ product_qty = 0
+
# Compute price_unit in appropriate currency
if requisition.company_id.currency_id != currency:
price_unit = requisition.company_id.currency_id.compute(price_unit, currency)
@@ -258,6 +258,24 @@ class PurchaseOrder(models.Model):
return result
+class PurchaseOrderLine(models.Model):
+ _inherit = "purchase.order.line"
+
+ @api.onchange('product_qty', 'product_uom')
+ def _onchange_quantity(self):
+ res = super(PurchaseOrderLine, self)._onchange_quantity()
+ if self.order_id.requisition_id:
+ for line in self.order_id.requisition_id.line_ids:
+ if line.product_id == self.product_id:
+ if line.product_uom_id != self.product_uom:
+ self.price_unit = self.env['product.uom']._compute_price(
+ line.product_uom_id.id, line.price_unit, to_uom_id=self.product_uom.id)
+ else:
+ self.price_unit = line.price_unit
+ break
+ return res
+
+
class ProductTemplate(models.Model):
_inherit = 'product.template'
diff --git a/addons/rating_project/models/project.py b/addons/rating_project/models/project.py
index a82ca8cc1b9..345576768ae 100644
--- a/addons/rating_project/models/project.py
+++ b/addons/rating_project/models/project.py
@@ -10,10 +10,13 @@ class ProjectTaskType(models.Model):
_inherit = 'project.task.type'
+ def _default_domain_rating_template_id(self):
+ return [('model', '=', 'project.task')]
+
rating_template_id = fields.Many2one(
'mail.template',
string='Rating Email Template',
- domain=[('model', '=', 'project.task')],
+ domain=lambda self: self._default_domain_rating_template_id(),
help="Select an email template. An email will be sent to the customer when the task reach this step.")
auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False,
help="Automatically modify the kanban state when the customer reply to the feedback for this stage.\n"
diff --git a/addons/rating_project_issue/models/project_issue.py b/addons/rating_project_issue/models/project_issue.py
index f771f328f84..5b2b126cad2 100644
--- a/addons/rating_project_issue/models/project_issue.py
+++ b/addons/rating_project_issue/models/project_issue.py
@@ -28,6 +28,16 @@ class ProjectIssue(models.Model):
return super(ProjectIssue, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project_issue.mt_issue_rating")
+
+class Stage(models.Model):
+
+ _inherit = ['project.task.type']
+
+ def _default_domain_rating_template_id(self):
+ domain = super(Stage, self)._default_domain_rating_template_id()
+ return ['|'] + domain + [('model', '=', 'project.issue')]
+
+
class Project(models.Model):
_inherit = "project.project"
diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py
index e3dbc4ed176..84ab9490389 100644
--- a/addons/sale/wizard/sale_make_invoice_advance.py
+++ b/addons/sale/wizard/sale_make_invoice_advance.py
@@ -83,6 +83,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
else:
amount = self.amount
name = _('Down Payment')
+ if order.fiscal_position_id and self.product_id.taxes_id:
+ tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
+ else:
+ tax_ids = self.product_id.taxes_id.ids
invoice = inv_obj.create({
'name': order.client_order_ref or order.name,
@@ -102,7 +106,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'uom_id': self.product_id.uom_id.id,
'product_id': self.product_id.id,
'sale_line_ids': [(6, 0, [so_line.id])],
- 'invoice_line_tax_ids': [(6, 0, [x.id for x in self.product_id.taxes_id])],
+ 'invoice_line_tax_ids': [(6, 0, tax_ids)],
'account_analytic_id': order.project_id.id or False,
})],
'currency_id': order.pricelist_id.currency_id.id,
@@ -142,6 +146,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
if self.product_id.type != 'service':
raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
+ if order.fiscal_position_id and self.product_id.taxes_id:
+ tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
+ else:
+ tax_ids = self.product_id.taxes_id.ids
so_line = sale_line_obj.create({
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
'price_unit': amount,
@@ -150,7 +158,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'discount': 0.0,
'product_uom': self.product_id.uom_id.id,
'product_id': self.product_id.id,
- 'tax_id': [(6, 0, self.product_id.taxes_id.ids)],
+ 'tax_id': [(6, 0, tax_ids)],
'is_downpayment': True,
})
self._create_invoice(order, so_line, amount)
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index dc4b52bdf14..1738bb54216 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -393,16 +393,16 @@ class ProductTemplate(models.Model):
('none', 'No Tracking')], string="Tracking", default='none', required=True)
description_picking = fields.Text('Description on Picking', translate=True)
qty_available = fields.Float(
- 'Quantity On Hand', compute='_compute_quantities', search='_search_quantities',
+ 'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
digits=dp.get_precision('Product Unit of Measure'))
virtual_available = fields.Float(
- 'Forecasted Quantity', compute='_compute_quantities', search='_search_quantities',
+ 'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available',
digits=dp.get_precision('Product Unit of Measure'))
incoming_qty = fields.Float(
- 'Incoming', compute='_compute_quantities', search='_search_quantities',
+ 'Incoming', compute='_compute_quantities', search='_search_incoming_qty',
digits=dp.get_precision('Product Unit of Measure'))
outgoing_qty = fields.Float(
- 'Outgoing', compute='_compute_quantities', search='_search_quantities',
+ 'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
digits=dp.get_precision('Product Unit of Measure'))
location_id = fields.Many2one('stock.location', 'Location')
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse')
@@ -452,9 +452,23 @@ class ProductTemplate(models.Model):
}
return prod_available
- def _search_quantities(self, operator, value):
- # TDE FIXME: does this work anyway ?
- domain = [] # TDE ADDED
+ def _search_qty_available(self, operator, value):
+ domain = [('qty_available', operator, value)]
+ product_variant_ids = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variant_ids.ids)]
+
+ def _search_virtual_available(self, operator, value):
+ domain = [('virtual_available', operator, value)]
+ product_variant_ids = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variant_ids.ids)]
+
+ def _search_incoming_qty(self, operator, value):
+ domain = [('incoming_qty', operator, value)]
+ product_variant_ids = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variant_ids.ids)]
+
+ def _search_outgoing_qty(self, operator, value):
+ domain = [('outgoing_qty', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py
index 228e921c259..47f080a1109 100644
--- a/addons/stock/models/stock_inventory.py
+++ b/addons/stock/models/stock_inventory.py
@@ -91,14 +91,12 @@ class Inventory(models.Model):
('category', _('One product category')),
('product', _('One product only')),
('partial', _('Select products manually'))]
- stock_settings = self.env['stock.config.settings'].search([], limit=1, order='id DESC')
- if not stock_settings:
- return res_filter
- if stock_settings.group_stock_tracking_owner:
+
+ if self.user_has_groups('stock.group_tracking_owner'):
res_filter += [('owner', _('One owner only')), ('product_owner', _('One product for a specific owner'))]
- if stock_settings.group_stock_production_lot:
+ if self.user_has_groups('stock.group_production_lot'):
res_filter.append(('lot', _('One Lot/Serial Number')))
- if stock_settings.group_stock_tracking_lot:
+ if self.user_has_groups('stock.group_tracking_lot'):
res_filter.append(('pack', _('A Pack')))
return res_filter
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 7cb80c8b360..0ddd9de24d2 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -194,7 +194,6 @@ class StockMove(models.Model):
@api.multi
def _compute_string_qty_information(self):
- StockConfig = self.env['stock.config.settings']
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
void_moves = self.filtered(lambda move: move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal')
other_moves = self - void_moves
@@ -205,9 +204,7 @@ class StockMove(models.Model):
total_available = move.product_id.uom_id._compute_quantity(total_available, move.product_uom, round=False)
total_available = float_round(total_available, precision_digits=precision)
info = str(total_available)
- # look in the settings if we need to display the UoM name or not
- config = StockConfig.search([], limit=1, order='id DESC')
- if config and config.group_uom:
+ if self.user_has_groups('product.group_uom'):
info += ' ' + move.product_uom.name
if move.reserved_availability:
if move.reserved_availability != total_available:
@@ -380,7 +377,7 @@ class StockMove(models.Model):
# if no specialized push rule has been found yet, we try to find a general one (without route)
rules = Push.search(domain + [('route_id', '=', False)], order='sequence')
# Make sure it is not returning the return
- if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_id.id != rules.location_dest_id.id):
+ if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_dest_id.id != rules.location_dest_id.id):
rules._apply(move)
return True
diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py
index 9ddc1c686f4..5dba13166ae 100644
--- a/addons/stock_account/models/account_invoice.py
+++ b/addons/stock_account/models/account_invoice.py
@@ -77,7 +77,7 @@ class AccountInvoiceLine(models.Model):
def _get_price(self, company_currency, price_unit):
if self.invoice_id.currency_id.id != company_currency:
- price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id)
+ price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id)
else:
price = price_unit * self.quantity
return round(price, self.invoice_id.currency_id.decimal_places)
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index 5ab32db789a..a81b0260253 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -3,6 +3,7 @@
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
+from odoo.tools import float_is_zero
class ProductTemplate(models.Model):
@@ -105,7 +106,7 @@ class ProductProduct(models.Model):
for location in locations:
for product in self.with_context(location=location.id, compute_child=False):
diff = product.standard_price - new_price
- if diff:
+ if float_is_zero(diff, precision_rounding=product.currency_id.rounding):
raise UserError(_("No difference between standard price and new price!"))
if not product_accounts[product.id].get('stock_valuation', False):
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 4cf59568085..634c3cfbdd0 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -115,6 +115,11 @@ class StockQuant(models.Model):
else:
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id)
+ if move.company_id.anglo_saxon_accounting and move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'customer':
+ # Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
+ journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
+ self.with_context(force_company=move.company_id.id)._create_account_move_line(move, acc_src, acc_dest, journal_id)
+
def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id):
# group quants by cost
quant_cost_qty = defaultdict(lambda: 0.0)
diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js
index 4255f163068..e0d0135185f 100644
--- a/addons/web/static/src/js/view_manager.js
+++ b/addons/web/static/src/js/view_manager.js
@@ -459,6 +459,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
},
do_load_state: function(state, warm) {
+ if (state.view_type && state.view_type !== this.active_view.type) {
+ this.switch_mode(state.view_type, true);
+ }
this.active_view.controller.do_load_state(state, warm);
},
});
diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js
index 82c3bc8c4a7..65e94e671eb 100644
--- a/addons/web/static/src/js/views/form_common.js
+++ b/addons/web/static/src/js/views/form_common.js
@@ -430,6 +430,7 @@ var FormWidget = Widget.extend(InvisibilityChangerMixin, {
this.process_modifiers();
this._super();
this.$el.addClass(this.node.attrs["class"] || "");
+ this.$el.attr('style', this.node.attrs.style);
},
destroy: function() {
$.fn.tooltip('destroy');
diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js
index dd4c4fb0fb6..00640696b41 100644
--- a/addons/web/static/src/js/views/form_relational_widgets.js
+++ b/addons/web/static/src/js/views/form_relational_widgets.js
@@ -1275,6 +1275,9 @@ var FieldOne2Many = FieldX2Many.extend({
return self.mutex.def;
});
},
+ is_false: function() {
+ return false;
+ },
});
var Many2ManyListView = X2ManyListView.extend({
diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js
index f2e4f565a79..9382b255b01 100644
--- a/addons/web/static/src/js/views/list_view.js
+++ b/addons/web/static/src/js/views/list_view.js
@@ -1733,7 +1733,7 @@ var Column = Class.extend({
}
var aggregation_func = (this.sum && 'sum') || (this.avg && 'avg') ||
- (this.max && 'max') || (this.min && 'min') || this.group_operator;
+ (this.max && 'max') || (this.min && 'min');
if (!aggregation_func) {
return {};
diff --git a/addons/web/static/src/js/web_client.js b/addons/web/static/src/js/web_client.js
index d94f686c440..52962ed284b 100644
--- a/addons/web/static/src/js/web_client.js
+++ b/addons/web/static/src/js/web_client.js
@@ -113,25 +113,37 @@ return AbstractWebClient.extend({
}
},
on_hashchange: function(event) {
- var self = this;
- var stringstate = event.getState(false);
- if (!_.isEqual(this._current_state, stringstate)) {
- var state = event.getState(true);
- if(!state.action && state.menu_id) {
- self.menu.is_bound.done(function() {
- self.menu.menu_click(state.menu_id);
- });
- } else {
- state._push_me = false; // no need to push state back...
- this.action_manager.do_load_state(state, !!this._current_state).then(function () {
- var action = self.action_manager.get_inner_action();
- if (action) {
- self.menu.open_action(action.action_descr.id);
- }
- });
- }
+ if (this._ignore_hashchange) {
+ this._ignore_hashchange = false;
+ return;
}
- this._current_state = stringstate;
+
+ var self = this;
+ this.clear_uncommitted_changes().then(function () {
+ var stringstate = event.getState(false);
+ if (!_.isEqual(self._current_state, stringstate)) {
+ var state = event.getState(true);
+ if(!state.action && state.menu_id) {
+ self.menu.is_bound.done(function() {
+ self.menu.menu_click(state.menu_id);
+ });
+ } else {
+ state._push_me = false; // no need to push state back...
+ self.action_manager.do_load_state(state, !!self._current_state).then(function () {
+ var action = self.action_manager.get_inner_action();
+ if (action) {
+ self.menu.open_action(action.action_descr.id);
+ }
+ });
+ }
+ }
+ self._current_state = stringstate;
+ }, function () {
+ if (event) {
+ self._ignore_hashchange = true;
+ window.location = event.originalEvent.oldURL;
+ }
+ });
},
on_menu_action: function(options) {
var self = this;
diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less
index e992ab5a748..7d870122502 100644
--- a/addons/web/static/src/less/form_view.less
+++ b/addons/web/static/src/less/form_view.less
@@ -460,6 +460,15 @@
}
}
}
+
+ // Notebooks
+ .o_notebook {
+ .nav-tabs {
+ > .tab-pane {
+ min-height: 100px;
+ }
+ }
+ }
}
// Overriden style when form view in modal
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 923b78492b3..5ea843bcaae 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -226,7 +226,7 @@ class Web_Editor(http.Controller):
@http.route('/web_editor/customize_template_get', type='json', auth='user', website=True)
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``.
- returns templates (which can be active or not)
+ returns templates info (which can be active or not)
``bundles=True`` returns also the asset bundles
"""
return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 64ee45b567d..2b0af052c23 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -175,42 +175,21 @@ class IrUiView(models.Model):
return views_to_return
@api.model
- def customize_template_get(self, key, full=False, bundles=False):
- """ Get inherit view's informations of the template ``key``.
- returns templates (which can be active or not)
+ def _customize_template_get_views(self, key, full=False, bundles=False):
+ """ Get inherit views of the template ``key``.
+ returns views (which can be active or not)
``bundles=True`` returns also the asset bundles
"""
- user = self.env.user
- user_groups = set(user.groups_id)
+ user_groups = set(self.env.user.groups_id)
views = self.with_context(active_test=False)._views_get(key, bundles=bundles)
+ views = views.filtered(lambda v: user_groups.issuperset(v.groups_id))
+ return views
- view_theme_id = self.env['ir.model.data'].xmlid_to_res_id('website.theme')
- done = set()
- result = []
- for view in views:
- if full or (view.customize_show and view.inherit_id.id != view_theme_id):
- if not user_groups.issuperset(view.groups_id):
- continue
- if view.inherit_id not in done:
- result.append({
- 'name': view.inherit_id.name,
- 'id': view.id,
- 'key': view.key,
- 'xml_id': view.xml_id,
- 'arch': view.arch,
- 'inherit_id': view.inherit_id.id,
- 'header': True,
- 'active': False
- })
- done.add(view.inherit_id)
- result.append({
- 'name': view.name,
- 'id': view.id,
- 'key': view.key,
- 'xml_id': view.xml_id,
- 'arch': view.arch,
- 'inherit_id': view.inherit_id.id,
- 'header': False,
- 'active': view.active,
- })
- return result
+ @api.model
+ def customize_template_get(self, key, full=False, bundles=False):
+ """ Get inherit view's informations of the template ``key``.
+ returns templates info (which can be active or not)
+ ``bundles=True`` returns also the asset bundles
+ """
+ views = self._customize_template_get_views(key, full=full, bundles=bundles)
+ return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js
index 5fbe7589265..56500383851 100644
--- a/addons/web_editor/static/src/js/ace.js
+++ b/addons/web_editor/static/src/js/ace.js
@@ -149,7 +149,7 @@ var ViewEditor = Widget.extend({
view.children = [];
});
_.each(index, function (view) {
- var parentId = view.inherit_id;
+ var parentId = view.inherit_id[0];
if (parentId && index[parentId]) {
index[parentId].children.push(view);
} else {
diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js
index 7a3ab09dd76..61ec1564ac5 100644
--- a/addons/web_editor/static/src/js/widgets.js
+++ b/addons/web_editor/static/src/js/widgets.js
@@ -1145,7 +1145,7 @@ var LinkDialog = Dialog.extend({
$preview.attr("target", new_window ? '_blank' : "")
.attr("href", url && url.length ? url : "#")
.html((label && label.length ? label : url))
- .attr("class", classes.replace(/pull-\w+/, ''));
+ .attr("class", classes.replace(/pull-\w+/, '') + " o_btn_preview");
});
}
});
diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less
index 7dd882ffbd0..44f3923c35e 100644
--- a/addons/web_editor/static/src/less/web_editor.ui.components.less
+++ b/addons/web_editor/static/src/less/web_editor.ui.components.less
@@ -189,7 +189,7 @@ body .modal {
ul.pager {
margin: 0;
> li > a {
- border-radius: 3px;
+ border-radius: 0px;
margin-left: 2px;
padding: 6px 14px;
}
@@ -206,20 +206,34 @@ body .modal {
}
}
}
- .dropdown-toggle {
- line-height: 0;
- height: 34px;
+
+ .well {
+ border-top-width: 0;
+
+ label {
+ margin: auto 10px;
+ }
+
+ .btn-group {
+ margin-right: 10px;
+ }
}
+
.o_existing_attachment_cell {
@odoo-border-image: 1px;
.o_existing_attachment_remove {
.o-position-absolute(@odoo-border-image, 15px + @odoo-border-image);
- padding: 3px 3px 12px 12px;
- background-color: white;
- border-bottom: 1px solid black;
- border-left: 1px solid black;
- border-radius: 0 0 0 100%;
- opacity: 0.8;
+ padding: 3px 5px 5px 5px;
+ background-color: fade(white, 80%);
+ text-shadow: 0 0 1px white;
+ border-radius: 0 0 0 2px;
+ opacity: 0;
+ cursor: pointer;
+
+ &:hover {
+ background-color: white;
+ color: @o-we-color-danger;
+ }
}
.o_image {
border: @odoo-border-image solid transparent;
@@ -235,15 +249,29 @@ body .modal {
border-color: @odoo-brand-primary;
box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
}
+ &:hover {
+ .o_existing_attachment_remove {
+ opacity: 1;
+ }
+ }
}
- .font-icons-icons > span {
- font-size: 30px;
- margin: 5px;
- padding: 3px;
- cursor: pointer;
+ .font-icons-icons {
+ padding: 20px 0;
- &.o_selected {
- box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
+ > span {
+ text-align: center;
+ font-size: 22px;
+ margin: 5px;
+ width: 50px;
+ height: 50px;
+ padding: 15px;
+ cursor: pointer;
+
+ &.o_selected {
+ cursor: auto;
+ background-color: @o-we-color-paper;
+ box-shadow: 0px 0px 0px 1px @o-we-color-success;
+ }
}
}
}
diff --git a/addons/web_editor/static/src/less/web_editor.variables.less b/addons/web_editor/static/src/less/web_editor.variables.less
index 9e0c7a2a014..37e219302a0 100644
--- a/addons/web_editor/static/src/less/web_editor.variables.less
+++ b/addons/web_editor/static/src/less/web_editor.variables.less
@@ -9,6 +9,9 @@
@o-we-color-text-normal: #999999;
@o-we-color-text-light: #d4d5d7;
@o-we-color-danger: #e6586c;
+@o-we-color-warning: #f0ad4e;
+@o-we-color-success: #40ad67;
+@o-we-color-info: #6999a8;
@o-we-sidebar-width: 210px;
@@ -76,3 +79,119 @@
from { background: @odoo-brand-optional; }
to { width: 0; background: transparent; }
}
+
+
+// ============ Preservetion mixins ===============
+// Used to preserve web_editor and website UI from themes customizations
+
+.o-w-preserve-base() {
+ font-family: @o-we-font-family;
+ line-height: 1.42857143;
+ color: @o-we-color-divider;
+
+ .text-muted {
+ color: @o-we-color-text-normal;
+ }
+}
+
+.o-w-preserve-headings() {
+ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
+ font-family: @o-we-font-family;
+ line-height: 1.42857143;
+ color: @o-we-color-dark;
+ font-weight: bold;
+ }
+}
+
+.o-w-preserve-links() {
+ a:not(.o_btn_preview) {
+ color: @odoo-brand-optional;
+
+ &:focus, &:active, &:focus:active {
+ outline: none!important;
+ }
+ }
+}
+
+.o-w-preserve-forms() {
+ .form-control {
+ height: 34px;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857143;
+ border: 1px solid @o-we-color-text-light;
+ color: #555;
+ background-color: #fff;
+ border-radius: 0;
+ }
+}
+
+.o-w-preserve-modals() {
+ background: 0;
+
+ .modal-content {
+ border-radius: 0;
+ background-color: @o-we-color-paper;
+
+ .modal-body {
+ background-color: white;
+ }
+ .modal-footer {
+ text-align: left;
+ }
+ }
+}
+
+.o-w-preserve-btn() {
+ .btn:not(.o_btn_preview) {
+ border-width: 1px;
+ border-radius: 0;
+ font-weight: normal;
+ .button-size(6px; 12px; 14px; 1.42857143; 0);
+
+ &.btn-primary {
+ .button-variant(white, @odoo-brand-optional, darken(@odoo-brand-optional,5%));
+ text-transform: uppercase;
+ }
+ &.btn-default {
+ .button-variant(@odoo-brand-optional, white, @o-we-color-text-light);
+ text-transform: uppercase;
+ }
+ &.btn-link {
+ .button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
+ }
+ &.btn-success {
+ .button-variant(white; @o-we-color-success; darken(@o-we-color-success, 5%));
+ }
+ &.btn-info {
+ .button-variant(white; @o-we-color-info; darken(@o-we-color-info, 5%));
+ }
+ &.btn-warning {
+ .button-variant(white; @o-we-color-warning; darken(@o-we-color-warning, 5%));
+ }
+ &.btn-danger {
+ .button-variant(white; @o-we-color-danger; darken(@o-we-color-danger, 5%));
+ }
+
+ &.btn-lg {
+ .button-size(10px; 16px; 18px; 1.3333333; 0);
+ }
+ &.btn-sm {
+ .button-size(5px; 10px; 12px; 1.5; 0);
+ }
+ &.btn-xs {
+ .button-size(1px; 5px; 12px; 1.5; 0);
+ }
+ }
+}
+
+.o-w-preserve-wells() {
+ .well {
+ padding: 19px;
+ margin-bottom: 20px;
+ background-color: @o-we-color-paper;
+ border: 1px solid darken(@o-we-color-paper,5%);
+ border-radius: 0;
+ .box-shadow(none);
+ }
+}
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index 27213d7fdd8..2ad47703700 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -251,31 +251,31 @@
-
- Basic
+ Basic
- Success
+ Success
- Primary
+ Primary
- Info
+ Info
- Warning
+ Warning
- Danger
+ Danger
@@ -287,19 +287,19 @@
- Extra Small
+ Extra Small
- Small
+ Small
- Default
+ Default
- Large
+ Large
diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js
index b755c7559dd..7cfde911a92 100644
--- a/addons/web_kanban/static/src/js/kanban_view.js
+++ b/addons/web_kanban/static/src/js/kanban_view.js
@@ -62,7 +62,7 @@ var KanbanView = View.extend({
this.qweb = new QWeb(session.debug, {_s: session.origin});
- this.limit = this.options.limit || 40;
+ this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40;
this.fields = {};
this.fields_keys = _.keys(this.fields_view.fields);
this.grouped = undefined;
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 0826ac839d7..3a64816a248 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -262,10 +262,10 @@ class Website(Home):
@http.route('/website/customize_template_get', type='json', auth='user', website=True)
def customize_template_get(self, key, full=False, bundles=False):
- """ Get inherit view's informations of the template ``key``. By default, only
- :returns ``customize_show`` templates (which can be active or not), if
- ``full=True`` returns inherit view's informations of the template ``key``.
- ``bundles=True`` returns also the asset bundles
+ """ Get inherit view's informations of the template ``key``.
+ returns templates info (which can be active or not)
+ ``full=False`` returns only the customize_show template
+ ``bundles=True`` returns also the asset bundles
"""
return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
@@ -370,6 +370,7 @@ class Website(Home):
], type='http', auth="public", website=True)
def actions_server(self, path_or_xml_id_or_id, **post):
ServerActions = request.env['ir.actions.server']
+ action = action_id = None
# find the action_id: either an xml_id, the path, or an ID
if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index acd9256ea52..110ce7e39ee 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -136,25 +136,13 @@ class View(models.Model):
return qcontext
@api.model
- def customize_template_get(self, key, full=False, bundles=False):
- """ Get inherit view's informations of the template ``key``. By default, only
- :returns ``customize_show`` templates (which can be active or not), if
- ``full=True`` returns inherit view's informations of the template ``key``.
- ``bundles=True`` returns also the asset bundles
+ def _customize_template_get_views(self, key, full=False, bundles=False):
+ """ Get inherit view's informations of the template ``key``.
+ returns views (which can be active or not)
+ ``full=False`` returns only the customize_show template
+ ``bundles=True`` returns also the asset bundles
"""
- result = super(View, self).customize_template_get(key, full=full, bundles=bundles)
-
- imd = self.env['ir.model.data']
- view_theme_id = imd.xmlid_to_res_id('website.theme')
-
- result_filtered = []
- for x in result:
- if not full:
- view = self.browse(x['id'])
- if not view.customize_show:
- continue
- if view_theme_id and x['inherit_id'] == view_theme_id:
- continue
- result_filtered.append(x)
-
- return result_filtered
+ views = super(View, self)._customize_template_get_views(key, full=full, bundles=bundles)
+ if full:
+ return views
+ return views.filtered(lambda v: v.customize_show)
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js
index 5d2d7706560..a110e6eca3e 100644
--- a/addons/website/static/src/js/website.editor.js
+++ b/addons/website/static/src/js/website.editor.js
@@ -74,16 +74,17 @@ website.TopBarCustomize = Widget.extend({
key: this.view_name,
full: false
}).then(function (result) {
+ var current_group = "";
_.each(result, function (item) {
- if (item.header) {
- $menu.append('
';
this.$target.html(editor+size+'');
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index cfd0b561778..4fbb79dd89e 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -311,8 +311,10 @@ options.registry.parallax = options.Class.extend({
this.$target.data("snippet-view", new animation.registry.parallax(this.$target));
}
this.scroll();
- this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh.bind(this));
- this.$target.on('snippet-option-change snippet-option-preview', this._refresh.bind(this));
+
+ this._refresh_callback = this._refresh.bind(this);
+ this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh_callback);
+ this.$target.on('snippet-option-change snippet-option-preview', this._refresh_callback);
},
scroll: function (type, value) {
this.$target.attr("data-scroll-background-ratio", value);
@@ -330,6 +332,11 @@ options.registry.parallax = options.Class.extend({
this._super.apply(this, arguments);
this._refresh();
},
+ on_remove: function () {
+ this._super.apply(this, arguments);
+ this.$target.off("snippet-option-change snippet-option-preview", this._refresh_callback);
+ this.buildingBlock.$el.off("snippet-dropped snippet-activated", this._refresh_callback);
+ },
});
options.registry.ul = options.Class.extend({
diff --git a/addons/website/static/src/less/website.ui.components.less b/addons/website/static/src/less/website.ui.components.less
index e1230d2b358..5c2aa821e13 100644
--- a/addons/website/static/src/less/website.ui.components.less
+++ b/addons/website/static/src/less/website.ui.components.less
@@ -1,52 +1,25 @@
// MODALS
body .modal {
- a {
- &:focus, &:active, &:focus:active {
- outline: none!important;
- }
- }
&.o_website_modal {
- font-family: @o-we-font-family;
-
- h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
- font-family: @o-we-font-family;
- }
+ .o-w-preserve-base();
+ .o-w-preserve-headings();
+ .o-w-preserve-forms();
+ .o-w-preserve-links();
+ .o-w-preserve-btn();
+ .o-w-preserve-wells();
+ .o-w-preserve-modals();
.modal-content {
- border-radius: 0;
- color: initial!important;
- background-color: @o-we-color-paper!important;
.modal-header .o_subtitle {
margin-left: 10px;
}
.modal-body {
- color: inherit!important;
- background-color: transparent!important;
.o_modal_header {
.o-webclient-padding(@top: 10px, @bottom: 10px);
.clearfix();
}
}
- .modal-footer {
- text-align: left;
-
- .btn {
- border-radius: 0;
-
- &.btn-primary {
- .button-variant(white, @odoo-brand-optional, @odoo-brand-optional);
- text-transform: uppercase;
- }
- &.btn-default {
- .button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
- text-transform: uppercase;
- }
- &.btn-link {
- .button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
- }
- }
- }
}
@media (min-width: @screen-sm-min) {
@@ -89,7 +62,6 @@ body .modal {
.modal-content {
background-color: black!important;
- color: lightgray!important;
border: 3px outset gray;
border-radius: 20px;
@@ -99,6 +71,7 @@ body .modal {
font-family: @o-we-font-family;
h4 {
+ color: lightgray;
font-family:inherit;
font-weight: normal;
@@ -109,6 +82,8 @@ body .modal {
}
.modal-body {
+ background-color: inherit!important;
+ border-radius: 20px;
padding: 15px;
@mobile-preview-width: 320px;
@@ -324,6 +299,10 @@ body .modal {
// ACE EDITOR
.o_ace_view_editor {
+ .o-w-preserve-base();
+ .o-w-preserve-btn();
+ .o-w-preserve-forms();
+
.o-position-absolute(@odoo-navbar-height, 0, 0);
position: fixed;
z-index: 1001;
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index 5199b8b9f46..1d963c68026 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -139,6 +139,24 @@ class TestViewSaving(common.TransactionCase):
Company = self.env['res.company']
View = self.env['ir.ui.view']
+ # create a view with an xmlid, like the file import would
+ with self.env.norecompute():
+ self.view_id = View.create(self.cr, self.uid, {
+ 'name': "Test View",
+ 'type': 'qweb',
+ 'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8')
+ })
+ imd = self.env['ir.model.data'].create({
+ 'module': 'website',
+ 'name': 'test_view',
+ 'model': 'ir.ui.view',
+ 'res_id': self.view_id,
+ })
+
+ # the xml_id of the view should not be flagged as 'noupdate'
+ self.assertEqual(View.browse(self.cr, self.uid, self.view_id).model_data_id, imd)
+ self.assertFalse(imd.noupdate)
+
replacement = ET.tostring(h.DIV(
h.H3("Column 2"),
h.UL(
@@ -149,6 +167,9 @@ class TestViewSaving(common.TransactionCase):
), encoding='utf-8')
self.view_id.save(value=replacement, xpath='/div/div[2]')
+ # the xml_id of the view should be flagged as 'noupdate'
+ self.assertTrue(imd.noupdate)
+
company = Company.browse(1)
self.assertEqual(company.name, "Acme Corporation")
self.assertEqual(company.phone, "+12 3456789")
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index 63911cf92ff..ddc1f1a04c2 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -31,6 +31,20 @@ class Blog(models.Model):
post_ids.write({'active': vals['active']})
return res
+ @api.multi
+ def message_post(self, parent_id=False, subtype=None, **kwargs):
+ """ Temporary workaround to avoid spam. If someone replies on a channel
+ through the 'Presentation Published' email, it should be considered as a
+ note as we don't want all channel followers to be notified of this answer. """
+ self.ensure_one()
+ if parent_id:
+ parent_message = self.env['mail.message'].sudo().browse(parent_id)
+ if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_blog.mt_blog_blog_published'):
+ if kwargs.get('subtype_id'):
+ kwargs['subtype_id'] = False
+ subtype = 'mail.mt_note'
+ return super(Blog, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
+
@api.multi
def all_tags(self, min_limit=1):
req = """
@@ -87,11 +101,13 @@ class BlogPost(models.Model):
blog_post.website_url = "/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post))
@api.multi
+ @api.depends('post_date', 'visits')
def _compute_ranking(self):
res = {}
for blog_post in self:
- age = datetime.now() - fields.Datetime.from_string(blog_post.post_date)
- res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days)
+ if blog_post.id: # avoid to rank one post not yet saved and so withtout post_date in case of an onchange.
+ age = datetime.now() - fields.Datetime.from_string(blog_post.post_date)
+ res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days)
return res
def _default_content(self):
@@ -167,6 +183,8 @@ class BlogPost(models.Model):
def _set_post_date(self):
for blog_post in self:
blog_post.published_date = blog_post.post_date
+ if not blog_post.published_date:
+ blog_post._write(dict(post_date=blog_post.create_date)) # dont trigger inverse function
def _check_for_publication(self, vals):
if vals.get('website_published'):
diff --git a/addons/website_quote/views/website_quote_templates.xml b/addons/website_quote/views/website_quote_templates.xml
index 1bfa7e47a81..463f10b80d4 100644
--- a/addons/website_quote/views/website_quote_templates.xml
+++ b/addons/website_quote/views/website_quote_templates.xml
@@ -298,7 +298,7 @@
I agree that by signing this proposal, I
- accept it on the behalf of , for an amount of
+ accept it on the behalf of , for an amount of with payment terms: .
@@ -325,7 +325,7 @@
I agree that by paying this proposal, I
- accept it on the behalf of , for an amount of
+ accept it on the behalf of , for an amount of with payment terms: .
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index 9e53f039407..d5a8f77a72d 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -207,7 +207,6 @@ class Product(models.Model):
self.ensure_one()
return self.product_tmpl_id.website_publish_button()
-
class ProductAttribute(models.Model):
_inherit = "product.attribute"
diff --git a/addons/website_sale/static/src/js/website_sale_payment.js b/addons/website_sale/static/src/js/website_sale_payment.js
index 32d9a8dbca9..c4c4861654d 100644
--- a/addons/website_sale/static/src/js/website_sale_payment.js
+++ b/addons/website_sale/static/src/js/website_sale_payment.js
@@ -55,7 +55,7 @@ $(document).ready(function () {
}
$form.off('submit');
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', params).then(function (data) {
- $(data).submit();
+ $(data).appendTo('body').submit();
});
return false;
});
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index b6dc690cc99..d02da6b19cb 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -164,6 +164,20 @@ class Channel(models.Model):
self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']})
return res
+ @api.multi
+ def message_post(self, parent_id=False, subtype=None, **kwargs):
+ """ Temporary workaround to avoid spam. If someone replies on a channel
+ through the 'Presentation Published' email, it should be considered as a
+ note as we don't want all channel followers to be notified of this answer. """
+ self.ensure_one()
+ if parent_id:
+ parent_message = self.env['mail.message'].sudo().browse(parent_id)
+ if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'):
+ if kwargs.get('subtype_id'):
+ kwargs['subtype_id'] = False
+ subtype = 'mail.mt_note'
+ return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
+
class Category(models.Model):
""" Channel contain various categories to manage its slides """
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index b4376c99467..1e23c311da2 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -1208,6 +1208,7 @@ class IrModelData(models.Model):
datas = self.search([('module', 'in', modules_to_remove)])
wkf_todo = []
to_unlink = tools.OrderedSet()
+ undeletable = self.browse([])
for data in datas.sorted(key='id', reverse=True):
model = data.model
@@ -1232,6 +1233,7 @@ class IrModelData(models.Model):
_logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
def unlink_if_refcount(to_unlink):
+ undeletable = self.browse()
for model, res_id in to_unlink:
external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)])
if external_ids - datas:
@@ -1255,28 +1257,30 @@ class IrModelData(models.Model):
self.env[model].browse(res_id).unlink()
except Exception:
_logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
+ undeletable += external_ids
self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
else:
self._cr.execute('RELEASE SAVEPOINT record_unlink_save')
+ return undeletable
# Remove non-model records first, then model fields, and finish with models
- unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
- unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)])
constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)])
constraints._module_data_uninstall()
- unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)])
relations._module_data_uninstall()
- unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
+ undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
self._cr.commit()
- datas.unlink()
+ (datas - undeletable).unlink()
@api.model
def _process_end(self, modules):
diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py
index 67c755581d3..10bf7c627b4 100644
--- a/odoo/addons/base/ir/ir_ui_view.py
+++ b/odoo/addons/base/ir/ir_ui_view.py
@@ -229,10 +229,11 @@ actual arch.
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
view_wo_lang.arch = view.arch_base
+ @api.depends('write_date')
def _compute_model_data_id(self):
- # get the last ir_model_data record corresponding to self
+ # get the first ir_model_data record corresponding to self
domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
- for data in self.env['ir.model.data'].search_read(domain, ['res_id']):
+ for data in self.env['ir.model.data'].search_read(domain, ['res_id'], order='id desc'):
view = self.browse(data['res_id'])
view.model_data_id = data['id']
@@ -343,7 +344,15 @@ actual arch.
values['name'] = "%s %s" % (values.get('model'), values['type'])
self.clear_caches()
- return super(View, self).create(self._compute_defaults(values))
+
+ if 'install_mode_data' in self._context:
+ # the view is created from a data file by installing a module; delay
+ # the recomputation of field 'model_data_id' until its xmlid record
+ # is created
+ with self.env.norecompute():
+ return super(View, self).create(self._compute_defaults(values))
+ else:
+ return super(View, self).create(self._compute_defaults(values))
@api.multi
def write(self, vals):
@@ -1168,4 +1177,4 @@ actual arch.
try:
self.browse(vid)._check_xml()
except Exception as e:
- self.raise_view_error("Can't validate view: %s" % e.message, vid)
+ self.raise_view_error("Can't validate view:\n%s" % (e.message or repr(e)), vid)
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index 38f30308fab..adbabdc48ad 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -12,6 +12,14 @@ from odoo.tests import common
from odoo.tools import mute_logger
+class ViewXMLID(common.TransactionCase):
+ def test_model_data_id(self):
+ """ Check whether views know their xmlid record. """
+ view = self.env.ref('base.view_company_form')
+ self.assertTrue(view)
+ self.assertTrue(view.model_data_id)
+ self.assertEqual(view.model_data_id.complete_name, 'base.view_company_form')
+
class ViewCase(common.TransactionCase):
def setUp(self):
super(ViewCase, self).setUp()
diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py
index e80f6df0866..226ba9943f6 100644
--- a/odoo/addons/test_impex/tests/test_export.py
+++ b/odoo/addons/test_impex/tests/test_export.py
@@ -23,7 +23,7 @@ class CreatorCase(common.TransactionCase):
def export(self, value, fields=('value',), context=None):
record = self.make(value)
record.invalidate_cache()
- return record._BaseModel__export_rows([f.split('/') for f in fields])
+ return record._export_rows([f.split('/') for f in fields])
class test_boolean_field(CreatorCase):
@@ -449,7 +449,7 @@ class test_o2m_multiple(CreatorCase):
def export(self, value=None, fields=('child1', 'child2',), context=None, **values):
record = self.make(value, **values)
- return record._BaseModel__export_rows([f.split('/') for f in fields])
+ return record._export_rows([f.split('/') for f in fields])
def test_empty(self):
self.assertEqual(
diff --git a/odoo/addons/test_new_api/tests/__init__.py b/odoo/addons/test_new_api/tests/__init__.py
index 2bbcfe40990..b90ccc96825 100644
--- a/odoo/addons/test_new_api/tests/__init__.py
+++ b/odoo/addons/test_new_api/tests/__init__.py
@@ -3,5 +3,6 @@
from . import test_new_fields
from . import test_onchange
from . import test_attributes
+from . import test_one2many
from . import test_ui
from . import test_domain
diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py
index 87fc8229b7c..a6e2bff1b35 100644
--- a/odoo/addons/test_new_api/tests/test_onchange.py
+++ b/odoo/addons/test_new_api/tests/test_onchange.py
@@ -176,7 +176,7 @@ class TestOnChange(common.TransactionCase):
self.assertEqual(values, {
'name': partner.name,
'partner': partner.id,
- 'lines': [(5,), (4, line.id)],
+ 'lines': [(6, 0, [line.id])],
})
# modify 'partner'
diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py
new file mode 100644
index 00000000000..38dee13ab54
--- /dev/null
+++ b/odoo/addons/test_new_api/tests/test_one2many.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+from openerp.tests.common import TransactionCase
+
+
+class One2manyCase(TransactionCase):
+ def setUp(self):
+ super(One2manyCase, self).setUp()
+ self.Line = self.env["test_new_api.multi.line"]
+ self.multi = self.env["test_new_api.multi"].create({
+ "name": "What is up?"
+ })
+
+ def operations(self):
+ """Run operations on o2m fields to check all works fine."""
+ # Check the lines first
+ self.assertItemsEqual(
+ self.multi.lines.mapped('name'),
+ map(str, range(10)))
+ # Modify the first line and drop the last one
+ self.multi.lines[0].name = "hello"
+ self.multi.lines = self.multi.lines[:-1]
+ self.assertEqual(len(self.multi.lines), 9)
+ self.assertIn("hello", self.multi.lines.mapped('name'))
+ # Invalidate the cache and check again; this crashes if the value
+ # of self.multi.lines in cache contains new records
+ self.multi.invalidate_cache()
+ self.assertEqual(len(self.multi.lines), 9)
+ self.assertIn("hello", self.multi.lines.mapped('name'))
+
+ def test_new_one_by_one(self):
+ """Check lines created with ``new()`` and appended one by one."""
+ for name in range(10):
+ self.multi.lines |= self.Line.new({"name": str(name)})
+ self.operations()
+
+ def test_new_single(self):
+ """Check lines created with ``new()`` and added in one step."""
+ self.multi.lines = self.Line.browse(
+ [self.Line.new({"name": str(name)}).id for name in range(10)]
+ )
+ self.operations()
+
+ def test_create_one_by_one(self):
+ """Check lines created with ``create()`` and appended one by one."""
+ for name in range(10):
+ self.multi.lines |= self.Line.create({"name": str(name)})
+ self.operations()
+
+ def test_create_single(self):
+ """Check lines created with ``create()`` and added in one step."""
+ self.multi.lines = self.Line.browse(
+ [self.Line.create({"name": str(name)}).id for name in range(10)]
+ )
+ self.operations()
+
+ def test_rpcstyle_one_by_one(self):
+ """Check lines created with RPC style and appended one by one."""
+ for name in range(10):
+ self.multi.lines = [(0, 0, {"name": str(name)})]
+ self.operations()
+
+ def test_rpcstyle_single(self):
+ """Check lines created with RPC style and added in one step"""
+ self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
+ self.operations()
diff --git a/odoo/addons/test_pylint/tests/test_pylint.py b/odoo/addons/test_pylint/tests/test_pylint.py
index 251db1f6eba..38fa4c3d51a 100644
--- a/odoo/addons/test_pylint/tests/test_pylint.py
+++ b/odoo/addons/test_pylint/tests/test_pylint.py
@@ -22,6 +22,7 @@ _logger = logging.getLogger(__name__)
class TestPyLint(TransactionCase):
ENABLED_CODES = [
+ 'E0601', # using variable before assignment
'W0123', # eval used
'W0101', # unreachable code
]
diff --git a/odoo/fields.py b/odoo/fields.py
index ee732b5b62f..c542945a781 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -894,10 +894,12 @@ class Field(object):
env.invalidate(spec)
else:
- # simply write to the database, and update cache
+ # Write to database
write_value = self.convert_to_write(self.convert_to_record(value, record), record)
record.write({self.name: write_value})
- record._cache[self] = value
+ # Update the cache unless value contains a new record
+ if not (self.relational and not all(value)):
+ record._cache[self] = value
############################################################################
#
@@ -1987,7 +1989,7 @@ class _RelationalMulti(_Relational):
def convert_to_write(self, value, record):
# make result with new and existing records
- result = [(5,)]
+ result = [(6, 0, [])]
for record in value:
if not record.id:
values = {name: record[name] for name in record._cache}
@@ -1998,7 +2000,7 @@ class _RelationalMulti(_Relational):
values = record._convert_to_write(values)
result.append((1, record.id, values))
else:
- result.append((4, record.id))
+ result[0][2].append(record.id)
return result
def convert_to_onchange(self, value, record, fnames=()):
@@ -2171,7 +2173,11 @@ class One2many(_RelationalMulti):
query = "SELECT id FROM %s WHERE %s=%%s AND id <> ALL(%%s)" % (comodel._table, inverse)
comodel._cr.execute(query, (record.id, act[2] or [0]))
lines = comodel.browse([row[0] for row in comodel._cr.fetchall()])
- lines.write({inverse: False})
+ inverse_field = comodel._fields[inverse]
+ if inverse_field.ondelete == 'cascade':
+ lines.unlink()
+ else:
+ lines.write({inverse: False})
class Many2many(_RelationalMulti):
diff --git a/odoo/models.py b/odoo/models.py
index 9270782eadb..8f15a59d7cd 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -738,7 +738,7 @@ class BaseModel(object):
return '__export__.' + name
@api.multi
- def __export_rows(self, fields):
+ def _export_rows(self, fields):
""" Export fields of the records in ``self``.
:param fields: list of lists of fields to traverse
@@ -785,7 +785,7 @@ class BaseModel(object):
# recursively export the fields that follow name
fields2 = [(p[1:] if p and p[0] == name else []) for p in fields]
- lines2 = value.__export_rows(fields2)
+ lines2 = value._export_rows(fields2)
if lines2:
# merge first line with record's main line
for j, val in enumerate(lines2[0]):
@@ -804,6 +804,9 @@ class BaseModel(object):
return lines
+ # backward compatibility
+ __export_rows = _export_rows
+
@api.multi
def export_data(self, fields_to_export, raw_data=False):
""" Export fields for selected objects
@@ -817,7 +820,7 @@ class BaseModel(object):
fields_to_export = map(fix_import_export_id_paths, fields_to_export)
if raw_data:
self = self.with_context(export_raw_data=True)
- return {'datas': self.__export_rows(fields_to_export)}
+ return {'datas': self._export_rows(fields_to_export)}
@api.model
def load(self, fields, data):
@@ -3505,8 +3508,7 @@ class BaseModel(object):
``(6, _, ids)``
replaces all existing records in the set by the ``ids`` list,
equivalent to using the command ``5`` followed by a command
- ``4`` for each ``id`` in ``ids``. Can not be used on
- :class:`~odoo.fields.One2many`.
+ ``4`` for each ``id`` in ``ids``.
.. note:: Values marked as ``_`` in the list above are ignored and
can be anything, generally ``0`` or ``False``.
diff --git a/odoo/tools/config.py b/odoo/tools/config.py
index 5584002c520..ea030a80f10 100644
--- a/odoo/tools/config.py
+++ b/odoo/tools/config.py
@@ -363,7 +363,7 @@ class configmanager(object):
rcfilepath = old_rcfilepath
self.rcfile = os.path.abspath(
- self.config_file or opt.config or os.environ.get('OPENERP_SERVER') or rcfilepath)
+ self.config_file or opt.config or os.environ.get('ODOO_RC') or os.environ.get('OPENERP_SERVER') or rcfilepath)
self.load()
# Verify that we want to log or not, if not the output will go to stdout