[MERGE] forward port branch saas-16 up to 88395bdb8e
This commit is contained in:
@@ -904,6 +904,14 @@ class AccountTax(models.Model):
|
||||
return incl_tax.compute_all(price)['total_excluded']
|
||||
return price
|
||||
|
||||
@api.model
|
||||
def _fix_tax_included_price_company(self, price, prod_taxes, line_taxes, company_id):
|
||||
if company_id:
|
||||
#To keep the same behavior as in _compute_tax_id
|
||||
prod_taxes = prod_taxes.filtered(lambda tax: tax.company_id == company_id)
|
||||
line_taxes = line_taxes.filtered(lambda tax: tax.company_id == company_id)
|
||||
return self._fix_tax_included_price(price, prod_taxes, line_taxes)
|
||||
|
||||
class AccountReconcileModel(models.Model):
|
||||
_name = "account.reconcile.model"
|
||||
_description = "Preset to create journal entries during a invoices and payments matching"
|
||||
|
||||
@@ -600,6 +600,7 @@ class AccountInvoice(models.Model):
|
||||
|
||||
self.account_id = account_id
|
||||
self.payment_term_id = payment_term_id
|
||||
self.date_due = False
|
||||
self.fiscal_position_id = fiscal_position
|
||||
|
||||
if type in ('in_invoice', 'out_refund'):
|
||||
@@ -1228,7 +1229,7 @@ class AccountInvoice(models.Model):
|
||||
if self.origin:
|
||||
communication = '%s (%s)' % (communication, self.origin)
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
payment_vals = {
|
||||
'invoice_ids': [(6, 0, self.ids)],
|
||||
'amount': pay_amount or self.residual,
|
||||
'payment_date': date or fields.Date.context_today(self),
|
||||
@@ -1240,7 +1241,11 @@ class AccountInvoice(models.Model):
|
||||
'payment_method_id': payment_method.id,
|
||||
'payment_difference_handling': writeoff_acc and 'reconcile' or 'open',
|
||||
'writeoff_account_id': writeoff_acc and writeoff_acc.id or False,
|
||||
})
|
||||
}
|
||||
if self.env.context.get('tx_currency_id'):
|
||||
payment_vals['currency_id'] = self.env.context.get('tx_currency_id')
|
||||
|
||||
payment = self.env['account.payment'].create(payment_vals)
|
||||
payment.post()
|
||||
|
||||
return True
|
||||
|
||||
@@ -235,6 +235,7 @@ var StatementModel = BasicModel.extend({
|
||||
return $.when(partner && this._changePartner(handle, partner.id))
|
||||
.then(function() {
|
||||
line.reconciliation_proposition = [];
|
||||
self._computeLine(line);
|
||||
return self.changeMode(handle, 'match');
|
||||
})
|
||||
.then(function () {
|
||||
|
||||
@@ -607,6 +607,10 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
* @param {keyup event} event
|
||||
*/
|
||||
_onInputKeyup: function (event) {
|
||||
var target_partner_id = $(event.target).parents('[name="partner_id"]');
|
||||
if (target_partner_id.length === 1) {
|
||||
return;
|
||||
}
|
||||
if(event.keyCode === 13) {
|
||||
if (_.findWhere(this.model.lines, {mode: 'create'}).balance.amount) {
|
||||
this._onCreateProposition();
|
||||
|
||||
@@ -741,7 +741,7 @@ QUnit.module('account', {
|
||||
});
|
||||
|
||||
QUnit.test('Reconciliation change partner', function (assert) {
|
||||
assert.expect(13);
|
||||
assert.expect(17);
|
||||
|
||||
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
|
||||
|
||||
@@ -759,6 +759,20 @@ QUnit.module('account', {
|
||||
var widget = clientAction.widgets[0];
|
||||
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait");
|
||||
assert.strictEqual(widget.$('.match table tr').length, 2, "agrolait should have 2 propositions for reconciliation");
|
||||
|
||||
// Adding the two propositions
|
||||
// This is in order to try that after changing partner the propositions are emptied
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
widget.$('.match .cell_account_code:first').trigger('click');
|
||||
assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "Both proposition should be selected");
|
||||
|
||||
// Similate changing partner to one that does not have propositions to see if create mode is open after
|
||||
widget.$('.o_input_dropdown input').trigger('click');
|
||||
$('.ui-autocomplete .ui-menu-item a:contains(partner 1)').trigger('mouseenter').trigger('click');
|
||||
clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'partner 1', id: 1}}, stopped: false});
|
||||
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "partner 1", "the partner many2one should display partner 1");
|
||||
assert.strictEqual(widget.$('.match table tr.mv_line').length, 0, "partner 1 should have 0 propositions for reconciliation");
|
||||
assert.strictEqual(widget.$el.data('mode'), 'create', "widget should be in create mode");
|
||||
|
||||
// Simulate changing partner
|
||||
widget.$('.o_input_dropdown input').trigger('click');
|
||||
|
||||
@@ -664,7 +664,7 @@
|
||||
<field name="view_mode">tree,kanban,form,calendar,pivot,graph</field>
|
||||
<field eval="False" name="view_id"/>
|
||||
<field name="domain">[('type','=','out_refund')]</field>
|
||||
<field name="context">{'default_type': 'out_refund', 'journal_type': 'sale'}</field>
|
||||
<field name="context">{'default_type': 'out_refund', 'type': 'out_refund', 'journal_type': 'sale'}</field>
|
||||
<field name="search_view_id" ref="view_account_invoice_filter"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
@@ -738,7 +738,7 @@
|
||||
<field name="view_mode">tree,kanban,form,calendar,pivot,graph</field>
|
||||
<field eval="False" name="view_id"/>
|
||||
<field name="domain">[('type','=','in_refund')]</field>
|
||||
<field name="context">{'default_type': 'in_refund', 'journal_type': 'purchase'}</field>
|
||||
<field name="context">{'default_type': 'in_refund', 'type': 'in_refund', 'journal_type': 'purchase'}</field>
|
||||
<field name="search_view_id" ref="view_account_invoice_filter"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<td><span t-esc="aml.move_id.name <> '/' and aml.move_id.name or ('*'+str(aml.move_id.id))"/></td>
|
||||
<td><span t-field="aml.date"/></td>
|
||||
<td><span t-field="aml.account_id.code"/></td>
|
||||
<td><span t-esc="aml.sudo().partner_id and aml.sudo().partner_id.name[:23] or ''"/></td>
|
||||
<td><span t-esc="aml.sudo().partner_id and aml.sudo().partner_id.name and aml.sudo().partner_id.name[:23] or ''"/></td>
|
||||
<td><span t-esc="aml.name[:35]"/></td>
|
||||
<td><span t-esc="aml.debit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
|
||||
<td><span t-esc="aml.credit" t-options="{'widget': 'monetary', 'display_currency': res_company.currency_id}"/></td>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -212,6 +212,7 @@
|
||||
<field name="product_id"
|
||||
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}"
|
||||
attrs="{'readonly': ['|', ('qty_invoiced', '>', 0), ('procurement_ids', '!=', [])]}"
|
||||
force_save="1"
|
||||
/>
|
||||
<field name="layout_category_id" groups="sale.group_sale_layout"/>
|
||||
<field name="invoice_status" invisible="1"/>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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: '<form>' +
|
||||
'<div class="oe_title">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: '<tree editable="bottom"><field name="foo"/><field name="bar"/></tree>',
|
||||
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: '<tree editable="bottom"><field name="foo"/></tree>',
|
||||
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");
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -283,6 +283,11 @@
|
||||
<field name="state_id"/>
|
||||
<field name="image_small"/>
|
||||
<field name="lang"/>
|
||||
<!-- fields in form x2many view to diminish requests -->
|
||||
<field name="comment"/>
|
||||
<field name="customer"/>
|
||||
<field name="display_name"/>
|
||||
<field name="supplier"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<t t-set="color" t-value="kanban_color(record.color.raw_value)"/>
|
||||
|
||||
Reference in New Issue
Block a user