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 \