diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 7d1344f7555..104e3a1d335 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -1016,7 +1016,7 @@ class AccountTax(models.Model): if tax.include_base_amount: base = recompute_base(base, incl_fixed_amount, incl_percent_amount) incl_fixed_amount = incl_percent_amount = 0 - if tax.price_include: + if self._context.get('force_price_include', tax.price_include): if tax.amount_type == 'fixed': incl_fixed_amount += tax.amount elif tax.amount_type == 'percent': @@ -1038,7 +1038,10 @@ class AccountTax(models.Model): # took into account on the base amount except for 'division' tax: # (tax.amount_type == 'percent' && not tax.price_include) # == (tax.amount_type == 'division' && tax.price_include) - tax_amount = tax.with_context(force_price_include=False)._compute_amount(base, price_unit, quantity, product, partner) + # N.B: don't use the with_context if force_price_include already False in context + if 'force_price_include' not in self._context or self._context['force_price_include']: + tax = tax.with_context(force_price_include=False) + tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner) if not round_tax: tax_amount = round(tax_amount, prec) else: diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 964b000cfc0..a0b2a7441e6 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -812,6 +812,7 @@ class AccountBankStatementLine(models.Model): whose value is the same as described in process_reconciliation except that ids are used instead of recordsets. """ AccountMoveLine = self.env['account.move.line'] + ctx = dict(self._context, force_price_include=False) for st_line, datum in pycompat.izip(self, data): payment_aml_rec = AccountMoveLine.browse(datum.get('payment_aml_ids', [])) for aml_dict in datum.get('counterpart_aml_dicts', []): @@ -819,7 +820,7 @@ class AccountBankStatementLine(models.Model): del aml_dict['counterpart_aml_id'] if datum.get('partner_id') is not None: st_line.write({'partner_id': datum['partner_id']}) - st_line.process_reconciliation(datum.get('counterpart_aml_dicts', []), payment_aml_rec, datum.get('new_aml_dicts', [])) + st_line.with_context(ctx).process_reconciliation(datum.get('counterpart_aml_dicts', []), payment_aml_rec, datum.get('new_aml_dicts', [])) def fast_counterpart_creation(self): for st_line in self: diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index ffc572a2cab..f180652de78 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -50,8 +50,9 @@ class AccountInvoice(models.Model): @api.depends('invoice_line_ids.price_subtotal', 'tax_line_ids.amount', 'tax_line_ids.amount_rounding', 'currency_id', 'company_id', 'date_invoice', 'type') def _compute_amount(self): + round_curr = self.currency_id.round self.amount_untaxed = sum(line.price_subtotal for line in self.invoice_line_ids) - self.amount_tax = sum(line.amount_total for line in self.tax_line_ids) + self.amount_tax = sum(round_curr(line.amount_total) for line in self.tax_line_ids) self.amount_total = self.amount_untaxed + self.amount_tax amount_total_company_signed = self.amount_total amount_untaxed_signed = self.amount_untaxed diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 6a72ba478ea..c4ff295fb70 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1238,7 +1238,7 @@ class AccountMoveLine(models.Model): taxes = self.env['account.tax'].browse(tax_ids) currency = self.env['res.currency'].browse(vals.get('currency_id')) partner = self.env['res.partner'].browse(vals.get('partner_id')) - res = taxes.with_context(round=True).compute_all(amount, + res = taxes.with_context(dict(self._context, round=True)).compute_all(amount, currency, 1, vals.get('product_id'), partner) # Adjust line amount if any tax is price_include if abs(res['total_excluded']) < abs(amount): diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index fa7a70d106f..f6fc6efbf27 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -184,7 +184,8 @@ class AccountVoucher(models.Model): 'amount_currency': (sign * abs(self.amount) # amount < 0 for refunds if company_currency != current_currency else 0.0), 'date': self.account_date, - 'date_maturity': self.date_due + 'date_maturity': self.date_due, + 'payment_id': self._context.get('payment_id'), } return move_line @@ -223,6 +224,24 @@ class AccountVoucher(models.Model): for voucher in self: return voucher.currency_id.compute(amount, voucher.company_id.currency_id) + @api.multi + def voucher_pay_now_payment_create(self): + payment_methods = self.journal_id.outbound_payment_method_ids + return { + 'payment_type': 'outbound', + 'payment_method_id': payment_methods and payment_methods[0].id or False, + 'partner_type': 'supplier', + 'partner_id': self.partner_id.id, + 'amount': self.amount, + 'currency_id': self.currency_id.id, + 'payment_date': self.date, + 'journal_id': self.journal_id.id, + 'company_id': self.company_id.id, + 'communication': self.name, + 'name': self.name, + 'state': 'reconciled', + } + @api.multi def voucher_move_line_create(self, line_total, move_id, company_currency, current_currency): ''' @@ -260,8 +279,8 @@ class AccountVoucher(models.Model): 'tax_ids': [(4,t.id) for t in line.tax_ids], 'amount_currency': line.price_subtotal if current_currency != company_currency else 0.0, 'currency_id': company_currency != current_currency and current_currency or False, + 'payment_id': self._context.get('payment_id'), } - self.env['account.move.line'].with_context(apply_taxes=True).create(move_line) return line_total @@ -281,6 +300,9 @@ class AccountVoucher(models.Model): ctx = local_context.copy() ctx['date'] = voucher.account_date ctx['check_move_validity'] = False + # Create a payment to allow the reconciliation when pay_now = 'pay_now'. + if self.pay_now == 'pay_now' and self.amount > 0: + ctx['payment_id'] = self.env['account.payment'].create(self.voucher_pay_now_payment_create()).id # Create the account move record. move = self.env['account.move'].create(voucher.account_move_get()) # Get the name of the account_move just created diff --git a/addons/base_automation/models/base_automation.py b/addons/base_automation/models/base_automation.py index bef0b88914d..b5884515cb9 100644 --- a/addons/base_automation/models/base_automation.py +++ b/addons/base_automation/models/base_automation.py @@ -282,7 +282,15 @@ class BaseAutomation(models.Model): # retrieve all actions, and patch their corresponding model for action_rule in self.with_context({}).search([]): - Model = self.env[action_rule.model_name] + Model = self.env.get(action_rule.model_name) + + # Do not crash if the model of the base_action_rule was uninstalled + if Model is None: + _logger.warning("Action rule with ID %d depends on model %s" % + (action_rule.id, + action_rule.model_name)) + continue + if action_rule.trigger == 'on_create': patch(Model, 'create', make_create()) diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 790bc5f3203..a6aadc4e59e 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -85,7 +85,6 @@ class TestWorkOrderProcess(common.TransactionCase): # -------------------------------------------------------------- # Process assembly line # --------------------------------------------------------- - finished_lot =self.env['stock.production.lot'].create({'product_id': production_table.product_id.id}) workorder.write({'final_lot_id': finished_lot.id}) workorder.button_start() @@ -112,6 +111,119 @@ class TestWorkOrderProcess(common.TransactionCase): self.assertEqual(product_table_leg.qty_available, 19) self.assertEqual(product_table_sheet.qty_available, 19) + def test_00b_workorder_process(self): + """ Testing consume quants and produced quants with workorder """ + dining_table = self.env.ref("mrp.product_product_computer_desk") + product_table_sheet = self.env.ref('mrp.product_product_computer_desk_head') + product_table_leg = self.env.ref('mrp.product_product_computer_desk_leg') + product_bolt = self.env.ref('mrp.product_product_computer_desk_bolt') + + production_table = self.env['mrp.production'].create({ + 'product_id': dining_table.id, + 'product_qty': 2.0, + 'product_uom_id': dining_table.uom_id.id, + 'bom_id': self.ref("mrp.mrp_bom_desk") + }) + + # Set tracking lot on finish and consume products. + dining_table.tracking = 'lot' + product_table_sheet.tracking = 'lot' + product_table_leg.tracking = 'lot' + product_bolt.tracking = "lot" + + # Initial inventory of product sheet, lags and bolt + lot_sheet = self.env['stock.production.lot'].create({'product_id': product_table_sheet.id}) + lot_leg = self.env['stock.production.lot'].create({'product_id': product_table_leg.id}) + lot_bolt = self.env['stock.production.lot'].create({'product_id': product_bolt.id}) + + # Initialize inventory + # -------------------- + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': product_table_sheet.id, + 'product_uom_id': product_table_sheet.uom_id.id, + 'product_qty': 20, + 'prod_lot_id': lot_sheet.id, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': product_table_leg.id, + 'product_uom_id': product_table_leg.uom_id.id, + 'product_qty': 20, + 'prod_lot_id': lot_leg.id, + 'location_id': self.source_location_id + }), (0, 0, { + 'product_id': product_bolt.id, + 'product_uom_id': product_bolt.uom_id.id, + 'product_qty': 20, + 'prod_lot_id': lot_bolt.id, + 'location_id': self.source_location_id + })] + }) + inventory.action_done() + + # Create work order + production_table.button_plan() + # Check Work order created or not + self.assertEqual(len(production_table.workorder_ids), 3) + + # --------------------------------------------------------- + # Process all workorder and check it state. + # ---------------------------------------------------------- + + workorders = production_table.workorder_ids + self.assertEqual(workorders[0].state, 'ready', "First workorder state should be ready.") + self.assertEqual(workorders[1].state, 'pending') + self.assertEqual(workorders[2].state, 'pending') + + # -------------------------------------------------------------- + # Process cutting operation... + # --------------------------------------------------------- + finished_lot = self.env['stock.production.lot'].create({'product_id': production_table.product_id.id}) + workorders[0].write({'final_lot_id': finished_lot.id, 'qty_producing': 1.0}) + workorders[0].button_start() + workorders[0].active_move_lot_ids[0].write({'lot_id': lot_sheet.id, 'quantity_done': 1}) + self.assertEqual(workorders[0].state, 'progress') + workorders[0].record_production() + + move_table_sheet = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_sheet) + self.assertEqual(move_table_sheet.quantity_done, 1) + + # -------------------------------------------------------------- + # Process drilling operation ... + # --------------------------------------------------------- + workorders[1].button_start() + workorders[1].qty_producing = 1.0 + workorders[1].active_move_lot_ids[0].write({'lot_id': lot_leg.id, 'quantity_done': 4}) + workorders[1].record_production() + move_leg = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_leg) + #self.assertEqual(workorders[1].state, 'done') + self.assertEqual(move_leg.quantity_done, 4) + + # -------------------------------------------------------------- + # Process fitting operation ... + # --------------------------------------------------------- + workorders[2].button_start() + workorders[2].qty_producing = 1.0 + move_lot = workorders[2].active_move_lot_ids[0] + move_lot.write({'lot_id': lot_bolt.id, 'quantity_done': 4}) + move_table_bolt = production_table.move_raw_ids.filtered(lambda x : x.product_id.id == product_bolt.id) + workorders[2].record_production() + self.assertEqual(move_table_bolt.quantity_done, 4) + + # Change the quantity of the production order to 1 + wiz = self.env['change.production.qty'].create({'mo_id': production_table.id , + 'product_qty': 1.0}) + wiz.change_prod_qty() + # --------------------------------------------------------------- + # Check consume quants and produce quants after posting inventory + # --------------------------------------------------------------- + production_table.post_inventory() + self.assertEqual(sum(move_table_sheet.quant_ids.mapped('qty')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name) + self.assertEqual(sum(move_leg.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name) + self.assertEqual(sum(move_table_bolt.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name) + def test_01_without_workorder(self): """ Testing consume quants and produced quants without workorder """ unit = self.ref("product.product_uom_unit") diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index bf447feee61..2c6c1aebcdb 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError +from odoo.tools import float_is_zero import math class ChangeProductionQty(models.TransientModel): @@ -37,6 +38,7 @@ class ChangeProductionQty(models.TransientModel): @api.multi def change_prod_qty(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for wizard in self: production = wizard.mo_id produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done')) @@ -63,11 +65,14 @@ class ChangeProductionQty(models.TransientModel): wo.duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': - quantity = 1.0 + quantity = 1.0 if float_is_zero(quantity, precision_digits=precision) else 0.0 else: - quantity = wo.qty_production - wo.qty_produced quantity = quantity if (quantity > 0) else 0 + if float_is_zero(quantity, precision_digits=precision): + wo.final_lot_id = False + wo.active_move_lot_ids.unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' @@ -80,6 +85,6 @@ class ChangeProductionQty(models.TransientModel): moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) - if wo.move_raw_ids.filtered(lambda x: x.product_id.tracking != 'none') and not wo.active_move_line_ids: + if quantity > 0 and wo.move_raw_ids.filtered(lambda x: x.product_id.tracking != 'none') and not wo.active_move_line_ids: wo._generate_lot_ids() return {} diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index e0d3e5e60fb..bddecdfa270 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1730,7 +1730,7 @@ exports.Orderline = Backbone.Model.extend({ var sign = 1; if(base < 0){ base = -base; - var sign = -1; + sign = -1; } var incl_fixed_amount = 0.0; @@ -1777,8 +1777,8 @@ exports.Orderline = Backbone.Model.extend({ }); return { taxes: taxes_vals, - total_excluded: round_pr(total_excluded, currency_rounding_bak), - total_included: round_pr(total_included, currency_rounding_bak) + total_excluded: sign * round_pr(total_excluded, currency_rounding_bak), + total_included: sign * round_pr(total_included, currency_rounding_bak) }; }, get_all_prices: function(){ @@ -1796,6 +1796,7 @@ exports.Orderline = Backbone.Model.extend({ return t.id === el; })); }); + product_taxes = _.map(product_taxes, this._map_tax_fiscal_position.bind(this)); var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding); _(all_taxes.taxes).each(function(tax) { diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 392447809f4..ce9aec95e43 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -445,7 +445,7 @@ class PricelistItem(models.Model): elif self.compute_price == 'percentage': self.price = _("%s %% discount") % (self.percent_price) else: - self.price = _("%s %% discount and %s surcharge") % (abs(self.price_discount), self.price_surcharge) + self.price = _("%s %% discount and %s surcharge") % (self.price_discount, self.price_surcharge) @api.onchange('applied_on') def _onchange_applied_on(self): diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 9f6ec7b96a8..4fa085c03ee 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -966,15 +966,16 @@ class SaleOrderLine(models.Model): # TO DO: move me in master/saas-16 on sale.order if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=self.order_id.pricelist_id.id).price - price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + final_price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) pricelist_item = self.env['product.pricelist.item'].browse(rule_id) - if (pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id.discount_policy == 'with_discount'): - price, rule_id = pricelist_item.base_pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - return price + if pricelist_item.base == 'pricelist': + base_price, rule_id = pricelist_item.base_pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + base_price = pricelist_item.base_pricelist_id.currency_id.compute(base_price, self.order_id.pricelist_id.currency_id) else: - from_currency = self.order_id.company_id.currency_id - product_price = product[pricelist_item.base] if (pricelist_item and pricelist_item.base != 'pricelist') else product.lst_price - return from_currency.compute(product_price, self.order_id.pricelist_id.currency_id) + base_price = product[pricelist_item.base] if pricelist_item else product.lst_price + base_price = product.currency_id.compute(base_price, self.order_id.pricelist_id.currency_id) + # negative discounts (= surcharge) are included in the display price (= unit price) + return max(base_price, final_price) @api.multi @api.onchange('product_id') diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 156260cd6c3..f9571724d2a 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -301,7 +301,7 @@ class InventoryLine(models.Model): domain=[('type', '=', 'product')], index=True, required=True) product_name = fields.Char( - 'Product Name', related='product_id.name', store=True) + 'Product Name', related='product_id.name', store=True, readonly=True) product_code = fields.Char( 'Product Code', related='product_id.default_code', store=True) product_uom_id = fields.Many2one( @@ -364,8 +364,14 @@ class InventoryLine(models.Model): self._compute_theoretical_qty() self.product_qty = self.theoretical_qty + @api.model + def write(self, values): + values.pop('product_name', False) + res = super(InventoryLine, self).write(values) + @api.model def create(self, values): + values.pop('product_name', False) if 'product_id' in values and 'product_uom_id' not in values: values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id existings = self.search([ diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml index 0ec55f33537..b88346da4e8 100644 --- a/addons/stock/views/stock_quant_views.xml +++ b/addons/stock/views/stock_quant_views.xml @@ -13,6 +13,8 @@ + + @@ -117,7 +119,7 @@ stock.quant form tree,form,pivot,graph - {'search_default_internal_loc': 1,} + {'search_default_internal_loc': 1, 'search_default_stockable': 1, 'group_by': ['product_id', 'location_id']} This analysis gives you a fast overview on the current stock level of your products and their current inventory value. diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index cef45c2a976..fc29ba77466 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -391,6 +391,8 @@ var AbstractField = Widget.extend({ * Please do not use this flag unless you really need it. Our only use * case is currently the pad widget, which does a _setValue in the * renderEdit method. + * @param {boolean} [options.notifyChange=true] if false, the basic model + * will not notify and not trigger the onchange, even though it was changed. * @param {boolean} [options.forceChange=false] if true, the change event will be * triggered even if the new value is the same as the old one * @returns {Deferred} @@ -421,6 +423,7 @@ var AbstractField = Widget.extend({ changes: changes, viewType: this.viewType, doNotSetDirty: options && options.doNotSetDirty, + notifyChange: !options || options.notifyChange !== false, onSuccess: def.resolve.bind(def), onFailure: def.reject.bind(def), }); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 8eae8f5b8f1..549b2e256be 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -205,7 +205,7 @@ var InputField = DebouncedField.extend({ if (!event || event === this.lastChangeEvent) { this.isDirty = false; } - if (this.isDirty || (event && event.target === this)) { + if (this.isDirty || (event && event.target === this && event.data.changes[this.name] === this.value)) { return $.when(); } else { return this._render(); diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index c4422285ae7..f2cacb3327f 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1033,13 +1033,25 @@ var FieldX2Many = AbstractField.extend({ */ _onResequence: function (event) { var self = this; - _.each(event.data.rowIDs, function (rowID, index) { + var rowIDs = event.data.rowIDs.slice(); + var rowID = rowIDs.pop(); + var defs = _.map(rowIDs, function (rowID, index) { var data = {}; data[event.data.handleField] = event.data.offset + index; - self._setValue({ + return self._setValue({ operation: 'UPDATE', id: rowID, data: data, + }, { + notifyChange: false, + }); + }); + $.when.apply($, defs).then(function () { + // trigger only once the onchange for parent record + self._setValue({ + operation: 'UPDATE', + id: rowID, + data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]), }); }); }, diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index c899530de07..5fc68e3124e 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -447,7 +447,11 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { */ _setMode: function (mode, recordID) { if ((recordID || this.handle) === this.handle) { - return this.update({mode: mode}, {reload: false}); + return this.update({mode: mode}, {reload: false}).then(function () { + // necessary to allow all sub widgets to use their dimensions in + // layout related activities, such as autoresize on fieldtexts + core.bus.trigger('DOM_updated'); + }); } return $.when(); }, 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 1df118ecfd9..f3b23bdab7a 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1092,6 +1092,9 @@ var BasicModel = AbstractModel.extend({ * @param {boolean} [options.doNotSetDirty=false] if this flag is set to * true, then we will not tag the record as dirty. This should be avoided * for most situations. + * @param {boolean} [options.notifyChange=true] if this flag is set to + * false, then we will not notify and not trigger the onchange, even though + * it was changed. * @param {string} [options.viewType] current viewType. If not set, we will assume * main viewType from the record * @returns {Deferred} @@ -1119,6 +1122,10 @@ var BasicModel = AbstractModel.extend({ } } + if (options.notifyChange === false) { + return $.when(); + } + return $.when.apply($, defs).then(function () { var onChangeFields = []; // the fields that have changed and that have an on_change for (var fieldName in changes) { @@ -2305,6 +2312,7 @@ var BasicModel = AbstractModel.extend({ var ids = record.data[fieldName] || []; var list = self._makeDataPoint({ count: ids.length, + context: record.context, fieldsInfo: fieldsInfo, fields: view ? view.fields : fieldInfo.relatedFields, limit: fieldInfo.limit, diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index 08d03c037f0..f76f40460a1 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -56,7 +56,6 @@ var CalendarController = AbstractController.extend({ // Public //-------------------------------------------------------------------------- - /** * Render the buttons according to the CalendarView.buttons template and * add listeners on it. Set this.$buttons with the produced jQuery element @@ -160,10 +159,10 @@ var CalendarController = AbstractController.extend({ } this.quickCreating = true; this.model.createRecord(event) - .then(function (id) { + .then(function () { self.quick.destroy(); self.quick = null; - self.reload(id); + self.reload(); }) .fail(function (error, errorEvent) { // This will occurs if there are some more fields required @@ -215,7 +214,6 @@ var CalendarController = AbstractController.extend({ if(!options.disableQuickCreate && !event.data.disableQuickCreate && this.quickAddPop) { this.quick = new QuickCreate(this, true, options, data, event.data); - this.quick.on('added', this, this.reload.bind(this)); this.quick.open(); this.quick.focus(); return; diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 4374421d7b3..b32374057bb 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -322,6 +322,7 @@ return AbstractModel.extend({ // List authorized values for every field // fields with an active 'all' filter are skipped var authorizedValues = {}; + var avoidValues = {}; _.each(this.data.filters, function (filter) { // Skip 'all' filters because they do not affect the domain @@ -329,11 +330,20 @@ return AbstractModel.extend({ // Loop over subfilters to complete authorizedValues _.each(filter.filters, function (f) { - if (!authorizedValues[filter.fieldName]) - authorizedValues[filter.fieldName] = []; + if (filter.write_model) { + if (!authorizedValues[filter.fieldName]) + authorizedValues[filter.fieldName] = []; - if (f.active) { - authorizedValues[filter.fieldName].push(f.value); + if (f.active) { + authorizedValues[filter.fieldName].push(f.value); + } + } else { + if (!avoidValues[filter.fieldName]) + avoidValues[filter.fieldName] = []; + + if (!f.active) { + avoidValues[filter.fieldName].push(f.value); + } } }); }); @@ -343,6 +353,9 @@ return AbstractModel.extend({ for (var field in authorizedValues) { domain.push([field, 'in', authorizedValues[field]]); } + for (var field in avoidValues) { + domain.push([field, 'not in', avoidValues[field]]); + } return domain; }, diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js index aaf7e4a5859..2909308cf29 100644 --- a/addons/web/static/src/js/views/field_manager_mixin.js +++ b/addons/web/static/src/js/views/field_manager_mixin.js @@ -46,7 +46,7 @@ var FieldManagerMixin = { */ _applyChanges: function (dataPointID, changes, event) { var self = this; - var options = _.pick(event.data, 'viewType', 'doNotSetDirty'); + var options = _.pick(event.data, 'viewType', 'doNotSetDirty', 'notifyChange'); return this.model.notifyChanges(dataPointID, changes, options) .then(function (result) { if (event.data.force_save) { 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 5224a6c149a..7e18849af3f 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -800,10 +800,6 @@ var FormRenderer = BasicRenderer.extend({ this.$el.toggleClass('o_form_editable', this.mode === 'edit'); this.$el.toggleClass('o_form_readonly', this.mode === 'readonly'); - // Necessary to allow all sub widgets to use their dimensions in - // layout related activities, such as autoresize on fieldtexts - core.bus.trigger('DOM_updated'); - // Attach the tooltips on the fields' label _.each(this.allFieldWidgets[this.state.id], function (widget) { var idForLabel = self.idsForLabels[widget.name]; 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 b0ddbdcd385..c1ef23b6b5e 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 @@ -101,6 +101,7 @@ ListRenderer.include({ }); var $row = self.$('.o_data_row:nth(' + rowIndex + ')'); self._setDecorationClasses(state.data[rowIndex], $row); + self._updateFooter(); } return widgets; }); @@ -542,12 +543,21 @@ ListRenderer.include({ _resequence: function (event, ui) { var self = this; var movedRecordID = ui.item.data('id'); - var rowIDs = _.pluck(this.state.data, 'id'); - rowIDs = _.without(rowIDs, movedRecordID); - rowIDs.splice(ui.item.index(), 0, movedRecordID); - var sequences = _.map(this.state.data, function(record) { - return record.data[self.handleField]; - }); + var rows = this.state.data; + var row = _.findWhere(rows, {id: movedRecordID}); + var index0 = rows.indexOf(row); + var index1 = ui.item.index(); + rows = rows.slice(Math.min(index0, index1), Math.max(index0, index1) + 1); + rows = _.without(rows, row); + if (index0 > index1) { + rows.unshift(row); + } else { + rows.push(row); + } + + var sequences = _.pluck(_.pluck(rows, 'data'), self.handleField); + var rowIDs = _.pluck(rows, 'id'); + this.trigger_up('resequence', { rowIDs: rowIDs, offset: _.min(sequences), diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index d37120339d6..8b24d265be8 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -681,6 +681,17 @@ var ListRenderer = BasicRenderer.extend({ $tr.toggleClass(cssClass, py.PY_isTrue(py.evaluate(expr, record.evalContext))); }); }, + /** + * Update the footer aggregate values. This method should be called each + * time the state of some field is changed, to make sure their sum are kept + * in sync. + * + * @private + */ + _updateFooter: function () { + this._computeAggregates(); + this.$('tfoot').replaceWith(this._renderFooter(!!this.state.groupedBy.length)); + }, /** * Whenever we change the state of the selected rows, we need to call this * method to keep the this.selection variable in sync, and also to recompute @@ -695,8 +706,7 @@ var ListRenderer = BasicRenderer.extend({ return $(row).data('id'); }); this.trigger_up('selection_changed', { selection: this.selection }); - this._computeAggregates(); - this.$('tfoot').replaceWith(this._renderFooter(!!this.state.groupedBy.length)); + this._updateFooter(); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index 9bfbd6c535e..cd9d550843a 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -203,6 +203,7 @@ &.disabled { opacity: 1.0; color: @text-muted; + pointer-events: none; border-left: 1px solid @gray-lighter-dark; } diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less index 82debf1a035..395bf633e45 100644 --- a/addons/web/static/src/less/list_view.less +++ b/addons/web/static/src/less/list_view.less @@ -138,6 +138,14 @@ // consider this later if needed. z-index: 1; + // The in-edition elements will overflow other elements (the rows below, + // the content below the list view, ...). As those might be using + // position: relative, the in-edition elements have to define a z-index. + // Technically, creating a stacking context for a meaningful parent + // (maybe the .o_content) might be required for safety but we should + // consider this later if needed. + z-index: 1; + // Field widgets override > .o_field_widget { .o-position-absolute(0, 0, auto, 0); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 2d328f0b7ff..594dd6103d5 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2003,9 +2003,10 @@ QUnit.module('relational_fields', { }); QUnit.test('embedded one2many with handle widget', function (assert) { - assert.expect(8); + assert.expect(7); + + this.data.partner.records[0].turtles = [1, 2, 3]; - this.data.partner.records[0].p = [1, 2, 4]; var form = createView({ View: FormView, model: 'partner', @@ -2014,10 +2015,10 @@ QUnit.module('relational_fields', { '' + '' + '' + - '' + - '' + - '' + - '' + + '' + + '' + + '' + + '' + '' + '' + '' + @@ -2028,14 +2029,14 @@ QUnit.module('relational_fields', { }); testUtils.intercept(form, "field_changed", function (event) { - assert.step(event.data.changes.p.data.int_field.toString()); + assert.step(event.data.changes.turtles.data.turtle_int.toString()); }, true); - assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); form.$buttons.find('.o_form_button_edit').click(); - assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should still have the 3 rows in the correct order"); // Drag and drop the fourth line in second position @@ -2045,21 +2046,169 @@ QUnit.module('relational_fields', { {position: 'top'} ); - assert.verifySteps(["0", "1", "2"], + assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); - assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should have the 3 rows in the new order"); form.$buttons.find('.o_form_button_save').click(); - assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", + assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the new order"); form.destroy(); }); + QUnit.test('onchange for embedded one2many with handle widget', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1, 2, 3]; + var partnerOnchange = 0; + this.data.partner.onchanges = { + turtles: function (obj) { + partnerOnchange++; + }, + }; + var turtleOnchange = 0; + this.data.turtle.onchanges = { + turtle_int: function (obj) { + turtleOnchange++; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + + // Drag and drop the fourth line in second position + testUtils.dragAndDrop( + form.$('.ui-sortable-handle').eq(1), + form.$('tbody tr').first(), + {position: 'top'} + ); + + assert.strictEqual(turtleOnchange, 2, "should trigger one onchange per line updated"); + assert.strictEqual(partnerOnchange, 1, "should trigger only one onchange on the parent"); + + form.destroy(); + }); + + QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) { + var done = assert.async(); + assert.expect(20); + + + this.data.turtle.records[0].turtle_int = 6; + this.data.turtle.records.push({ + id: 4, + turtle_int: 20, + turtle_foo: "a1", + }, { + id: 5, + turtle_int: 9, + turtle_foo: "a2", + }, { + id: 6, + turtle_int: 2, + turtle_foo: "a3", + }, { + id: 7, + turtle_int: 11, + turtle_foo: "a4", + }); + this.data.partner.records[0].turtles = [1, 2, 3, 4, 5, 6, 7]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + testUtils.intercept(form, "field_changed", function (event) { + assert.step(form.model.get(event.data.changes.turtles.id).res_id); + }, true); + + form.$buttons.find('.o_form_button_edit').click(); + + var steps = []; + var positions = [ + [6, 0, 'top', [3, 6, 1, 2, 5, 7, 4]], // move the last to the first line + [5, 1, 'top', [7, 6, 1, 2, 5]], // move the penultimate to the second line + [2, 5, 'center', [1, 2, 5, 6]], // move the third to the penultimate line + ]; + function dragAndDrop() { + var pos = positions.shift(); + + testUtils.dragAndDrop( + form.$('.ui-sortable-handle').eq(pos[0]), + form.$('tbody tr').eq(pos[1]), + {position: pos[2]} + ); + + steps = steps.concat(pos[3]); + assert.verifySteps(steps, + "sequences values should be apply from the begin index to the drop index"); + + if (positions.length) { + + setTimeout(dragAndDrop, 10); + + } else { + + assert.deepEqual(_.pluck(form.model.get(form.handle).data.turtles.data, 'data'), [ + { id: 3, turtle_foo: "kawa", turtle_int: 2 }, + { id: 7, turtle_foo: "a4", turtle_int: 3 }, + { id: 1, turtle_foo: "yop", turtle_int: 4 }, + { id: 2, turtle_foo: "blip", turtle_int: 5 }, + { id: 5, turtle_foo: "a2", turtle_int: 6 }, + { id: 6, turtle_foo: "a3", turtle_int: 7 }, + { id: 4, turtle_foo: "a1", turtle_int: 8 } + ], "sequences must be apply correctly"); + + form.destroy(); + done(); + } + } + + dragAndDrop(); + }); + QUnit.test('embedded one2many (editable list) with handle widget', function (assert) { - assert.expect(9); + assert.expect(8); this.data.partner.records[0].p = [1, 2, 4]; var form = createView({ @@ -2101,7 +2250,7 @@ QUnit.module('relational_fields', { {position: 'top'} ); - assert.verifySteps(["0", "1", "2"], + assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", @@ -5904,6 +6053,7 @@ QUnit.module('relational_fields', { if (args.method === 'read' && args.model === 'partner_type') { assert.deepEqual(args.kwargs.context, { key: 'yop', + active_field: 2, someKey: 'some value', }, "sent context should be correct"); } @@ -6383,6 +6533,36 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('propagate context to sub views', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + assert.strictEqual(args.kwargs.context.flutter, 'shy', + 'view context key should be used for every rpcs'); + return this._super.apply(this, arguments); + }, + viewOptions: {context: {flutter: 'shy'}}, + }); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input[name="turtle_foo"]').val('pinky pie').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index ed9e416a302..14b7ae42b03 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -31,15 +31,15 @@ QUnit.module('Views', { event: { fields: { id: {string: "ID", type: "integer"}, - user_id: {string: "user", type: "many2one", relation: 'user'}, - partner_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.partner_id'}, + user_id: {string: "user", type: "many2one", relation: 'user', default: session.uid}, + partner_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.partner_id', default: 1}, name: {string: "name", type: "char"}, start_date: {string: "start date", type: "date"}, stop_date: {string: "stop date", type: "date"}, start: {string: "start datetime", type: "datetime"}, stop: {string: "stop datetime", type: "datetime"}, allday: {string: "allday", type: "boolean"}, - partner_ids: {string: "attendees", type: "one2many", relation: 'partner'}, + partner_ids: {string: "attendees", type: "one2many", relation: 'partner', default: [[6, 0, [1]]]}, type: {string: "type", type: "integer"}, }, records: [ @@ -1085,7 +1085,6 @@ QUnit.module('Views', { assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body"); }); - QUnit.test('readonly date_start field', function (assert) { assert.expect(4); @@ -1207,6 +1206,305 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('create event with filters', function (assert) { + assert.expect(7); + + this.data.event.fields.user_id.default = 5; + this.data.event.fields.partner_id.default = 3; + this.data.user.records.push({id: 5, display_name: "user 5", partner_id: 3}); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + ''+ + ''+ + '', + viewOptions: { + initialDate: initialDate, + }, + }); + + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + calendar.$('.o_calendar_filter_item[data-value=4] input').click(); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 5, "should display 5 filter items"); + assert.strictEqual(calendar.$('.fc-event').length, 3, "should display 3 events"); + + // quick create a record + var left = calendar.$('.fc-bg td:eq(4)').offset().left+15; + var top = calendar.$('.fc-slats tr:eq(4) td:first').offset().top+15; + try { + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + } catch (e) { + calendar.destroy(); + $view.remove(); + throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); + } + testUtils.triggerPositionalMouseEvent(left, top + 200, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top + 200, "mouseup"); + + $('.modal-body input:first').val('coucou').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click'); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 6, "should add the missing filter (active)"); + assert.strictEqual(calendar.$('.fc-event').length, 4, "should display the created item"); + + // change default value for quick create an hide record + this.data.event.fields.user_id.default = 4; + this.data.event.fields.partner_id.default = 4; + + // quick create and other record + left = calendar.$('.fc-bg td:eq(3)').offset().left+15; + top = calendar.$('.fc-slats tr:eq(4) td:first').offset().top+15; + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + testUtils.triggerPositionalMouseEvent(left, top + 200, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top + 200, "mouseup"); + + $('.modal-body input:first').val('coucou 2').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click'); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 6, "should have the same filters"); + assert.strictEqual(calendar.$('.fc-event').length, 4, "should not display the created item"); + + calendar.$('.o_calendar_filter_item[data-value=4] input').click(); + + assert.strictEqual(calendar.$('.fc-event').length, 11, "should display all records"); + + calendar.destroy(); + $view.remove(); + }); + + QUnit.test('create event with filters (no quickCreate)', function (assert) { + assert.expect(4); + + this.data.event.fields.user_id.default = 5; + this.data.event.fields.partner_id.default = 3; + this.data.user.records.push({ + id: 5, + display_name: "user 5", + partner_id: 3 + }); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + ''+ + ''+ + '', + archs: { + "event,false,form": + '
'+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '
', + }, + viewOptions: { + initialDate: initialDate, + }, + }); + + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + calendar.$('.o_calendar_filter_item[data-value=4] input').click(); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 5, "should display 5 filter items"); + assert.strictEqual(calendar.$('.fc-event').length, 3, "should display 3 events"); + + // quick create a record + var left = calendar.$('.fc-bg td:eq(4)').offset().left+15; + var top = calendar.$('.fc-slats tr:eq(4) td:first').offset().top+15; + try { + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + } catch (e) { + calendar.destroy(); + $view.remove(); + throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); + } + testUtils.triggerPositionalMouseEvent(left, top + 200, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top + 200, "mouseup"); + + $('.modal-body input:first').val('coucou').trigger('input'); + + $('.modal button.btn:contains(Edit)').trigger('click'); + $('.modal button.btn:contains(Save)').trigger('click'); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 6, "should add the missing filter (active)"); + assert.strictEqual(calendar.$('.fc-event').length, 4, "should display the created item"); + + calendar.destroy(); + $view.remove(); + }); + + QUnit.test('Update event with filters', function (assert) { + assert.expect(4); + + var records = this.data.user.records; + records.push({ + id: 5, + display_name: "user 5", + partner_id: 3 + }); + + this.data.event.onchanges = { + user_id: function (obj) { + obj.partner_id = _.findWhere(records, {id:obj.user_id}).partner_id; + } + }; + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + ''+ + ''+ + '', + archs: { + "event,false,form": + '
'+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '
', + }, + viewOptions: { + initialDate: initialDate, + }, + }); + + calendar.$('.o_calendar_filter_item[data-value=4] input').click(); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 5, "should display 5 filter items"); + assert.strictEqual(calendar.$('.fc-event').length, 3, "should display 3 events"); + + calendar.$('.fc-event:contains(event 2) .fc-content').trigger('click'); + $('.modal button.btn:contains(Edit)').trigger('click'); + $('.modal .o_field_widget[name="user_id"] input').trigger('click'); + $('.ui-menu-item a:contains(user 5)').trigger('mouseenter').trigger('click'); + $('.modal button.btn:contains(Save)').trigger('click'); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 6, "should add the missing filter (active)"); + assert.strictEqual(calendar.$('.fc-event').length, 3, "should display the updated item"); + + calendar.destroy(); + }); + + QUnit.test('change pager with filters', function (assert) { + assert.expect(3); + + this.data.user.records.push({ + id: 5, + display_name: "user 5", + partner_id: 3 + }); + this.data.event.records.push({ + id: 7, + user_id: 5, + partner_id: 3, + name: "event 7", + start: "2016-12-06 04:00:00", + stop: "2016-12-06 08:00:00", + allday: false, + partner_ids: [1,2,3], + type: 1 + }, { + id: 8, + user_id: session.uid, + partner_id: 1, + name: "event 8", + start: "2016-12-07 04:00:00", + stop: "2016-12-07 08:00:00", + allday: false, + partner_ids: [1,2,3], + type: 1 + },{ + id: 9, + user_id: 4, + partner_id: 4, + name: "event 9", + start: "2016-12-08 04:00:00", + stop: "2016-12-08 08:00:00", + allday: false, + partner_ids: [1,2,3], + type: 1 + }); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + ''+ + ''+ + ''+ + ''+ + '', + viewOptions: { + initialDate: initialDate, + }, + }); + + calendar.$('.o_calendar_filter_item[data-value=4] input').click(); + $('.o_calendar_button_prev').click(); + + assert.strictEqual(calendar.$('.o_calendar_filter_item').length, 6, "should display 6 filter items"); + assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events"); + assert.strictEqual(calendar.$('.fc-event .o_field_name').text().replace(/\s/g, ''), "event7event8", + "should display 2 events"); + + calendar.destroy(); + }); + QUnit.test('ensure events are still shown if filters give an empty domain', function (assert) { assert.expect(2); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index c6720c429eb..1b32857c951 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1116,6 +1116,42 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('properly apply onchange when changed field is active field', function (assert) { + assert.expect(3); + + this.data.partner.onchanges = { + int_field: function (obj) { + obj.int_field = 14; + }, + }; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '
', + res_id: 2, + viewOptions: {mode: 'edit'}, + }); + + + assert.strictEqual(form.$('input').val(), "9", + "should contain input with initial value"); + + form.$('input').val("666").trigger('input'); + + assert.strictEqual(form.$('input').val(), "14", + "value should have been set to 14 by onchange"); + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), "14", + "value should still be 14"); + + form.destroy(); + }); + QUnit.test('onchange send only the present fields to the server', function (assert) { assert.expect(1); this.data.partner.records[0].product_id = false; @@ -6045,6 +6081,49 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('autoresize of text fields is done when switching to edit mode', function (assert) { + assert.expect(4); + + this.data.partner.fields.text_field = { string: 'Text field', type: 'text' }; + this.data.partner.fields.text_field.default = "some\n\nmulti\n\nline\n\ntext\n"; + this.data.partner.records[0].text_field = "a\nb\nc\nd\ne\nf"; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + }); + + // switch to edit mode to ensure that autoresize is correctly done + form.$buttons.find('.o_form_button_edit').click(); + var height = form.$('.o_field_widget[name=text_field]').height(); + // focus the field to manually trigger autoresize + form.$('.o_field_widget[name=text_field]').trigger('focus'); + assert.strictEqual(form.$('.o_field_widget[name=text_field]').height(), height, + "autoresize should have been done automatically at rendering"); + // next assert simply tries to ensure that the textarea isn't stucked to + // its minimal size, even after being focused + assert.ok(height > 80, "textarea should have an height of at least 80px"); + + // save and create a new record to ensure that autoresize is correctly done + form.$buttons.find('.o_form_button_save').click(); + form.$buttons.find('.o_form_button_create').click(); + height = form.$('.o_field_widget[name=text_field]').height(); + // focus the field to manually trigger autoresize + form.$('.o_field_widget[name=text_field]').trigger('focus'); + assert.strictEqual(form.$('.o_field_widget[name=text_field]').height(), height, + "autoresize should have been done automatically at rendering"); + assert.ok(height > 80, "textarea should have an height of at least 80px"); + + form.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 4a89a4a70e7..67566692f92 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -595,23 +595,43 @@ QUnit.module('Views', { arch: '', }); - var $group_1_header = list.$('.o_group_header').filter(function (index, el) { + var $groupHeader1 = list.$('.o_group_header').filter(function (index, el) { return $(el).data('group').res_id === 1; }); - var $group_2_header = list.$('.o_group_header').filter(function (index, el) { + var $groupHeader2 = list.$('.o_group_header').filter(function (index, el) { return $(el).data('group').res_id === 2; }); - assert.strictEqual($group_1_header.find('td:nth(1)').text(), "23", "first group total should be 23"); - assert.strictEqual($group_2_header.find('td:nth(1)').text(), "9", "second group total should be 9"); + assert.strictEqual($groupHeader1.find('td:nth(1)').text(), "23", "first group total should be 23"); + assert.strictEqual($groupHeader2.find('td:nth(1)').text(), "9", "second group total should be 9"); assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32"); - $group_1_header.click(); + $groupHeader1.click(); list.$('tbody .o_list_record_selector input').first().click(); assert.strictEqual(list.$('tfoot td:nth(2)').text(), "10", "total should be 10 as first record of first group is selected"); list.destroy(); }); + QUnit.test('aggregates are updated when a line is edited', function (assert) { + assert.expect(2); + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + }); + + assert.strictEqual(list.$('td[title="Sum"]').text(), "32", "current total should be 32"); + + list.$('tr.o_data_row td.o_data_cell').first().click(); + list.$('td.o_data_cell input').val("15").trigger("input"); + + assert.strictEqual(list.$('td[title="Sum"]').text(), "37", + "current total should now be 37"); + list.destroy(); + }); + QUnit.test('properly apply onchange in simple case', function (assert) { assert.expect(2); @@ -2707,7 +2727,7 @@ QUnit.module('Views', { "should write the sequence starting from the lowest current one"); assert.strictEqual(args.field, 'int_field', "should write the right field as sequence"); - assert.deepEqual(args.ids, [1, 4, 2 , 3], + assert.deepEqual(args.ids, [4, 2 , 3], "should write the sequence in correct order"); return $.when(); } @@ -2760,7 +2780,7 @@ QUnit.module('Views', { "should write the sequence starting from the lowest current one"); assert.strictEqual(args.field, 'int_field', "should write the right field as sequence"); - assert.deepEqual(args.ids, [1, 4, 2, 3], + assert.deepEqual(args.ids, [4, 2, 3], "should write the sequence in correct order"); return $.when(); } @@ -2820,7 +2840,7 @@ QUnit.module('Views', { "should write the sequence starting from the lowest current one"); assert.strictEqual(args.field, 'int_field', "should write the right field as sequence"); - assert.deepEqual(args.ids, [1, 4, 2, 3], + assert.deepEqual(args.ids, [4, 2, 3], "should write the sequence in correct order"); return $.when(def); } diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index ef102bf081d..33bf1f38046 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -539,6 +539,13 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { so = 0; eo = 1; } + + var parentNode = sc && sc.parentNode; + if (parentNode && sc.tagName === 'BR') { + sc = parentNode; + ec = parentNode; + } + return { sc: sc, so: so, @@ -1130,9 +1137,9 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) { if (!r) return; if (!r.isCollapsed()) { - if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) { + if ((dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) && dom.node(r.sc) !== dom.node(r.ec)) { remove_table_content(r); - r = range.create(r.ec, 0).select(); + r = range.create(r.ec, 0); } r.select(); } diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index e397337ccd6..913addf8fb8 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -6,7 +6,6 @@ odoo.define('website_sale_delivery.checkout', function (require) { /* Handle interactive carrier choice + cart update */ var $pay_button = $('#o_payment_form_pay'); - $pay_button.prop('disabled', false); var _onCarrierUpdateAnswer = function(result) { var $amount_delivery = $('#order_delivery span.oe_currency_value'); diff --git a/doc/cla/individual/IOAyman.md b/doc/cla/individual/IOAyman.md new file mode 100644 index 00000000000..96a37ff862c --- /dev/null +++ b/doc/cla/individual/IOAyman.md @@ -0,0 +1,11 @@ +Algeria, 10-19-2017 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Ayman Nedjmeddine theycallmethedr@gmail.com https://github.com/IOAyman diff --git a/odoo/addons/base/res/res_partner_view.xml b/odoo/addons/base/res/res_partner_view.xml index ca3990dc019..c9af5693b37 100644 --- a/odoo/addons/base/res/res_partner_view.xml +++ b/odoo/addons/base/res/res_partner_view.xml @@ -397,7 +397,8 @@ - + @@ -411,8 +412,8 @@ - - + +
diff --git a/odoo/sql_db.py b/odoo/sql_db.py index 17d73c4d353..50e4ac5dce7 100644 --- a/odoo/sql_db.py +++ b/odoo/sql_db.py @@ -605,7 +605,7 @@ class ConnectionPool(object): cnx.close() break else: - raise PoolError('This connection does not below to the pool') + raise PoolError('This connection does not belong to the pool') @locked def close_all(self, dsn=None):