diff --git a/addons/account/models/account.py b/addons/account/models/account.py index b3c981b8241..09923efec07 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -707,7 +707,12 @@ class AccountTax(models.Model): if not round_tax: prec += 5 - total_excluded = total_included = base = round(price_unit * quantity, prec) + + base_values = self.env.context.get('base_values') + if not base_values: + total_excluded = total_included = base = round(price_unit * quantity, prec) + else: + total_excluded, total_included, base = base_values # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a # search. However, the search method is overridden in account.tax in order to add a domain @@ -715,9 +720,10 @@ class AccountTax(models.Model): # case of group taxes. for tax in self.sorted(key=lambda r: r.sequence): if tax.amount_type == 'group': - ret = tax.children_tax_ids.compute_all(price_unit, currency, quantity, product, partner) + children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base)) + ret = children.compute_all(price_unit, currency, quantity, product, partner) total_excluded = ret['total_excluded'] - base = ret['base'] + base = ret['base'] if tax.include_base_amount else base total_included = ret['total_included'] tax_amount = total_included - total_excluded taxes += ret['taxes'] diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 0dddb10c03c..a1bdf5d12f4 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -477,7 +477,7 @@ class AccountBankStatementLine(models.Model): 'details': { 'name': _("Automatically reconciled items"), 'model': 'account.move', - 'ids': automatic_reconciliation_entries.ids + 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').ids } }] return { diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index e295bab8fa7..c2b7ca31eea 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -563,6 +563,14 @@ class AccountInvoice(models.Model): '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), } + + # 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. + # This is necessary in situations were (part of) the taxes cannot be reclaimed, + # to ensure the tax move is allocated to the proper analytic account. + if not vals.get('account_analytic_id') and line.account_analytic_id and vals['account_id'] == line.account_id.id: + vals['account_analytic_id'] = line.account_analytic_id.id + return vals @api.multi @@ -573,14 +581,6 @@ class AccountInvoice(models.Model): 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 = 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. - # This is necessary in situations were (part of) the taxes cannot be reclaimed, - # to ensure the tax move is allocated to the proper analytic account. - if not val.get('account_analytic_id') and line.account_analytic_id and val['account_id'] == line.account_id.id: - val['account_analytic_id'] = line.account_analytic_id.id - key = self.env['account.tax'].browse(tax['id']).get_grouping_key(val) if key not in tax_grouped: diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8af478244fc..215d40f3b8d 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -993,6 +993,9 @@ class AccountMoveLine(models.Model): return True rec_move_ids = self.env['account.partial.reconcile'] for account_move_line in self: + for invoice in account_move_line.payment_id.invoice_ids: + if account_move_line in invoice.payment_move_line_ids: + account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]}) rec_move_ids += account_move_line.matched_debit_ids rec_move_ids += account_move_line.matched_credit_ids return rec_move_ids.unlink() diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py index d221e705412..02308fa6498 100644 --- a/addons/account/tests/test_tax.py +++ b/addons/account/tests/test_tax.py @@ -43,10 +43,46 @@ class TestTax(AccountTestUsers): (4, self.percent_tax.id, 0) ] }) + self.group_tax_bis = self.tax_model.create({ + 'name': "Group tax bis", + 'amount_type': 'group', + 'amount': 0, + 'sequence': 6, + 'children_tax_ids': [ + (4, self.fixed_tax.id, 0), + (4, self.percent_tax.id, 0) + ] + }) + self.group_of_group_tax = self.tax_model.create({ + 'name': "Group of group tax", + 'amount_type': 'group', + 'amount': 0, + 'sequence': 7, + 'children_tax_ids': [ + (4, self.group_tax.id, 0), + (4, self.group_tax_bis.id, 0) + ] + }) self.bank_journal = self.env['account.journal'].search([('type', '=', 'bank'), ('company_id', '=', self.account_manager.company_id.id)])[0] self.bank_account = self.bank_journal.default_debit_account_id self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later + def test_tax_group_of_group_tax(self): + self.fixed_tax.include_base_amount = True + self.group_tax.include_base_amount = True + self.group_of_group_tax.include_base_amount = True + res = self.group_of_group_tax.compute_all(200.0) + self.assertEquals(res['total_excluded'], 200.0) + # After calculation of first group + # base = 210 + # total_included = 231 + # Base of the first grouped is passed + # Base after the second group (220) is dropped. + # Base of the group of groups is passed out, + # so we obtain base as after first group + self.assertEquals(res['base'], 210.0) + self.assertEquals(res['total_included'], 263.0) + def test_tax_group(self): res = self.group_tax.compute_all(200.0) self.assertEquals(res['total_excluded'], 200.0) diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index 6606ffe5569..a743cb6562f 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -139,7 +139,11 @@ var DashBoard = form_common.FormWidget.extend({ }, on_close_action: function(e) { if (confirm(_t("Are you sure you want to remove this item ?"))) { - $(e.currentTarget).parents('.oe_action:first').remove(); + var $container = $(e.currentTarget).parents('.oe_action:first'); + var am = _.findWhere(this.action_managers, { am_id: $container.data('am_id') }); + am.destroy(); + this.action_managers.splice(_.indexOf(this.action_managers, am), 1); + $container.remove(); this.do_save_dashboard(); } }, @@ -223,7 +227,13 @@ var DashBoard = form_common.FormWidget.extend({ var am = new ActionManager(this), // FIXME: ideally the dashboard view shall be refactored like kanban. $action = $('#' + this.view.element_id + '_action_' + index); - $action.parent().data('action_attrs', action_attrs); + var $action_container = $action.closest('.oe_action'); + var am_id = _.uniqueId('action_manager_'); + am.am_id = am_id; + $action_container.data({ + action_attrs: action_attrs, + am_id: am_id, + }); this.action_managers.push(am); am.appendTo($action).then(function () { am.do_action(action).then(function () { diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 646951df163..848b07a272b 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -9,6 +9,7 @@ var utils = require('mail.utils'); var config = require('web.config'); var core = require('web.core'); var form_common = require('web.form_common'); +var framework = require('web.framework'); var web_utils = require('web.utils'); var _t = core._t; @@ -441,7 +442,14 @@ var Chatter = form_common.AbstractField.extend({ }, load_more_messages: function () { - this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}); + var self = this; + var top_msg_id = this.$('.o_thread_message').first().data('messageId'); + var top_msg_selector = '.o_thread_message[data-message-id="' + top_msg_id + '"]'; + var offset = -framework.getPosition(document.querySelector(top_msg_selector)).top; + this.fetch_and_render_thread(this.msg_ids, {force_fetch: true}).then(function(){ + offset += framework.getPosition(document.querySelector(top_msg_selector)).top; + self.thread.scroll_to({offset: offset}); + }); }, /** diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 28973248676..f0aa92f44cf 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -213,8 +213,10 @@ class PosOrder(models.Model): partner_id = ResPartner._find_accounting_partner(order.partner_id).id or False 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) move = self._create_account_move( - order.session_id.start_at, order.name, order.sale_journal.id, order.company_id.id) + order.session_id.start_at, order.name, int(journal_id), order.company_id.id) def insert_data(data_type, values): # if have_to_group_by: diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 3b69b071157..086b7980a1d 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -18,7 +18,9 @@ class PosSession(models.Model): for session in self: company_id = session.config_id.journal_id.company_id.id orders = session.order_ids.filtered(lambda order: order.state == 'paid') - move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, session.config_id.journal_id.id, company_id) + journal_id = self.env['ir.config_parameter'].sudo().get_param( + 'pos.closing.journal_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'): if order.state not in ('paid', 'invoiced'): diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js index 577a1ec42f0..517cae64c31 100644 --- a/addons/web/static/src/js/framework/data.js +++ b/addons/web/static/src/js/framework/data.js @@ -793,18 +793,19 @@ var BufferedDataSet = DataSetStatic.extend({ var def = $.Deferred(); this.mutex.exec(function () { var dirty = false; - _.each(data, function (v, k) { - if (!_.isEqual(v, cached.values[k])) { + // _.each is broken if a field "length" is present + for (var k in data) { + if (!_.isEqual(data[k], cached.values[k])) { dirty = true; - if (_.isEqual(v, cached.from_read[k])) { // clean changes + if (_.isEqual(data[k], cached.from_read[k])) { // clean changes delete cached.changes[k]; } else { - cached.changes[k] = v; + cached.changes[k] = data[k]; } } else { delete data[k]; } - }); + } self._update_cache(id, options); if (dirty) { diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 1a916843e6d..df6274ef12d 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -470,11 +470,12 @@ var ListView = View.extend({ self.records.remove(record); return; } - _.each(values, function (value, key) { + // _.each is broken if a field "length" is present + for (var key in values) { if (fields[key] && fields[key].type === 'many2many') record.set(key + '__display', false, {silent: true}); - record.set(key, value, {silent: true}); - }); + record.set(key, values[key], {silent: true}); + } record.trigger('change', record); /* When a record is reloaded, there is a rendering lag because of the addition/suppression of diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py index 811f3aac4d0..841c14fd40e 100644 --- a/addons/web/tests/test_js.py +++ b/addons/web/tests/test_js.py @@ -1,5 +1,8 @@ +import unittest import openerp.tests class WebSuite(openerp.tests.HttpCase): + + @unittest.skip('Memory leak in this test lead to phantomjs crash, making it unreliable') def test_01_js(self): self.phantom_js('/web/tests?mod=web',"","", login='admin') diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js index 73ea30b91fd..12a745590d2 100644 --- a/addons/website/static/src/js/website.ace.js +++ b/addons/website/static/src/js/website.ace.js @@ -141,7 +141,7 @@ var ViewEditor = Widget.extend({ var args = { key: $(document.documentElement).data('view-xmlid'), full: true, - bundles: !!$('script[src*=".assets_common"]').length + bundles: this.$('.js_include_bundles')[0].checked }; return ajax .jsonRpc('/website/customize_template_get', 'call', args) diff --git a/debian/control b/debian/control index e37938dc77e..e97db13df42 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Depends: node-less, postgresql-client, python, + python-babel, python-dateutil, python-decorator, python-docutils, @@ -32,7 +33,6 @@ Depends: python-passlib, python-psutil, python-psycopg2, - python-pybabel, python-pychart, python-pydot, python-pyparsing, diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index aa67546e346..89590dda2cd 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -114,6 +114,10 @@ class IrModel(models.Model): if model.state != 'manual': raise UserError(_("Model '%s' contains module data and cannot be removed!") % model.name) + # prevent screwing up fields that depend on these models' fields + for model in self: + model.field_id._prepare_update() + self._drop_table() res = super(IrModel, self).unlink() @@ -395,6 +399,19 @@ class IrModelFields(models.Model): return True + @api.multi + def _prepare_update(self): + """ Check whether the fields in ``self`` may be modified or removed. + This method prevents the modification/deletion of many2one fields + that have an inverse one2many, for instance. + """ + for record in self: + model = self.env[record.model] + field = model._fields[record.name] + if field.type == 'many2one' and model._field_inverses.get(field): + msg = _("The field '%s' cannot be removed because the field '%s' depends on it.") + raise UserError(msg % (field, model._field_inverses[field][0])) + @api.multi def unlink(self): # Prevent manual deletion of module columns @@ -402,6 +419,9 @@ class IrModelFields(models.Model): any(field.state != 'manual' for field in self): raise UserError(_("This column contains module data and cannot be removed!")) + # prevent screwing up fields that depend on these fields + self._prepare_update() + model_names = self.mapped('model') self._drop_column() res = super(IrModelFields, self).unlink() @@ -493,6 +513,7 @@ class IrModelFields(models.Model): if vals.get('name', item.name) != item.name: # We need to rename the column + item._prepare_update() if column_rename: raise UserError(_('Can only rename one field at a time!')) if vals['name'] in obj._fields: diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 01c26b2ec4e..716b96dbe80 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -87,7 +87,7 @@ class configmanager(object): self.config_file = fname self._LOGLEVELS = dict([ - (getattr(loglevels, 'LOG_%s' % x), getattr(logging, x)) + (getattr(loglevels, 'LOG_%s' % x), getattr(logging, x)) for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET') ]) @@ -111,7 +111,7 @@ class configmanager(object): group.add_option("--addons-path", dest="addons_path", help="specify additional addons paths (separated by commas).", action="callback", callback=self._check_addons_path, nargs=1, type="string") - group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules default=web") + group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules. Default is 'web,web_kanban'") group.add_option("-D", "--data-dir", dest="data_dir", my_default=_get_default_datadir(), help="Directory where to store Odoo data") @@ -395,6 +395,7 @@ class configmanager(object): 'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', 'data_dir', + 'server_wide_modules', ] posix_keys = [ @@ -448,14 +449,21 @@ class configmanager(object): if len(self.options['language']) > 5: raise Exception('ERROR: The Lang name must take max 5 chars, Eg: -lfr_BE') + # server_wide_modules defaults to web,web_kanban if empty or unset + server_wide_modules = self.options['server_wide_modules'] = ( + self.options['server_wide_modules'] + if self.options['server_wide_modules'] + else 'web,web_kanban' + ) + if opt.save: self.save() openerp.conf.addons_paths = self.options['addons_path'].split(',') - if opt.server_wide_modules: - openerp.conf.server_wide_modules = map(lambda m: m.strip(), opt.server_wide_modules.split(',')) - else: - openerp.conf.server_wide_modules = ['web','web_kanban'] + + openerp.conf.server_wide_modules = [ + m.strip() for m in server_wide_modules.split(',') + ] def _is_addons_path(self, path): for f in os.listdir(path): diff --git a/setup/package.dfdebian b/setup/package.dfdebian index 8ad98e7aba0..4c6e417dfa9 100644 --- a/setup/package.dfdebian +++ b/setup/package.dfdebian @@ -21,6 +21,7 @@ RUN apt-get update -qq && \ postgresql \ postgresql-client \ python \ + python-babel \ python-dateutil \ python-decorator \ python-docutils \ @@ -36,7 +37,6 @@ RUN apt-get update -qq && \ python-passlib \ python-psutil \ python-psycopg2 \ - python-pybabel \ python-pychart \ python-pydot \ python-pyparsing \