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):