diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 7f4b044d5f3..62effa08a37 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -708,7 +708,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 @@ -716,9 +721,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 aab8d023e8f..aef88b59e87 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -564,6 +564,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 @@ -574,14 +582,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 1850797ca04..dc763859b23 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1002,6 +1002,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 692ec8e5f41..98640acafe7 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: @@ -546,7 +548,10 @@ class PosOrder(models.Model): for order in self: address = order.partner_id.address_get(['delivery']) or {} picking_type = order.picking_type_id - picking_id = False + return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id + order_picking = Picking + return_picking = Picking + moves = Move location_id = order.location_id.id if order.partner_id: destination_id = order.partner_id.property_stock_customer.id @@ -556,9 +561,10 @@ class PosOrder(models.Model): destination_id = customerloc.id else: destination_id = picking_type.default_location_dest_id.id + if picking_type: - pos_qty = all([x.qty >= 0 for x in order.lines]) - picking_id = Picking.create({ + message = _("This transfer has been created from the point of sale session: %s") % (order.id, order.name) + picking_vals = { 'origin': order.name, 'partner_id': address.get('delivery', False), 'date_done': order.date_order, @@ -566,44 +572,72 @@ class PosOrder(models.Model): 'company_id': order.company_id.id, 'move_type': 'direct', 'note': order.note or "", - 'location_id': location_id if pos_qty else destination_id, - 'location_dest_id': destination_id if pos_qty else location_id, - }) - message = _("This transfer has been created from the point of sale session: %s") % (order.id, order.name) - picking_id.message_post(body=message) - order.write({'picking_id': picking_id.id}) + 'location_id': location_id, + 'location_dest_id': destination_id, + } + pos_qty = any([x.qty >= 0 for x in order.lines]) + if pos_qty: + order_picking = Picking.create(picking_vals.copy()) + order_picking.message_post(body=message) + neg_qty = any([x.qty < 0 for x in order.lines]) + if neg_qty: + return_vals = picking_vals.copy() + return_vals.update({ + 'location_id': destination_id, + 'location_dest_id': return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id, + 'picking_type_id': return_pick_type.id + }) + return_picking = Picking.create(return_vals) + return_picking.message_post(body=message) for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']): - Move += Move.create({ + moves |= Move.create({ 'name': line.name, 'product_uom': line.product_id.uom_id.id, - 'picking_id': picking_id and picking_id.id or False, - 'picking_type_id': picking_type.id, + 'picking_id': order_picking.id if line.qty >= 0 else return_picking.id, + 'picking_type_id': picking_type.id if line.qty >= 0 else return_pick_type.id, 'product_id': line.product_id.id, 'product_uom_qty': abs(line.qty), 'state': 'draft', 'location_id': location_id if line.qty >= 0 else destination_id, - 'location_dest_id': destination_id if line.qty >= 0 else location_id, + 'location_dest_id': destination_id if line.qty >= 0 else return_pick_type != picking_type and return_pick_type.default_location_dest_id.id or location_id, }) - if picking_id: - picking_id.action_confirm() - picking_id.force_assign() - order.set_pack_operation_lot() - picking_id.action_done() - elif Move: - Move.action_confirm() - Move.force_assign() - Move.action_done() + + # prefer associating the regular order picking, not the return + order.write({'picking_id': order_picking.id or return_picking.id}) + + if return_picking: + order._force_picking_done(return_picking) + if order_picking: + order._force_picking_done(order_picking) + + # when the pos.config has no picking_type_id set only the moves will be created + if moves and not return_picking and not order_picking: + moves.action_confirm() + moves.force_assign() + moves.action_done() + return True - def set_pack_operation_lot(self): + def _force_picking_done(self, picking): + """Force picking in order to be set as done.""" + self.ensure_one() + picking.action_confirm() + picking.force_assign() + self.set_pack_operation_lot(picking) + picking.action_done() + + def set_pack_operation_lot(self, picking=None): """Set Serial/Lot number in pack operations to mark the pack operation done.""" StockProductionLot = self.env['stock.production.lot'] PosPackOperationLot = self.env['pos.pack.operation.lot'] + if not picking: + picking = order.picking_id + for order in self: - for pack_operation in order.picking_id.pack_operation_ids: + for pack_operation in picking.pack_operation_ids: qty = 0 qty_done = 0 pack_lots = [] @@ -872,7 +906,7 @@ class ReportSaleDetails(models.AbstractModel): 'taxes': taxes.values(), 'products': sorted([{ 'product_id': product.id, - 'product_name': product.name[:20], + 'product_name': product.name, 'code': product.default_code, 'quantity': qty, 'price_unit': price_unit, @@ -888,4 +922,4 @@ class ReportSaleDetails(models.AbstractModel): date_stop = self.env.context.get('date_stop', False) data = dict(data or {}, date_start=date_start, date_stop=date_stop) data.update(self.get_sale_details(date_start, date_stop, company)) - return self.env['report'].render('point_of_sale.report_saledetails', data) \ No newline at end of file + return self.env['report'].render('point_of_sale.report_saledetails', data) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index cb036939b43..17a0cd73c7b 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/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index c3ba7c8953f..6a0fe186040 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -861,7 +861,7 @@
- + diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index 023eff033a0..4f029df9439 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -119,9 +119,208 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I test that the generated journal entry is attached to the PoS order self.assertTrue(self.pos_order_pos0.account_move, "Journal entry has not been attached to Pos order.") + def test_order_to_picking(self): + """ + In order to test the Point of Sale in module, I will do three orders from the sale to the payment, + invoicing + picking, but will only check the picking consistency in the end. + + TODO: Check the negative picking after changing the picking relation to One2many (also for a mixed use case), + check the quantity, the locations and return picking logic + """ + # I click on create a new session button + self.pos_config.open_session_cb() + + # I create a PoS order with 2 units of PCSC234 at 450 EUR + # and 3 units of PCSC349 at 300 EUR. + self.pos_order_pos1 = self.PosOrder.create({ + 'company_id': self.company_id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0001", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': 2.0, + }), (0, 0, { + 'name': "OL/0002", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': 3.0, + })] + }) + + # I click on the "Make Payment" wizard to pay the PoS order with the total amount (2*450 + 3*300 = 1800) + context_make_payment = { + "active_ids": [self.pos_order_pos1.id], + "active_id": self.pos_order_pos1.id + } + self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 1800 + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos1.id} + self.pos_make_payment_2.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual( + self.pos_order_pos1.state, + 'paid', + 'Order should be in paid state.' + ) + + # I generate the journal entries + self.pos_order_pos1._create_account_move_line() + + # I test that the generated journal entry is attached to the PoS order + self.assertTrue( + self.pos_order_pos1.account_move, + "Journal entry has not been attached to Pos order." + ) + + # I test that the pickings are created as expected + # One picking attached and having all the positive move lines in the correct state + self.pos_order_pos1.create_picking() + self.assertEqual( + self.pos_order_pos1.picking_id.state, + 'done', + 'Picking should be in done state.' + ) + self.assertEqual( + self.pos_order_pos1.picking_id.move_lines.mapped('state'), + ['done', 'done'], + 'Move Lines should be in done state.' + ) + + self.pos_order_pos2 = self.PosOrder.create({ + 'company_id': self.company_id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0003", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': (-2.0), + }), (0, 0, { + 'name': "OL/0004", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': (-3.0), + })] + }) + + # I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + -3*300 = -1800) + context_make_payment = { + "active_ids": [self.pos_order_pos2.id], + "active_id": self.pos_order_pos2.id + } + self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': (-1800) + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos2.id} + self.pos_make_payment_3.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual( + self.pos_order_pos2.state, + 'paid', + 'Order should be in paid state.' + ) + + # I generate the journal entries + self.pos_order_pos2._create_account_move_line() + + # I test that the generated journal entry is attached to the PoS order + self.assertTrue( + self.pos_order_pos2.account_move, + "Journal entry has not been attached to PoS order." + ) + + # I test that the pickings are created as expected + # One picking attached and having all the positive move lines in the correct state + self.pos_order_pos2.create_picking() + self.assertEqual( + self.pos_order_pos2.picking_id.state, + 'done', + 'Picking should be in done state.' + ) + self.assertEqual( + self.pos_order_pos2.picking_id.move_lines.mapped('state'), + ['done', 'done'], + 'Move Lines should be in done state.' + ) + + self.pos_order_pos3 = self.PosOrder.create({ + 'company_id': self.company_id, + 'pricelist_id': self.partner1.property_product_pricelist.id, + 'partner_id': self.partner1.id, + 'lines': [(0, 0, { + 'name': "OL/0005", + 'product_id': self.product3.id, + 'price_unit': 450, + 'discount': 0.0, + 'qty': (-2.0), + }), (0, 0, { + 'name': "OL/0006", + 'product_id': self.product4.id, + 'price_unit': 300, + 'discount': 0.0, + 'qty': 3.0, + })] + }) + + # I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + 3*300 = 0) + context_make_payment = { + "active_ids": [self.pos_order_pos3.id], + "active_id": self.pos_order_pos3.id + } + self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({ + 'amount': 0 + }) + + # I click on the validate button to register the payment. + context_payment = {'active_id': self.pos_order_pos3.id} + self.pos_make_payment_4.with_context(context_payment).check() + + # I check that the order is marked as paid + self.assertEqual( + self.pos_order_pos3.state, + 'paid', + 'Order should be in paid state.' + ) + + # I generate the journal entries + self.pos_order_pos3._create_account_move_line() + + # I test that the generated journal entry is attached to the PoS order + self.assertTrue( + self.pos_order_pos3.account_move, + "Journal entry has not been attached to PoS order." + ) + + # I test that the pickings are created as expected + # One picking attached and having all the positive move lines in the correct state + self.pos_order_pos3.create_picking() + self.assertEqual( + self.pos_order_pos3.picking_id.state, + 'done', + 'Picking should be in done state.' + ) + self.assertEqual( + self.pos_order_pos3.picking_id.move_lines.mapped('state'), + ['done'], + 'Move Lines should be in done state.' + ) + def test_order_to_invoice(self): - #I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) + # I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl) self.pos_order_pos1 = self.PosOrder.create({ 'company_id': self.company_id, 'partner_id': self.partner1.id, diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js index a350dadb59e..c160ebaf539 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 aece38b9d71..5bf94100898 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/static/test/list-editable.js b/addons/web/static/test/list-editable.js index be2acdb1365..4b88d9f06e5 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -196,6 +196,13 @@ odoo.define_section('list.edition', ['web.data', 'web.ListView', 'web.data_manag mock.add('demo:onchange', function () { return {}; }); + mock.add('demo:fields_get', function () { + return { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }; + }); } test('newrecord', function (assert, data, ListView, data_manager) { @@ -263,6 +270,13 @@ odoo.define_section('list.edition.events', ['web.data', 'web.ListView', 'web.dat mock.add('demo:read', function () { return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }]; }); + mock.add('demo:fields_get', function () { + return { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }; + }); } test('edition events',function (assert, data, ListView, data_manager) { @@ -345,6 +359,13 @@ odoo.define_section('list.edition.onwrite', ['web.data', 'web.ListView', 'web.da mock.add('demo:default_get', function () { return {}; }); mock.add('demo:create', function () { return 1; }); mock.add('demo:on_write', function () { return [42]; }); + mock.add('demo:fields_get', function () { + return { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }; + }); var ds = new data.DataSetStatic(null, 'demo', null, []); var fields_view = data_manager._postprocess_fvg({ diff --git a/addons/web/static/test/list.js b/addons/web/static/test/list.js index 9d1d1b9ec7d..49adabba13d 100644 --- a/addons/web/static/test/list.js +++ b/addons/web/static/test/list.js @@ -2,7 +2,7 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data', 'web.data_manag test('record-deletion', function (assert, ListView, data, data_manager) { assert.expect(2); - + mock.add('demo:read', function (args, kwargs) { if (_.isEqual(args[0], [1, 2, 3])) { return [ @@ -18,7 +18,9 @@ odoo.define_section('list.buttons', ['web.ListView', 'web.data', 'web.data_manag throw new Error(JSON.stringify(_.toArray(arguments))); }); mock.add('/web/dataset/call_button', function () { return false; }); - + mock.add('demo:fields_get', function() { + return {a: {type: 'char', string: "A"}}; + }); var ds = new data.DataSetStatic(null, 'demo', null, [1, 2, 3]); var fields_view = data_manager._postprocess_fvg({ 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/web_diagram/static/src/js/diagram.js b/addons/web_diagram/static/src/js/diagram.js index 4192e30a185..56664176ebe 100644 --- a/addons/web_diagram/static/src/js/diagram.js +++ b/addons/web_diagram/static/src/js/diagram.js @@ -246,16 +246,13 @@ var DiagramView = View.extend({ self.dataset.read_index(_.keys(self.fields_view.fields)).then(self.on_diagram_loaded); }); - var form_fields = [self.parent_field]; - var form_controller = pop.view_form; - - form_controller.on("load_record", self, function(){ - _.each(form_fields, function(fld) { - if (!(fld in form_controller.fields)) { return; } - var field = form_controller.fields[fld]; + pop.opened().then(function(){ + var form_controller = pop.view_form; + var field = form_controller.fields[self.parent_field]; + if (field) { field.$input.prop('disabled', true); field.$dropdown.unbind(); - }); + } }); }, @@ -274,16 +271,13 @@ var DiagramView = View.extend({ } }).open(); - var form_controller = pop.view_form; - var form_fields = [this.parent_field]; - - form_controller.on("load_record", self, function(){ - _.each(form_fields, function(fld) { - if (!(fld in form_controller.fields)) { return; } - var field = form_controller.fields[fld]; + pop.opened().then(function(){ + var form_controller = pop.view_form; + var field = form_controller.fields[self.parent_field]; + if (field) { field.set_value(self.id); field.dirty = true; - }); + } }); }, @@ -326,12 +320,14 @@ var DiagramView = View.extend({ } }); - var form_controller = pop.view_form; - form_controller.on("load_record", self, function(){ - form_controller.fields[self.connectors.attrs.source].set_value(node_source_id); - form_controller.fields[self.connectors.attrs.source].dirty = true; - form_controller.fields[self.connectors.attrs.destination].set_value(node_dest_id); - form_controller.fields[self.connectors.attrs.destination].dirty = true; + pop.opened().then(function(){ + var form_controller = pop.view_form; + var source_field = form_controller.fields[self.connectors.attrs.source]; + var destination_field = form_controller.fields[self.connectors.attrs.destination]; + source_field.set_value(node_source_id); + source_field.dirty = true; + destination_field.set_value(node_dest_id); + destination_field.dirty = true; }); }, diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js index b86f3885e6a..3bb540fba0e 100644 --- a/addons/website/static/src/js/website.ace.js +++ b/addons/website/static/src/js/website.ace.js @@ -142,7 +142,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/addons/website/static/src/js/website.backendDashboard.js b/addons/website/static/src/js/website.backendDashboard.js index ed241fd2215..35b27340c5f 100644 --- a/addons/website/static/src/js/website.backendDashboard.js +++ b/addons/website/static/src/js/website.backendDashboard.js @@ -257,7 +257,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { // Check if the user is authenticated and has the right to make API calls if (!gapi.analytics.auth.getAuthResponse()) { this.display_unauthorized_message($analytics_components, 'not_connected'); - } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics ') === -1) { + } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) { this.display_unauthorized_message($analytics_components, 'no_right'); } else { this.make_analytics_calls($analytics_components); diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 70fc06ba5e9..5c3b1b5bb96 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -136,9 +136,9 @@ odoo.define('website.website', function (require) { var error = function (title, message, url) { return new Dialog(null, { - title: data.data ? data.data.arguments[0] : "", + title: title || "", $content: $(qweb.render('website.error_dialog', { - message: data.data ? data.data.arguments[1] : data.statusText, + message: message || "", backend_url: url, })), }).open(); 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 47fdfbc71f1..3510ae6171a 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 \