@@ -738,7 +738,7 @@
tree,kanban,form,calendar,pivot,graph
[('type','=','in_refund')]
- {'default_type': 'in_refund', 'journal_type': 'purchase'}
+ {'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}
diff --git a/addons/account/views/report_journal.xml b/addons/account/views/report_journal.xml
index ac94a8f2c50..f4f320e617f 100644
--- a/addons/account/views/report_journal.xml
+++ b/addons/account/views/report_journal.xml
@@ -49,7 +49,7 @@
|
|
|
- |
+ |
|
|
|
diff --git a/addons/bus/models/bus.py b/addons/bus/models/bus.py
index 8795a6047c8..0af875cecbb 100644
--- a/addons/bus/models/bus.py
+++ b/addons/bus/models/bus.py
@@ -108,6 +108,7 @@ class ImBus(models.Model):
class ImDispatch(object):
def __init__(self):
self.channels = {}
+ self.started = False
def poll(self, dbname, channels, last, options=None, timeout=TIMEOUT):
if options is None:
@@ -127,8 +128,17 @@ class ImDispatch(object):
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
notifications = env['bus.bus'].poll(channels, last, options)
+
+ # immediatly returns in peek mode
+ if options.get('peek'):
+ return dict(notifications=notifications, channels=channels)
+
# or wait for future ones
if not notifications:
+ if not self.started:
+ # Lazy start of events listener
+ self.start()
+
event = self.Event()
for channel in channels:
self.channels.setdefault(hashable(channel), []).append(event)
@@ -178,15 +188,16 @@ class ImDispatch(object):
import gevent
self.Event = gevent.event.Event
gevent.spawn(self.run)
- elif odoo.multi_process:
- # disabled in prefork mode
- return
else:
# threaded mode
self.Event = threading.Event
t = threading.Thread(name="%s.Bus" % __name__, target=self.run)
t.daemon = True
t.start()
+ self.started = True
return self
-dispatch = ImDispatch().start()
+dispatch = None
+if not odoo.multi_process or odoo.evented:
+ # We only use the event dispatcher in threaded and gevent mode
+ dispatch = ImDispatch()
diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py
index d2e9f28e7b3..01691a27b70 100644
--- a/addons/mrp/models/stock_warehouse.py
+++ b/addons/mrp/models/stock_warehouse.py
@@ -58,7 +58,7 @@ class StockWarehouse(models.Model):
wh_stock_loc = warehouse.lot_stock_id
seq = seq_obj.search([('code', '=', 'mrp.production')], limit=1)
other_pick_type = picking_type_obj.search([('warehouse_id', '=', warehouse.id)], order = 'sequence desc', limit=1)
- color = other_pick_type and other_pick_type.color or 1
+ color = other_pick_type.color if other_pick_type else 1
max_sequence = other_pick_type and other_pick_type.sequence or 0
manu_type = picking_type_obj.create({
'name': _('Manufacturing'),
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 7660476c6c8..64811953332 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -916,7 +916,7 @@ class PosOrderLine(models.Model):
self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id)
fpos = self.order_id.fiscal_position_id
tax_ids_after_fiscal_position = fpos.map_tax(self.tax_ids, self.product_id, self.order_id.partner_id) if fpos else self.tax_ids
- self.price_unit = self.env['account.tax']._fix_tax_included_price(price, self.product_id.taxes_id, tax_ids_after_fiscal_position)
+ self.price_unit = self.env['account.tax']._fix_tax_included_price_company(price, self.product_id.taxes_id, tax_ids_after_fiscal_position, self.company_id)
@api.onchange('qty', 'discount', 'price_unit', 'tax_ids')
def _onchange_qty(self):
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 1d0aec5901b..5ff7c57ed56 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -856,7 +856,7 @@ class PurchaseOrderLine(models.Model):
if not seller:
return
- price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, self.taxes_id) if seller else 0.0
+ price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0
if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id)
@@ -920,7 +920,7 @@ class ProcurementOrder(models.Model):
date=procurement.purchase_line_id.order_id.date_order and procurement.purchase_line_id.order_id.date_order[:10],
uom_id=procurement.purchase_line_id.product_uom)
- price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, procurement.purchase_line_id.product_id.supplier_taxes_id, procurement.purchase_line_id.taxes_id) if seller else 0.0
+ price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, procurement.purchase_line_id.product_id.supplier_taxes_id, procurement.purchase_line_id.taxes_id, self.company_id) if seller else 0.0
if price_unit and seller and procurement.purchase_line_id.order_id.currency_id and seller.currency_id != procurement.purchase_line_id.order_id.currency_id:
price_unit = seller.currency_id.compute(price_unit, procurement.purchase_line_id.order_id.currency_id)
@@ -985,7 +985,7 @@ class ProcurementOrder(models.Model):
if taxes_id:
taxes_id = taxes_id.filtered(lambda x: x.company_id.id == self.company_id.id)
- price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, taxes_id) if seller else 0.0
+ price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, taxes_id, self.company_id) if seller else 0.0
if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
price_unit = seller.currency_id.compute(price_unit, po.currency_id)
@@ -1110,7 +1110,7 @@ class ProcurementOrder(models.Model):
date=po.date_order and po.date_order[:10],
uom_id=procurement.product_id.uom_po_id)
- price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, line.product_id.supplier_taxes_id, line.taxes_id) if seller else 0.0
+ price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, line.product_id.supplier_taxes_id, line.taxes_id, self.company_id) if seller else 0.0
if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
price_unit = seller.currency_id.compute(price_unit, po.currency_id)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 5978b34ae61..e816827dff1 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -950,7 +950,7 @@ class SaleOrderLine(models.Model):
self._compute_tax_id()
if self.order_id.pricelist_id and self.order_id.partner_id:
- vals['price_unit'] = self.env['account.tax']._fix_tax_included_price(self._get_display_price(product), product.taxes_id, self.tax_id)
+ vals['price_unit'] = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id)
self.update(vals)
return result
@@ -970,7 +970,7 @@ class SaleOrderLine(models.Model):
uom=self.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position')
)
- self.price_unit = self.env['account.tax']._fix_tax_included_price(self._get_display_price(product), product.taxes_id, self.tax_id)
+ self.price_unit = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id)
@api.multi
def unlink(self):
@@ -1046,7 +1046,7 @@ class SaleOrderLine(models.Model):
price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id)
- new_list_price = self.env['account.tax']._fix_tax_included_price(new_list_price, self.product_id.taxes_id, self.tax_id)
+ new_list_price = self.env['account.tax']._fix_tax_included_price_company(new_list_price, self.product_id.taxes_id, self.tax_id, self.company_id)
if new_list_price != 0:
if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id:
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index ca44e7b312a..e60474de26c 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -212,6 +212,7 @@
diff --git a/addons/sale_payment/models/payment.py b/addons/sale_payment/models/payment.py
index a6b6ef3b209..3b0ca5cbef9 100644
--- a/addons/sale_payment/models/payment.py
+++ b/addons/sale_payment/models/payment.py
@@ -84,7 +84,7 @@ class PaymentTransaction(models.Model):
_logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)',
self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
self.acquirer_id.journal_id = default_journal
- created_invoice.pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=created_invoice.amount_total)
+ created_invoice.with_context(tx_currency_id=self.currency_id.id).pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=created_invoice.amount_total)
if created_invoice.payment_ids:
created_invoice.payment_ids[0].payment_transaction_id = self
else:
diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py
index 91a9061fe58..49e30f2fe91 100644
--- a/addons/stock/models/procurement.py
+++ b/addons/stock/models/procurement.py
@@ -289,6 +289,13 @@ class ProcurementOrder(models.Model):
1000 orderpoints.
This is appropriate for batch jobs only.
"""
+ if company_id and self.env.user.company_id.id != company_id:
+ # To ensure that the company_id is taken into account for
+ # all the processes triggered by this method
+ # i.e. If a PO is generated by the run of the procurements the
+ # sequence to use is the one for the specified company not the
+ # one of the user's company
+ self = self.with_context(company_id=company_id, force_company=company_id)
OrderPoint = self.env['stock.warehouse.orderpoint']
domain = self._get_orderpoint_domain(company_id=company_id)
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index d8bd64253c6..28ca24bb35f 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -188,8 +188,8 @@ class Warehouse(models.Model):
# choose the next available color for the operation types of this warehouse
all_used_colors = [res['color'] for res in PickingType.search_read([('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')]
- available_colors = [zef for zef in [0, 3, 4, 5, 6, 7, 8, 1, 2] if zef not in all_used_colors]
- color = available_colors and available_colors[0] or 1
+ available_colors = [zef for zef in [1, 3, 4, 5, 6, 7, 8, 0, 2] if zef not in all_used_colors]
+ color = available_colors[0] if available_colors else 1
# suit for each warehouse: reception, internal, pick, pack, ship
max_sequence = PickingType.search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc')
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index dd50929537d..b13839ca4a2 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -292,12 +292,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}).fail(view.loaded.reject.bind(view.loaded));
} else {
view.loaded = view.loaded.then(function() {
- // By default, the view will be loaded in readonly mode
- // Returns to this default mode if you load action from breadcrumb
view_options = _.extend({}, view_options, self.env);
- if (view_type === 'form') {
- view_options.mode = view_options.mode || 'readonly';
- }
return view.controller.reload(view_options);
});
}
@@ -382,8 +377,14 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
});
},
select_view: function (index) {
- var view_type = this.view_stack[index].type;
- return this.switch_mode(view_type);
+ var viewType = this.view_stack[index].type;
+ var viewOptions = {};
+ if (viewType === 'form') {
+ // reload form views in readonly, except for inline actions (i.e.
+ // settings views) that stay in edit
+ viewOptions.mode = this.action.target === 'inline' ? 'edit' : 'readonly';
+ }
+ return this.switch_mode(viewType, viewOptions);
},
/**
* Renders the switch buttons for multi- and mono-record views and adds
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index cf5a18488a6..103c0d4c5d2 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -768,7 +768,7 @@ var BasicModel = AbstractModel.extend({
removeLine: function (elementID) {
var record = this.localData[elementID];
var parent = this.localData[record.parentID];
- if (parent.parentID) {
+ if (parent.static) {
// x2Many case: the new record has been stored in _changes, as a
// command so we remove the command(s) related to that record
parent._changes = _.filter(parent._changes, function (change) {
@@ -777,6 +777,7 @@ var BasicModel = AbstractModel.extend({
} else {
// main list view case: the new record is in data
parent.data = _.without(parent.data, elementID);
+ parent.count--;
}
},
/**
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index e040a5fa7f0..09b264be0b5 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -273,18 +273,18 @@ var BasicRenderer = AbstractRenderer.extend({
* from internal referencing.
*
* @private
- * @param {Object} record
+ * @param {string} recordID id of the local resource
* @param {AbstractField} widget
* @returns {integer} the index of the removed widget
*/
- _destroyFieldWidget: function (record, widget) {
- var recordWidgets = this.allFieldWidgets[record.id];
+ _destroyFieldWidget: function (recordID, widget) {
+ var recordWidgets = this.allFieldWidgets[recordID];
var index = recordWidgets.indexOf(widget);
if (index >= 0) {
recordWidgets.splice(index, 1);
}
- this._unregisterModifiersElement(widget.__node, record, widget);
- widget.$el.destroy();
+ this._unregisterModifiersElement(widget.__node, recordID, widget);
+ widget.destroy();
return index;
},
/**
@@ -541,7 +541,7 @@ var BasicRenderer = AbstractRenderer.extend({
widget.$el.replaceWith(newWidget.$el);
// Destroy the old widget and position the new one at the old one's
- var oldIndex = this._destroyFieldWidget(record, widget);
+ var oldIndex = this._destroyFieldWidget(record.id, widget);
var recordWidgets = this.allFieldWidgets[record.id];
recordWidgets.splice(oldIndex, 0, newWidget);
recordWidgets.pop();
@@ -553,13 +553,13 @@ var BasicRenderer = AbstractRenderer.extend({
* node and record.
*
* @param {Object} node
- * @param {Object} record
+ * @param {string} recordID id of the local resource
* @param {jQuery|AbstractField} element
*/
- _unregisterModifiersElement: function (node, record, element) {
+ _unregisterModifiersElement: function (node, recordID, element) {
var modifiersData = this._getModifiersData(node);
if (modifiersData) {
- var elements = modifiersData.elementsByRecord[record.id];
+ var elements = modifiersData.elementsByRecord[recordID];
var index = _.findIndex(elements, function (oldElement) {
return oldElement.widget === element
|| oldElement.$el[0] === element[0];
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 6b9949897ca..5c18d4246e8 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -12,6 +12,7 @@ var FormRenderer = BasicRenderer.extend({
className: "o_form_view",
events: _.extend({}, BasicRenderer.prototype.events, {
'click .o_notification_box .oe_field_translate': '_onTranslate',
+ 'click .oe_title, .o_group': '_onClick',
}),
/**
* @override
@@ -835,6 +836,16 @@ var FormRenderer = BasicRenderer.extend({
// Handlers
//--------------------------------------------------------------------------
+ /**
+ * Makes the Edit button bounce in readonly
+ *
+ * @private
+ */
+ _onClick: function () {
+ if (this.mode === 'readonly') {
+ this.trigger_up('bounce_edit');
+ }
+ },
/**
* @override
* @private
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 7222d792c69..460ad5de100 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -182,6 +182,7 @@ var ListController = BasicController.extend({
if ((recordID || this.handle) !== this.handle) {
var state = this.model.get(this.handle);
this.renderer.removeLine(state, recordID);
+ this._updatePager();
}
},
/**
@@ -202,6 +203,7 @@ var ListController = BasicController.extend({
var state = self.model.get(self.handle);
self.renderer.updateState(state, {});
self.renderer.editRecord(recordID);
+ self._updatePager();
}).always(this._enableButtons.bind(this));
},
/**
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 7984ebf21e1..6519fdfaec4 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -133,6 +133,7 @@ ListRenderer.include({
* @param {string} recordID
*/
removeLine: function (state, recordID) {
+ var self = this;
var rowIndex = _.findIndex(this.state.data, {id: recordID});
this.state = state;
if (rowIndex === -1) {
@@ -141,8 +142,20 @@ ListRenderer.include({
if (rowIndex === this.currentRow) {
this.currentRow = null;
}
+
+ // remove the row
var $row = this.$('.o_data_row:nth(' + rowIndex + ')');
- $row.remove();
+ if (this.state.count >= 4) {
+ $row.remove();
+ } else {
+ $row.replaceWith(this._renderEmptyRow());
+ }
+
+ // destroy the removed row's widgets
+ var widgetsToDestroy = this.allFieldWidgets[recordID].slice();
+ _.each(widgetsToDestroy, function (widget) {
+ self._destroyFieldWidget(recordID, widget);
+ });
},
/**
* Updates the already rendered row associated to the given recordID so that
@@ -221,7 +234,7 @@ ListRenderer.include({
// destroyed. This is not the case for simple buttons so we have to
// do it here.
if ($td.hasClass('o_list_button')) {
- self._unregisterModifiersElement(node, record, $td.children());
+ self._unregisterModifiersElement(node, recordID, $td.children());
}
// For edit mode we only replace the content of the cell with its
@@ -231,14 +244,14 @@ ListRenderer.include({
if (editMode) {
$td.empty().append($newTd.contents());
} else {
- self._unregisterModifiersElement(node, record, $td);
+ self._unregisterModifiersElement(node, recordID, $td);
$td.replaceWith($newTd);
}
});
delete this.defs;
// Destroy old field widgets
- _.each(oldWidgets, this._destroyFieldWidget.bind(this, record));
+ _.each(oldWidgets, this._destroyFieldWidget.bind(this, recordID));
// Toggle selected class here so that style is applied at the end
$row.toggleClass('o_selected_row', editMode);
@@ -666,6 +679,17 @@ ListRenderer.include({
this._super.apply(this, arguments);
}
},
+ /**
+ * Overrides to prevent from sorting if we are currently editing a record.
+ *
+ * @override
+ * @private
+ */
+ _onSortColumn: function () {
+ if (this.currentRow === null) {
+ this._super.apply(this, arguments);
+ }
+ },
/**
* Triggers a delete event. I don't know why we stop the propagation of the
* event.
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 76281d9366a..b95a1c4f936 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -74,9 +74,16 @@ var DateWidget = Widget.extend({
* set datetime value
*/
changeDatetime: function () {
- if(this.isValid()) {
+ if (this.isValid()) {
+ var oldValue = this.getValue();
this._setValueFromUi();
- this.trigger("datetime_changed");
+ var newValue = this.getValue();
+
+ if (!oldValue !== !newValue || oldValue && newValue && !oldValue.isSame(newValue)) {
+ // The condition is strangely written; this is because the
+ // values can be false/undefined
+ this.trigger("datetime_changed");
+ }
}
},
/**
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index de3274c23ba..5855f7e8238 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -5913,5 +5913,36 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('bounce edit button in readonly mode', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ intercepts: {
+ bounce_edit: function() {
+ assert.step('bounce');
+ },
+ },
+ });
+
+ // in readonly
+ form.$('[name="display_name"]').click();
+ assert.verifySteps(['bounce']);
+
+ // in edit
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('[name="display_name"]').click();
+ assert.verifySteps(['bounce']);
+
+ form.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 6f25cff4721..a7368c75325 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2,6 +2,7 @@ odoo.define('web.list_tests', function (require) {
"use strict";
var config = require('web.config');
+var basicFields = require('web.basic_fields');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var testUtils = require('web.test_utils');
@@ -388,6 +389,54 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('editable list: add a line and discard', function (assert) {
+ assert.expect(11);
+
+ var oldDestroy = basicFields.FieldChar.prototype.destroy;
+ basicFields.FieldChar.prototype.destroy = function () {
+ assert.step('destroy');
+ oldDestroy.apply(this, arguments);
+ };
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ domain: [['foo', '=', 'yop']],
+ });
+
+ assert.strictEqual(list.$('tbody tr').length, 4,
+ "list should contain 4 rows");
+ assert.strictEqual(list.$('.o_data_row').length, 1,
+ "list should contain one record (and thus 3 empty rows)");
+ assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-1',
+ "pager should be correct");
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ assert.strictEqual(list.$('tbody tr').length, 4,
+ "list should still contain 4 rows");
+ assert.strictEqual(list.$('.o_data_row').length, 2,
+ "list should contain two record (and thus 2 empty rows)");
+ assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-2',
+ "pager should be correct");
+
+ list.$buttons.find('.o_list_button_discard').click();
+
+ assert.strictEqual(list.$('tbody tr').length, 4,
+ "list should still contain 4 rows");
+ assert.strictEqual(list.$('.o_data_row').length, 1,
+ "list should contain one record (and thus 3 empty rows)");
+ assert.strictEqual(list.pager.$('.o_pager_value').text(), '1-1',
+ "pager should be correct");
+ assert.verifySteps(['destroy'],
+ "should have destroyed the widget of the removed line");
+
+ basicFields.FieldChar.prototype.destroy = oldDestroy;
+ list.destroy();
+ });
+
QUnit.test('field changes are triggered correctly', function (assert) {
assert.expect(2);
@@ -436,6 +485,39 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('editable list view: save data when list sorting in edit mode', function (assert) {
+ assert.expect(3);
+
+ this.data.foo.fields.foo.sortable = true;
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args, [[1], {foo: 'xyz'}],
+ "should correctly save the edited record");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ list.$('.o_data_cell:first').click();
+ list.$('input[name="foo"]').val('xyz').trigger('input');
+ list.$('.o_column_sortable').click();
+
+ assert.ok(list.$('.o_data_row:first').hasClass('o_selected_row'),
+ "first row should still be in edition");
+
+ list.$buttons.find('.o_list_button_save').click();
+ assert.ok(!list.$buttons.hasClass('o-editing'),
+ "list buttons should be back to their readonly mode");
+
+ list.destroy();
+ });
+
QUnit.test('selection changes are triggered correctly', function (assert) {
assert.expect(8);
@@ -861,6 +943,7 @@ QUnit.module('Views', {
assert.ok(form.$('tbody tr:eq(2) td input').val(),
"Value 2 should be third (shouldn't be sorted)");
+ form.$('.o_form_sheet_bg').click(); // validate the row before sorting
$o2m.find('.o_column_sortable').click();
assert.ok(form.$('tbody tr:first td:contains(Value 3)').length,
"Value 3 should be first");
diff --git a/addons/web/static/tests/widgets/domain_selector_tests.js b/addons/web/static/tests/widgets/domain_selector_tests.js
index 19731156a2c..77d8eeb24da 100644
--- a/addons/web/static/tests/widgets/domain_selector_tests.js
+++ b/addons/web/static/tests/widgets/domain_selector_tests.js
@@ -151,7 +151,7 @@ QUnit.module('DomainSelector', {
});
QUnit.test("building a domain with a datetime", function (assert) {
- assert.expect(1);
+ assert.expect(2);
var $target = $("#qunit-fixture");
@@ -163,8 +163,16 @@ QUnit.module('DomainSelector', {
domainSelector.appendTo($target);
// Check that there is a datepicker to choose the date
- assert.strictEqual(domainSelector.$(".o_datepicker:visible").length, 1,
+ var $datepicker = domainSelector.$(".o_datepicker:visible");
+ assert.strictEqual($datepicker.length, 1,
"there should be a datepicker");
+
+ var val = $datepicker.find('input').focus().click().val();
+ $('.bootstrap-datetimepicker-widget :not(.today)[data-action="selectDay"]').click();
+ assert.notEqual(domainSelector.$(".o_datepicker:visible input").val(), val,
+ "datepicker value should have changed");
+
+ domainSelector.destroy();
});
});
});
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 67068bff115..497c0da20bf 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -68,7 +68,7 @@ class SaleOrder(models.Model):
if order.pricelist_id and order.partner_id:
order_line = order._cart_find_product_line(product.id)
if order_line:
- pu = self.env['account.tax']._fix_tax_included_price(pu, product.taxes_id, order_line[0].tax_id)
+ pu = self.env['account.tax']._fix_tax_included_price_company(pu, product.taxes_id, order_line[0].tax_id, self.company_id)
return {
'product_id': product_id,
@@ -156,10 +156,11 @@ class SaleOrder(models.Model):
'pricelist': order.pricelist_id.id,
})
product = self.env['product.product'].with_context(product_context).browse(product_id)
- values['price_unit'] = self.env['account.tax']._fix_tax_included_price(
+ values['price_unit'] = self.env['account.tax']._fix_tax_included_price_company(
order_line._get_display_price(product),
order_line.product_id.taxes_id,
- order_line.tax_id
+ order_line.tax_id,
+ self.company_id
)
order_line.write(values)
diff --git a/doc/reference/testing.rst b/doc/reference/testing.rst
index 7b5bb666988..ab32a9d3e0a 100644
--- a/doc/reference/testing.rst
+++ b/doc/reference/testing.rst
@@ -48,6 +48,11 @@ related to testing Odoo content (modules, mainly):
.. autoclass:: odoo.tests.common.SingleTransactionCase
:members: browse_ref, ref
+.. autoclass:: odoo.tests.common.SavepointCase
+
+.. autoclass:: odoo.tests.common.HttpCase
+ :members: browse_ref, ref, url_open, phantom_js
+
By default, tests are run once right after the corresponding module has been
installed. Test cases can also be configured to run after all modules have
been installed, and not run right after the module installation:
diff --git a/odoo/addons/base/res/res_partner_view.xml b/odoo/addons/base/res/res_partner_view.xml
index b191cf845ec..09c06a59880 100644
--- a/odoo/addons/base/res/res_partner_view.xml
+++ b/odoo/addons/base/res/res_partner_view.xml
@@ -283,6 +283,11 @@
+
+
+
+
+