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 @@ - - - + + @@ -85,7 +85,7 @@ + context="{'default_product_id': parent.product_id}"/>