diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index de6394b2ea2..189e7a3f832 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -342,7 +342,7 @@ class AccountBankStatement(models.Model):
class AccountBankStatementLine(models.Model):
_name = "account.bank.statement.line"
_description = "Bank Statement Line"
- _order = "statement_id desc, sequence"
+ _order = "statement_id desc, sequence, id desc"
_inherit = ['ir.needaction_mixin']
name = fields.Char(string='Label', required=True)
@@ -951,8 +951,23 @@ class AccountBankStatementLine(models.Model):
aml_dict['currency_id'] = statement_currency.id
# Create write-offs
+ # When we register a payment on an invoice, the write-off line contains the amount
+ # currency if all related invoices have the same currency. We apply the same logic in
+ # the manual reconciliation.
+ counterpart_aml = self.env['account.move.line']
+ for aml_dict in counterpart_aml_dicts:
+ counterpart_aml |= aml_dict.get('move_line', self.env['account.move.line'])
+ new_aml_currency = False
+ if counterpart_aml\
+ and len(counterpart_aml.mapped('currency_id')) == 1\
+ and counterpart_aml[0].currency_id\
+ and counterpart_aml[0].currency_id != company_currency:
+ new_aml_currency = counterpart_aml[0].currency_id
for aml_dict in new_aml_dicts:
aml_dict['payment_id'] = payment and payment.id or False
+ if new_aml_currency and not aml_dict.get('currency_id'):
+ aml_dict['currency_id'] = new_aml_currency.id
+ aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], new_aml_currency)
aml_obj.with_context(check_move_validity=False, apply_taxes=True).create(aml_dict)
# Create counterpart move lines and reconcile them
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 01d3b8199f9..f27f5e4d42c 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -906,6 +906,8 @@ class AccountMoveLine(models.Model):
first_line_dict['account_id'] = self[0].account_id.id
if 'analytic_account_id' in first_line_dict:
del first_line_dict['analytic_account_id']
+ if 'tax_ids' in first_line_dict:
+ del first_line_dict['tax_ids']
# Writeoff line in specified writeoff account
second_line_dict = vals.copy()
@@ -1208,7 +1210,7 @@ class AccountMoveLine(models.Model):
""" Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having
an analytic account. This method is intended to be extended in other modules.
"""
- amount = (self.credit or 0.0) - (self.debit or 0.0)
+ amount = (self.debit or 0.0) - (self.credit or 0.0)
return {
'name': self.name,
'date': self.date,
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index d957f7c9f5a..bd40fa8816a 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -243,7 +243,7 @@ class TestReconciliation(AccountingTestCase):
self.check_results(bank_stmt.move_line_ids, {
self.account_euro.id: {'debit': 40.0, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
self.account_rcv.id: {'debit': 0.0, 'credit': 32.7, 'amount_currency': -41.97, 'currency_id': self.currency_usd_id, 'currency_diff': 0, 'amount_currency_diff': -8.03},
- self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': 0, 'currency_id': False},
+ self.diff_income_account.id: {'debit': 0.0, 'credit': 7.3, 'amount_currency': -9.37, 'currency_id': self.currency_usd_id},
})
# The invoice should be paid, as the payments totally cover its total
diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py
index 942c54dfdf1..144f0cc518e 100644
--- a/addons/account_asset/models/account_asset.py
+++ b/addons/account_asset/models/account_asset.py
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import calendar
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF
-from odoo.tools import float_compare
+from odoo.tools import float_compare, float_is_zero
class AccountAssetCategory(models.Model):
@@ -156,14 +157,26 @@ class AccountAssetAsset(models.Model):
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
- amount = (amount_to_depr / self.method_number) / total_days * days
+ if self.method_period % 12 != 0:
+ date = datetime.strptime(self.date, '%Y-%m-%d')
+ month_days = calendar.monthrange(date.year, date.month)[1]
+ days = month_days - date.day + 1
+ amount = (amount_to_depr / self.method_number) / month_days * days
+ else:
+ days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
+ amount = (amount_to_depr / self.method_number) / total_days * days
elif self.method == 'degressive':
amount = residual_amount * self.method_progress_factor
if self.prorata:
if sequence == 1:
- days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
- amount = (residual_amount * self.method_progress_factor) / total_days * days
+ if self.method_period % 12 != 0:
+ date = datetime.strptime(self.date, '%Y-%m-%d')
+ month_days = calendar.monthrange(date.year, date.month)[1]
+ days = month_days - date.day + 1
+ amount = (residual_amount * self.method_progress_factor) / month_days * days
+ else:
+ days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
+ amount = (residual_amount * self.method_progress_factor) / total_days * days
return amount
def _compute_board_undone_dotation_nb(self, depreciation_date, total_days):
@@ -216,6 +229,8 @@ class AccountAssetAsset(models.Model):
sequence = x + 1
amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date)
amount = self.currency_id.round(amount)
+ if float_is_zero(amount, precision_rounding=self.currency_id.rounding):
+ continue
residual_amount -= amount
vals = {
'amount': amount,
@@ -413,7 +428,7 @@ class AccountAssetAsset(models.Model):
'res_model': 'account.move',
'view_id': False,
'type': 'ir.actions.act_window',
- 'domain': [('id', 'in', move_ids)]
+ 'domain': [('id', 'in', move_ids)],
}
diff --git a/addons/account_asset/tests/test_account_asset.py b/addons/account_asset/tests/test_account_asset.py
index 8ef0bf56ef9..bbc36ac4dbb 100644
--- a/addons/account_asset/tests/test_account_asset.py
+++ b/addons/account_asset/tests/test_account_asset.py
@@ -55,7 +55,7 @@ class TestAccountAsset(common.TransactionCase):
asset_modify_number_0.with_context({'active_id': account_asset_asset_office0.id}).modify()
# I check the proper depreciation lines created.
- self.assertEqual(account_asset_asset_office0.method_number, len(account_asset_asset_office0.depreciation_line_ids) - 1)
+ self.assertEqual(account_asset_asset_office0.method_number, len(account_asset_asset_office0.depreciation_line_ids))
# I compute a asset on period.
context = {
"active_ids": [self.ref("account_asset.menu_asset_depreciation_confirmation_wizard")],
diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js
index 3575f36f655..7d881187fe0 100644
--- a/addons/board/static/src/js/dashboard.js
+++ b/addons/board/static/src/js/dashboard.js
@@ -348,6 +348,9 @@ core.form_tag_registry
FavoriteMenu.include({
start: function () {
var self = this;
+ if(this.action_id === undefined) {
+ return this._super();
+ }
var am = this.findAncestor(function (a) {
return a instanceof ActionManager;
});
diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js
index 2ffdab84420..a79c7500669 100644
--- a/addons/bus/static/src/js/bus.js
+++ b/addons/bus/static/src/js/bus.js
@@ -19,6 +19,7 @@ bus.Bus = Widget.extend({
this._super();
this.options = {};
this.activated = false;
+ this.bus_id = _.uniqueId('bus');
this.channels = [];
this.last = 0;
this.stop = false;
@@ -33,15 +34,24 @@ bus.Bus = Widget.extend({
this.trigger('window_focus', this.is_master);
}
});
- $(window).on("focus", _.bind(this.focus_change, this, true));
- $(window).on("blur", _.bind(this.focus_change, this, false));
- $(window).on("unload", _.bind(this.focus_change, this, false));
+ $(window).on("focus." + this.bus_id, _.bind(this.focus_change, this, true));
+ $(window).on("blur." + this.bus_id, _.bind(this.focus_change, this, false));
+ $(window).on("unload." + this.bus_id, _.bind(this.focus_change, this, false));
_.each('click,keydown,keyup'.split(','), function(evtype) {
- $(window).on(evtype, function() {
+ $(window).on(evtype + "." + self.bus_id, function() {
self.last_presence = new Date().getTime();
});
});
},
+ destroy: function () {
+ var self = this;
+ $(window).off("focus." + this.bus_id);
+ $(window).off("blur." + this.bus_id);
+ $(window).off("unload." + this.bus_id);
+ _.each('click,keydown,keyup'.split(','), function(evtype) {
+ $(window).off(evtype + "." + self.bus_id);
+ });
+ },
start_polling: function(){
if(!this.activated){
this.poll();
diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py
index f5a315ca399..4ce72cefef6 100644
--- a/addons/delivery/models/stock_picking.py
+++ b/addons/delivery/models/stock_picking.py
@@ -78,7 +78,7 @@ class StockPicking(models.Model):
def _compute_shipping_weight(self):
self.shipping_weight = self.weight_bulk + sum([pack.shipping_weight for pack in self.package_ids])
- carrier_price = fields.Float(string="Shipping Cost", readonly=True)
+ carrier_price = fields.Float(string="Shipping Cost")
delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True)
carrier_id = fields.Many2one("delivery.carrier", string="Carrier")
volume = fields.Float(copy=False)
@@ -90,6 +90,11 @@ class StockPicking(models.Model):
weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight')
shipping_weight = fields.Float("Weight for Shipping", compute='_compute_shipping_weight')
+ @api.onchange('carrier_id')
+ def onchange_carrier(self):
+ if self.carrier_id.delivery_type in ['fixed', 'base_on_rule']:
+ self.carrier_price = self.carrier_id.price
+
@api.depends('product_id', 'move_lines')
def _cal_weight(self):
for picking in self:
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index ecdda31d27d..88fe6423761 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -418,6 +418,13 @@ class HrExpenseSheet(models.Model):
self._add_followers()
return res
+ @api.multi
+ def unlink(self):
+ for expense in self:
+ if expense.state == "post":
+ raise UserError(_("You cannot delete a posted expense."))
+ super(HrExpenseSheet, self).unlink()
+
@api.multi
def set_to_paid(self):
self.write({'state': 'done'})
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
index adf2d5714f6..fe9ad84ba39 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -662,6 +662,10 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
target: 'current'
});
},
+ destroy: function() {
+ this.$buttons.off().destroy();
+ this._super.apply(this, arguments);
+ },
});
diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js
index 10c99ae6eb0..dd30dddf8f3 100644
--- a/addons/mail/static/src/js/composer.js
+++ b/addons/mail/static/src/js/composer.js
@@ -461,6 +461,11 @@ var BasicComposer = Widget.extend({
return this._super();
},
+ destroy: function () {
+ $(window).off(this.fileupload_id);
+ return this._super.apply(this, arguments);
+ },
+
toggle: function(state) {
this.$el.toggle(state);
},
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 0bf67bbd1da..4731d30afb1 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -215,7 +215,7 @@ class PosOrder(models.Model):
if move is None:
# Create an entry for the sale
journal_id = self.env['ir.config_parameter'].sudo().get_param(
- 'pos.closing.journal_id', default=order.sale_journal.id)
+ 'pos.closing.journal_id_%s' % current_company.id, default=order.sale_journal.id)
move = self._create_account_move(
order.session_id.start_at, order.name, int(journal_id), order.company_id.id)
@@ -609,7 +609,7 @@ class PosOrder(models.Model):
if moves and not return_picking and not order_picking:
moves.action_confirm()
moves.force_assign()
- moves.action_done()
+ moves.filtered(lambda m: m.product_id.tracking == 'none').action_done()
return True
@@ -619,7 +619,8 @@ class PosOrder(models.Model):
picking.action_confirm()
picking.force_assign()
self.set_pack_operation_lot(picking)
- picking.action_done()
+ if not any([(x.product_id.tracking != 'none') for x in picking.pack_operation_ids]):
+ picking.action_done()
def set_pack_operation_lot(self, picking=None):
"""Set Serial/Lot number in pack operations to mark the pack operation done."""
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 3605c70824a..5c6afff5dd8 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -19,7 +19,7 @@ class PosSession(models.Model):
company_id = session.config_id.journal_id.company_id.id
orders = session.order_ids.filtered(lambda order: order.state == 'paid')
journal_id = self.env['ir.config_parameter'].sudo().get_param(
- 'pos.closing.journal_id', default=session.config_id.journal_id.id)
+ 'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id)
move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, int(journal_id), company_id)
orders.with_context(force_company=company_id)._create_account_move_line(session, move)
for order in session.order_ids.filtered(lambda o: o.state != 'done'):
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 164fd5f7040..ce9272f2c21 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1847,11 +1847,9 @@ var PaymentScreenWidget = ScreenWidget.extend({
self.$('.next').removeClass('highlight');
}
},
- // Check if the order is paid, then sends it to the backend,
- // and complete the sale process
- validate_order: function(force_validation) {
- var self = this;
+ order_is_valid: function(force_validation) {
+ var self = this;
var order = this.pos.get_order();
// FIXME: this check is there because the backend is unable to
@@ -1861,7 +1859,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
'title': _t('Empty Order'),
'body': _t('There must be at least one product in your order before it can be validated'),
});
- return;
+ return false;
}
var plines = order.get_paymentlines();
@@ -1871,12 +1869,12 @@ var PaymentScreenWidget = ScreenWidget.extend({
'message': _t('Negative Bank Payment'),
'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'),
});
- return;
+ return false;
}
}
if (!order.is_paid() || this.invoicing) {
- return;
+ return false;
}
// The exact amount must be paid if there is no cash payment method defined.
@@ -1890,7 +1888,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
title: _t('Cannot return change without a cash payment method'),
body: _t('There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration'),
});
- return;
+ return false;
}
}
@@ -1911,9 +1909,16 @@ var PaymentScreenWidget = ScreenWidget.extend({
self.validate_order('confirm');
},
});
- return;
+ return false;
}
+ return true;
+ },
+
+ finalize_validation: function() {
+ var self = this;
+ var order = this.pos.get_order();
+
if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) {
this.pos.proxy.open_cashbox();
@@ -1963,6 +1968,14 @@ var PaymentScreenWidget = ScreenWidget.extend({
}
},
+
+ // Check if the order is paid, then sends it to the backend,
+ // and complete the sale process
+ validate_order: function(force_validation) {
+ if (this.order_is_valid(force_validation)) {
+ this.finalize_validation();
+ }
+ },
});
gui.define_screen({name:'payment', widget: PaymentScreenWidget});
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index b2ae897866b..1351a8978d8 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -47,7 +47,7 @@
"+value.split(/
/).join("
")+"
"; - value = value.replace(/<\/p>/g, '').replace('
', '
').replace('
', ''); + try { + $(text)[0].innerHTML; + return text; + } catch (e) { + if (value.match(/^\s*$/)) { + value = '"+value.split(/
/).join("
")+"
"; + value = value.replace(/<\/p>/g, '').replace('
', '
').replace('
', ''); + } } return value; }, @@ -216,7 +221,7 @@ var FieldTextHtml = widget.extend({ self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px"); } }; - $(window).on('resize', self.resize); + $(window).on('resize', this.resize); var def = this._super.apply(this, arguments); this.$translate.remove(); @@ -377,7 +382,7 @@ var FieldTextHtml = widget.extend({ } }, destroy: function () { - $(window).off('resize', self.resize); + $(window).off('resize', this.resize); delete window.odoo[this.callback+"_editor"]; delete window.odoo[this.callback+"_content"]; delete window.odoo[this.callback+"_updown"]; diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js index dcfabb6feb4..4ddda0c838e 100644 --- a/addons/web_editor/static/src/js/snippets.editor.js +++ b/addons/web_editor/static/src/js/snippets.editor.js @@ -1032,12 +1032,12 @@ data.Editor = Class.extend({ if($parent.closest(":data(\"snippet-editor\")").length) { while (!$parent.data("snippet-editor")) { var $nextParent = $parent.parent(); - if ($parent.children().length === 0 && $parent.text().trim() === "") { + if ($parent.children().length === 0 && $parent.text().trim() === "" && !$parent.hasClass("oe_structure")) { $parent.remove(); } $parent = $nextParent; } - if ($parent.children().length === 0 && $parent.text().trim() === "") { + if ($parent.children().length === 0 && $parent.text().trim() === "" && !$parent.hasClass("oe_structure")) { _.defer(function () { $parent.data("snippet-editor").on_remove(); }); diff --git a/addons/web_kanban/static/src/js/kanban_record.js b/addons/web_kanban/static/src/js/kanban_record.js index 473ebc59e98..c784073f2a5 100644 --- a/addons/web_kanban/static/src/js/kanban_record.js +++ b/addons/web_kanban/static/src/js/kanban_record.js @@ -64,6 +64,7 @@ var KanbanRecord = Widget.extend({ qweb_context[p] = _.bind(this[p], this); } } + this.qweb_context = qweb_context; this.content = this.qweb.render('kanban-box', qweb_context); }, @@ -81,7 +82,18 @@ var KanbanRecord = Widget.extend({ }, start: function() { + var self = this; this.add_widgets(); + this.$('[tooltip]').each(function () { + var $el = $(this); + var tooltip = $el.attr('tooltip'); + if (tooltip) { + $el.tooltip({ + 'html': true, + 'title': self.qweb.render(tooltip, self.qweb_context) + }); + } + }); }, renderElement: function () { @@ -172,14 +184,15 @@ var KanbanRecord = Widget.extend({ if (elem == ev.currentTarget) { ischild = false; } + var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== "tooltip"); if (ischild) { children.push(elem); - if (events && events.click) { + if (test_event) { // do not trigger global click if one child has a click event registered trigger = false; } } - if (trigger && events && events.click) { + if (trigger && test_event) { _.each(events.click, function(click_event) { if (click_event.selector) { // For each parent of original target, check if a diff --git a/addons/website/static/src/js/website.snippets.gallery.js b/addons/website/static/src/js/website.snippets.gallery.js index cde906ca764..8cb8bf0ceaa 100644 --- a/addons/website/static/src/js/website.snippets.gallery.js +++ b/addons/website/static/src/js/website.snippets.gallery.js @@ -360,7 +360,16 @@ options.registry.gallery_img = options.Class.extend({ this.buildingBlock.make_active($parent); $parent.data('snippet-editor').styles.gallery.reapply(); }).bind(this)); - } + }, + on_focus: function () { + this._super.apply(this, arguments); + if (this._current_src && this._current_src !== this.$target.attr("src")) { + _.defer((function () { + snippet_editor.globalSelector.closest(this.$target.parent()).data('snippet-editor').styles.gallery.reapply(); + }).bind(this)); + } + this._current_src = this.$target.attr("src"); + }, }); diff --git a/odoo/fields.py b/odoo/fields.py index e5cd453ed1d..11320fb5877 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1243,6 +1243,13 @@ class Monetary(Field): return float_precision(value, currency.decimal_places) return float(value or 0.0) + def convert_to_read(self, value, record, use_name_get=True): + # float_precision values are not supported in pure XMLRPC + return float(value) + + def convert_to_write(self, value, record): + return value + class _String(Field): """ Abstract class for string fields. """ diff --git a/odoo/http.py b/odoo/http.py index 86d8028f0da..f39354510df 100644 --- a/odoo/http.py +++ b/odoo/http.py @@ -784,7 +784,7 @@ class HttpRequest(WebRequest): if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'): headers = { 'Access-Control-Max-Age': 60 * 60 * 24, - 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept' + 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-Debug-Mode' } return Response(status=200, headers=headers)