diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index c4ff295fb70..a34731158e5 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -797,57 +797,63 @@ class AccountMoveLine(models.Model): # For already reconciled lines, don't use amount_residual(_currency) if line.account_id.internal_type == 'liquidity': - amount = abs(debit - credit) - amount_currency = abs(line.amount_currency) + amount = debit - credit + amount_currency = line.amount_currency - # Get right debit / credit: target_currency = target_currency or company_currency - amount_currency = debit - credit - amount_currency_str = "" - total_amount_currency_str = "" - if line_currency != company_currency and target_currency == line_currency: - # The payment currency is the invoice currency, but they are different than the company currency - # We use the `amount_currency` computed during the invoice validation, at the invoice date - # to avoid exchange gain/loss - # e.g. an invoice of 100€ must be paid with 100€, whatever the company currency and the exchange rates - total_amount = line.amount_currency - actual_debit = debit > 0 and amount_currency or 0.0 - actual_credit = credit > 0 and -amount_currency or 0.0 - currency = line_currency - else: - # Either: - # - the invoice, payment, company currencies are all the same, - # - the payment currency is the company currency, but the invoice currency is different, - # - the invoice currency is the company currency, but the payment currency is different, - # - the invoice, payment and company currencies are all different. - # For the two first cases, we can simply use the debit/credit of the invoice move line, which are always in the company currency, - # and this is what the target need. - # For the two last cases, we can use the debit/credit which are in the company currency, and then change them to the target currency - total_amount = abs(debit - credit) - actual_debit = debit > 0 and amount or 0.0 - actual_credit = credit > 0 and -amount or 0.0 - currency = company_currency + + ctx = context.copy() + ctx.update({'date': target_date or line.date}) + # Use case: + # Let's assume that company currency is in USD and that we have the 3 following move lines + # Debit Credit Amount currency Currency + # 1) 25 0 0 NULL + # 2) 17 0 25 EUR + # 3) 33 0 25 YEN + # + # If we ask to see the information in the reconciliation widget in company currency, we want to see + # The following informations + # 1) 25 USD (no currency information) + # 2) 17 USD [25 EUR] (show 25 euro in currency information, in the little bill) + # 3) 33 USD [25 YEN] (show 25 yen in currencu information) + # + # If we ask to see the information in another currency than the company let's say EUR + # 1) 35 EUR [25 USD] + # 2) 25 EUR (no currency information) + # 3) 50 EUR [25 YEN] + # In that case, we have to convert the debit-credit to the currency we want and we show next to it + # the value of the amount_currency or the debit-credit if no amount currency + if target_currency == company_currency: + if line_currency == target_currency: + amount = amount + amount_currency = "" + total_amount = debit - credit + total_amount_currency = "" + else: + amount = amount + amount_currency = amount_currency + total_amount = debit - credit + total_amount_currency = line.amount_currency - if line_currency != target_currency: - amount_currency = target_currency.compute(total_amount, line_currency) - amount_currency_str = formatLang(self.env, abs(amount_currency), currency_obj=line_currency) - total_amount_currency_str = formatLang(self.env, target_currency.compute(total_amount, line_currency), currency_obj=line_currency) - if currency != target_currency: - ctx = context.copy() - ctx.update({'date': target_date or line.date}) - total_amount = currency.with_context(ctx).compute(total_amount, target_currency) - actual_debit = currency.with_context(ctx).compute(actual_debit, target_currency) - actual_credit = currency.with_context(ctx).compute(actual_credit, target_currency) - amount_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=target_currency) - total_amount_str = formatLang(self.env, total_amount, currency_obj=target_currency) + if target_currency != company_currency: + if line_currency == target_currency: + amount = amount_currency + amount_currency = "" + total_amount = line.amount_currency + total_amount_currency = "" + else: + amount_currency = line.currency_id and amount_currency or amount + amount = company_currency.with_context(ctx).compute(amount, target_currency) + total_amount = company_currency.with_context(ctx).compute((line.debit - line.credit), target_currency) + total_amount_currency = line.currency_id and line.amount_currency or (line.debit - line.credit) - ret_line['debit'] = abs(actual_debit) - ret_line['credit'] = abs(actual_credit) - ret_line['amount_str'] = amount_str - ret_line['total_amount_str'] = total_amount_str - ret_line['amount_currency_str'] = amount_currency_str + ret_line['debit'] = amount > 0 and amount or 0 + ret_line['credit'] = amount < 0 and -amount or 0 ret_line['amount_currency'] = amount_currency - ret_line['total_amount_currency_str'] = total_amount_currency_str + ret_line['amount_str'] = formatLang(self.env, abs(amount), currency_obj=target_currency) + ret_line['total_amount_str'] = formatLang(self.env, abs(total_amount), currency_obj=target_currency) + ret_line['amount_currency_str'] = amount_currency and formatLang(self.env, abs(amount_currency), currency_obj=line_currency) or "" + ret_line['total_amount_currency_str'] = total_amount_currency and formatLang(self.env, abs(total_amount_currency), currency_obj=line_currency) or "" ret.append(ret_line) return ret @@ -1618,6 +1624,15 @@ class AccountPartialReconcile(models.Model): created_lines |= line_to_rec return created_lines, partial_rec + def _get_tax_cash_basis_base_account(self, line, tax): + ''' Get the account of lines that will contain the base amount of taxes. + + :param line: An account.move.line record + :param tax: An account.tax record + :return: An account record + ''' + return line.account_id + def create_tax_cash_basis_entry(self, percentage_before_rec): self.ensure_one() move_date = self.debit_move_id.date @@ -1675,11 +1690,12 @@ class AccountPartialReconcile(models.Model): newly_created_move = self._create_tax_basis_move() #create cash basis entry for the base for tax in line.tax_ids: + account_id = self._get_tax_cash_basis_base_account(line, tax) self.env['account.move.line'].with_context(check_move_validity=False).create({ 'name': line.name, 'debit': rounded_amt > 0 and rounded_amt or 0.0, 'credit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': line.account_id.id, + 'account_id': account_id.id, 'tax_exigible': True, 'tax_ids': [(6, 0, [tax.id])], 'move_id': newly_created_move.id, @@ -1691,7 +1707,7 @@ class AccountPartialReconcile(models.Model): 'name': line.name, 'credit': rounded_amt > 0 and rounded_amt or 0.0, 'debit': rounded_amt < 0 and abs(rounded_amt) or 0.0, - 'account_id': line.account_id.id, + 'account_id': account_id.id, 'tax_exigible': True, 'move_id': newly_created_move.id, 'currency_id': line.currency_id.id, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 41fb2d32003..b07961b9ef1 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -805,7 +805,7 @@ var StatementModel = BasicModel.extend({ if (!prop.invalid) { total -= prop.amount; if (isOtherCurrencyId) { - amount_currency -= prop.amount_currency; + amount_currency -= (prop.amount < 0 ? -1 : 1) * Math.abs(prop.amount_currency); } } }); diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index 590619ae936..f448b9c42d5 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -920,7 +920,7 @@ class GoogleCalendar(models.AbstractModel): def get_minTime(self): number_of_week = self.env['ir.config_parameter'].sudo().get_param('calendar.week_synchro', default=13) - return datetime.now() - timedelta(weeks=number_of_week) + return datetime.now() - timedelta(weeks=int(number_of_week)) def get_need_synchro_attendee(self): return self.env['ir.config_parameter'].sudo().get_param('calendar.block_synchro_attendee', default=True) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 17cbc2b14bf..8ee0401b12f 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -442,13 +442,7 @@ class HrExpenseSheet(models.Model): @api.model def create(self, vals): - # Add the followers at creation, so they can be notified - if vals.get('employee_id'): - employee = self.env['hr.employee'].browse(vals['employee_id']) - users = self._get_users_to_subscribe(employee=employee) - self.env.user - vals['message_follower_ids'] = [] - for partner in users.mapped('partner_id'): - vals['message_follower_ids'] += self.env['mail.followers']._add_follower_command(self._name, [], {partner.id: None}, {})[0] + self._create_set_followers(vals) sheet = super(HrExpenseSheet, self).create(vals) self.check_consistency() return sheet @@ -500,6 +494,20 @@ class HrExpenseSheet(models.Model): users = self._get_users_to_subscribe() self.message_subscribe_users(user_ids=users.ids) + @api.model + def _create_set_followers(self, values): + # Add the followers at creation, so they can be notified + employee_id = values.get('employee_id') + if not employee_id: + return + + employee = self.env['hr.employee'].browse(employee_id) + users = self._get_users_to_subscribe(employee=employee) - self.env.user + values['message_follower_ids'] = [] + MailFollowers = self.env['mail.followers'] + for partner in users.mapped('partner_id'): + values['message_follower_ids'] += MailFollowers._add_follower_command(self._name, [], {partner.id: None}, {})[0] + @api.onchange('employee_id') def _onchange_employee_id(self): self.address_id = self.employee_id.address_home_id @@ -516,22 +524,6 @@ class HrExpenseSheet(models.Model): ).compute(expense.total_amount, self.currency_id) self.total_amount = total_amount - # FIXME: A 4 command is missing to explicitly declare the one2many relation - # between the sheet and the lines when using 'default_expense_line_ids':[ids] - # in the context. A fix from chm-odoo should come since - # several saas versions but sadly I had to add this hack to avoid this - # issue - @api.model - def _add_missing_default_values(self, values): - values = super(HrExpenseSheet, self)._add_missing_default_values(values) - if self.env.context.get('default_expense_line_ids', False): - lines_to_add = [] - for line in values.get('expense_line_ids', []): - if line[0] == 1: - lines_to_add.append([4, line[1], False]) - values['expense_line_ids'] = lines_to_add + values['expense_line_ids'] - return values - @api.one def _compute_attachment_number(self): self.attachment_number = sum(self.expense_line_ids.mapped('attachment_number')) diff --git a/addons/l10n_ca/data/account_chart_template_after_data.xml b/addons/l10n_ca/data/account_chart_template_after_data.xml index a3c1e720b18..6216f65899b 100644 --- a/addons/l10n_ca/data/account_chart_template_after_data.xml +++ b/addons/l10n_ca/data/account_chart_template_after_data.xml @@ -8,7 +8,7 @@ - + diff --git a/addons/l10n_ca/data/l10n_ca_chart_data.xml b/addons/l10n_ca/data/l10n_ca_chart_data.xml index 0a463374290..8610079a78f 100644 --- a/addons/l10n_ca/data/l10n_ca_chart_data.xml +++ b/addons/l10n_ca/data/l10n_ca_chart_data.xml @@ -217,7 +217,7 @@ 2171 - + Stock Received But Not Billed diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index d78d57e6c66..871dd856bfc 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -5,10 +5,11 @@ import base64 import io +from datetime import datetime from odoo import api, fields, models, _ from odoo.exceptions import Warning -from odoo.tools import pycompat +from odoo.tools import pycompat, DEFAULT_SERVER_DATE_FORMAT class AccountFrFec(models.TransientModel): @@ -33,7 +34,7 @@ class AccountFrFec(models.TransientModel): SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'Balance initiale PL' AS EcritureNum, + 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, '120/129' AS CompteNum, 'Benefice (perte) reporte(e)' AS CompteLib, @@ -67,8 +68,10 @@ class AccountFrFec(models.TransientModel): ''' company = self.env.user.company_id formatted_date_from = self.date_from.replace('-', '') + date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT) + formatted_date_year = date_from.year self._cr.execute( - sql_query, (formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) + sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) listrow = [] row = self._cr.fetchone() listrow = list(row) @@ -130,7 +133,7 @@ class AccountFrFec(models.TransientModel): SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'Balance initiale ' || MIN(aa.name) AS EcritureNum, + 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib, @@ -170,8 +173,10 @@ class AccountFrFec(models.TransientModel): HAVING sum(aml.balance) != 0 ''' formatted_date_from = self.date_from.replace('-', '') + date_from = datetime.strptime(self.date_from, DEFAULT_SERVER_DATE_FORMAT) + formatted_date_year = date_from.year self._cr.execute( - sql_query, (formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) + sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) for row in self._cr.fetchall(): listrow = list(row) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 4fa085c03ee..c2497b7f2ad 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -967,14 +967,11 @@ class SaleOrderLine(models.Model): if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=self.order_id.pricelist_id.id).price 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': - 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: - 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) + context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order) + base_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + if currency_id != self.order_id.pricelist_id.currency_id.id: + base_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(base_price, self.order_id.pricelist_id.currency_id) + # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) @api.multi @@ -1138,11 +1135,10 @@ class SaleOrderLine(models.Model): price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) - new_list_price = self.env['account.tax']._fix_tax_included_price_company(new_list_price, self.product_id.taxes_id, self.tax_id, self.company_id) if new_list_price != 0: - if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id: - # new_list_price is in company's currency while price in pricelist currency + if self.order_id.pricelist_id.currency_id.id != currency_id: + # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id) discount = (new_list_price - price) / new_list_price * 100 if discount > 0: diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index fc29ba77466..2534f8ae90a 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -43,6 +43,15 @@ var AbstractField = Widget.extend({ custom_events: { navigation_move: '_onNavigationMove', }, + + /** + * An object representing fields to be fetched by the model eventhough not present in the view + * This object contains "field name" as key and an object as value. + * That value object must contain the key "type" + * see FieldBinaryImage for an example. + */ + fieldDependencies: {}, + /** * If this flag is set to true, the field widget will be reset on every * change which is made in the view (if the view supports it). This is diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 549b2e256be..817c76323d5 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -20,6 +20,7 @@ var framework = require('web.framework'); var session = require('web.session'); var utils = require('web.utils'); var view_dialogs = require('web.view_dialogs'); +var field_utils = require('web.field_utils'); var qweb = core.qweb; var _t = core._t; @@ -1169,6 +1170,10 @@ var AbstractFieldBinary = AbstractField.extend({ }); var FieldBinaryImage = AbstractFieldBinary.extend({ + fieldDependencies: _.extend({}, AbstractFieldBinary.prototype.fieldDependencies, { + __last_update: {type: 'datetime'}, + }), + template: 'FieldBinaryImage', placeholder: "/web/static/src/img/placeholder.png", events: _.extend({}, AbstractFieldBinary.prototype.events, { @@ -1191,7 +1196,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({ id: JSON.stringify(this.res_id), field: this.nodeOptions.preview_image || this.name, // unique forces a reload of the image when the record has been updated - unique: (this.recordData.__last_update || '').replace(/[^0-9]/g, ''), + unique: field_utils.format.datetime(this.recordData.__last_update).replace(/[^0-9]/g, ''), }); } } diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js index 911e1c5582f..32d526e160d 100644 --- a/addons/web/static/src/js/services/data_manager.js +++ b/addons/web/static/src/js/services/data_manager.js @@ -388,6 +388,11 @@ return core.Class.extend({ } } } + + if (attrs.Widget.prototype.fieldDependencies) { + attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies; + } + return attrs; }, /** @@ -414,6 +419,15 @@ return core.Class.extend({ if (node.tag === 'field') { fieldsInfo[node.attrs.name] = self._processField(viewType, fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {}); + + if (fieldsInfo[node.attrs.name].fieldDependencies) { + var deps = fieldsInfo[node.attrs.name].fieldDependencies; + for (var dependency_name in deps) { + if (!(dependency_name in fieldsInfo)) { + fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type}; + } + } + } return false; } return node.tag !== 'arch'; 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 9f8ea3227f5..4412d29c455 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1109,6 +1109,10 @@ var BasicModel = AbstractModel.extend({ if (!options.doNotSetDirty) { record._isDirty = true; } + var initialData = {}; + this._visitChildren(record, function (elem) { + initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes')); + }); // apply changes to local data for (var fieldName in changes) { @@ -1137,12 +1141,18 @@ var BasicModel = AbstractModel.extend({ } } } - var onchangeDef; + var onchangeDef = $.Deferred(); if (onChangeFields.length) { - onchangeDef = self._performOnChange(record, onChangeFields, options.viewType).then(function (result) { - delete record._warning; - return _.keys(changes).concat(Object.keys(result && result.value || {})); - }); + self._performOnChange(record, onChangeFields, options.viewType) + .then(function (result) { + delete record._warning; + onchangeDef.resolve(_.keys(changes).concat(Object.keys(result && result.value || {}))); + }).fail(function () { + self._visitChildren(record, function (elem) { + _.extend(elem, initialData[elem.id]); + }); + onchangeDef.resolve({}); + }); } else { onchangeDef = $.Deferred().resolve(_.keys(changes)); } @@ -2653,13 +2663,21 @@ var BasicModel = AbstractModel.extend({ } commands[fieldName].push(command); } else if (_.contains(addedIds, list.res_ids[i])) { - // this is a new id + // this is a new id (maybe existing in DB, but new in JS) relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]}); changes = this._generateChanges(relRecord, options); if ('id' in changes) { + // the subrecord already exists in db delete changes.id; - commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes)); + if (this.isNew(record.id)) { + // if the main record is new, link the subrecord to it + commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id)); + } + if (!_.isEmpty(changes)) { + commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes)); + } } else { + // the subrecord is new, so create it commands[fieldName].push(x2ManyCommands.create(relRecord.ref, changes)); } } @@ -2892,7 +2910,8 @@ var BasicModel = AbstractModel.extend({ * @returns {boolean} */ _isFieldProtected: function (record, fieldName, viewType) { - var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; + var fieldInfo = record.fieldsInfo && + (record.fieldsInfo[viewType || record.viewType][fieldName]); if (fieldInfo) { var rawModifiers = fieldInfo.modifiers || {}; var modifiers = this._evalModifiers(record, rawModifiers); @@ -3249,15 +3268,18 @@ var BasicModel = AbstractModel.extend({ }); return $.when.apply($, defs) .then(function () { - return self._performOnChange(record, fields_key).then(function () { + var def = $.Deferred(); + self._performOnChange(record, fields_key).always(function () { if (record._warning) { if (params.allowWarning) { delete record._warning; } else { - return $.Deferred().reject(); + def.reject(); } } + def.resolve(); }); + return def; }) .then(function () { return self._fetchRelationalData(record); @@ -3324,10 +3346,7 @@ var BasicModel = AbstractModel.extend({ * @param {string[]} fields changed fields * @param {string} [viewType] current viewType. If not set, we will assume * main viewType from the record - * @returns {Deferred} Note: this deferred cannot fail. It is either in the - * 'success' or 'pending' state. If the onchange rpc fails, it will be - * resolved with an empty dictionary. Note that the initial change was - * already applied. + * @returns {Deferred} */ _performOnChange: function (record, fields, viewType) { var self = this; @@ -3347,9 +3366,7 @@ var BasicModel = AbstractModel.extend({ var context = this._getContext(record, options); var currentData = this._generateOnChangeData(record, {changesOnly: false}); - var def = $.Deferred(); - - self._rpc({ + return self._rpc({ model: record.model, method: 'onchange', args: [idList, currentData, fields, onchangeSpec, context], @@ -3374,12 +3391,9 @@ var BasicModel = AbstractModel.extend({ record._domains = _.extend(record._domains, result.domain); } return self._applyOnChange(result.value, record).then(function () { - def.resolve(result); + return result; }); - }).fail(function () { - def.resolve({}); }); - return def; }, /** * Once a record is created and some data has been fetched, we need to do 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 f76f40460a1..2b17710935c 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -115,7 +115,7 @@ var CalendarController = AbstractController.extend({ */ _onChangeDate: function (event) { var modelData = this.model.get(); - if (modelData.target_date.isSame(event.data.date)) { + if (modelData.target_date.format('YYYY-MM-DD') === event.data.date.format('YYYY-MM-DD')) { // When clicking on same date, toggle between the two views switch (modelData.scale) { case 'month': this.model.setScale('week'); break; 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 b32374057bb..a0f6515b98c 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -255,6 +255,8 @@ return AbstractModel.extend({ */ setDate: function (start, highlight) { this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start; + this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); + switch (this.data.scale) { case 'month': this.data.start_date = this.data.start_date.clone().startOf('month').startOf('week'); @@ -391,6 +393,12 @@ return AbstractModel.extend({ firstDay: moment().startOf('week').isoWeekday(), }; }, + /** + * Return a domain from the date range + * + * @private + * @returns {Array} + */ _getRangeDomain: function () { // Build OpenERP Domain to filter object by this.mapping.date_start field // between given start, end dates. @@ -623,8 +631,10 @@ return AbstractModel.extend({ date_stop = date_start.clone().add(date_delay,'hours'); } - date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); - date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); + if (!all_day) { + date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); + date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); + } if (this.mapping.all_day && evt[this.mapping.all_day]) { date_stop.add(1, 'days'); diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index a6248c827a2..7b53b60f90c 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -367,20 +367,8 @@ var FormController = BasicController.extend({ this._disableButtons(); - var attrs = event.data.attrs; - if (attrs.confirm) { - var d = $.Deferred(); - Dialog.confirm(this, attrs.confirm, { confirm_callback: function () { - self._callButtonAction(attrs, event.data.record); - }}).on("closed", null, function () { - d.resolve(); - }); - def = d.promise(); - } else if (attrs.special === 'cancel') { - def = this._callButtonAction(attrs, event.data.record); - } else if (!attrs.special || attrs.special === 'save') { - // save the record but don't switch to readonly mode - def = this.saveRecord(this.handle, { + function saveAndExecuteAction () { + return self.saveRecord(self.handle, { stayInEdit: true, }).then(function () { // we need to reget the record to make sure we have changes made @@ -390,6 +378,21 @@ var FormController = BasicController.extend({ return self._callButtonAction(attrs, record); }); } + var attrs = event.data.attrs; + if (attrs.confirm) { + var d = $.Deferred(); + Dialog.confirm(this, attrs.confirm, { + confirm_callback: saveAndExecuteAction, + }).on("closed", null, function () { + d.resolve(); + }); + def = d.promise(); + } else if (attrs.special === 'cancel') { + def = this._callButtonAction(attrs, event.data.record); + } else if (!attrs.special || attrs.special === 'save') { + // save the record but don't switch to readonly mode + def = saveAndExecuteAction(); + } def.always(this._enableButtons.bind(this)); }, 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 8b24d265be8..33c075fc77f 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -101,9 +101,6 @@ var ListRenderer = BasicRenderer.extend({ } else { data = this.state.data; } - if (data.length === 0) { - return; - } _.each(this.columns, this._computeColumnAggregates.bind(this, data)); }, @@ -145,7 +142,7 @@ var ListRenderer = BasicRenderer.extend({ } }); if (func === 'avg') { - aggregateValue = aggregateValue / count; + aggregateValue = count ? aggregateValue / count : aggregateValue; } column.aggregate = { help: attrs[func], diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 1e623e54d9a..99cc72144b6 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -1465,8 +1465,9 @@ QUnit.module('basic_fields', { QUnit.module('FieldImage'); QUnit.test('image fields are correctly rendered', function (assert) { - assert.expect(5); + assert.expect(6); + this.data.partner.records[0].__last_update = '2017-02-08 10:00:00'; this.data.partner.records[0].document = 'myimage'; var form = createView({ @@ -1477,7 +1478,10 @@ QUnit.module('basic_fields', { ' ' + '', res_id: 1, - mockRPC: function (route) { + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/read') { + assert.deepEqual(args.args[1], ['document', '__last_update', 'display_name'], "The fields document, display_name and __last_update should be present when reading an image"); + } if (route === 'data:image/png;base64,myimage') { assert.ok(true, "should called the correct route"); return $.when('wow'); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index b981a4241c6..f21db07f088 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -1070,7 +1070,9 @@ QUnit.module('relational_fields', { assert.deepEqual( obj.turtles, [ + [4, 2, false], [1, 2, {turtle_foo: 'blip'}], + [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], "should have properly created the x2many command list"); @@ -1099,7 +1101,9 @@ QUnit.module('relational_fields', { // unchanged state with the command 1, but this seems more // difficult. assert.deepEqual(args.args[0].turtles, [ + [4, 2, false], [1, 2, {turtle_foo: 'blip'}], + [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], 'should send proper commands to create method'); } @@ -6474,10 +6478,50 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('default value for nested one2manys (coming from onchange)', function (assert) { + assert.expect(3); + + this.data.partner.onchanges.p = function (obj) { + obj.p = [ + [5], + [0, 0, {turtles: [[5], [4, 1]]}], // link record 1 by default + ]; + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (args.method === 'create') { + assert.strictEqual(args.args[0].p[0][0], 0, + "should send a command 0 (CREATE) for p"); + assert.deepEqual(args.args[0].p[0][2], {turtles: [[4, 1, false]]}, + "should send the correct values"); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(form.$('.o_data_cell').text(), '1 record', + "should correctly display the value of the inner o2m"); + + form.$buttons.find('.o_form_button_save').click(); + + form.destroy(); + }); + QUnit.test('display correct value after validation error', function (assert) { assert.expect(4); - this.data.partner.onchanges.turtles = true; + this.data.partner.onchanges.turtles = function () { }; var form = createView({ View: FormView, @@ -6494,17 +6538,17 @@ QUnit.module('relational_fields', { '', mockRPC: function (route, args) { if (args.method === 'onchange') { - // we simulate a validation error. In the 'real' web client, - // the server error will be used by the session to display - // an error dialog. From the point of view of the basic - // model, the deferred is just rejected. - return $.Deferred().reject(); + if (args.args[1].turtles[0][2].turtle_foo === 'pinky') { + // we simulate a validation error. In the 'real' web client, + // the server error will be used by the session to display + // an error dialog. From the point of view of the basic + // model, the deferred is just rejected. + return $.Deferred().reject(); + } } if (args.method === 'write') { assert.deepEqual(args.args[1].turtles[0], [1,2,{turtle_foo: 'foo'}], - 'should send the "bad" value'); - // we simulate a validation error - return $.Deferred().reject(); + 'should send the "good" value'); } return this._super.apply(this, arguments); }, @@ -6514,21 +6558,25 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'blip', "initial text should be correct"); + + // click and edit value to 'foo', which will trigger onchange form.$('.o_data_row .o_data_cell:nth(0)').click(); form.$('.o_field_widget[name=turtle_foo]').val('foo').trigger('input'); + form.$el.click(); + assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo', + "field should have been changed to foo"); - // we try to validate the line. This triggers an onchange which will be - // rejected. The line will be returned to readonly mode, but with the - // new invalid value. + // click and edit value to 'pinky', which trigger a failed onchange + form.$('.o_data_row .o_data_cell:nth(0)').click(); + form.$('.o_field_widget[name=turtle_foo]').val('pinky').trigger('input'); form.$el.click(); assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo', - "turtle_foo text should now be foo (invalid value)"); + "turtle_foo text should now be set back to foo"); // we make sure here that when we save, the values are the current // values displayed in the field. form.$buttons.find('.o_form_button_save').click(); - assert.strictEqual(form.mode, 'edit', "form view should still be in edit mode"); form.destroy(); }); @@ -6628,7 +6676,6 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index dfcbdba5985..f7d3f2fb307 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -26,6 +26,9 @@ var MockServer = Class.extend({ if (!('display_name' in model.fields)) { model.fields.display_name = {string: "Display Name", type: "char"}; } + if (!('__last_update' in model.fields)) { + model.fields.__last_update = {string: "Last Modified on", type: "datetime"}; + } if (!('name' in model.fields)) { model.fields.name = {string: "Name", type: "char", default: "name"}; } diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 14b7ae42b03..b6cbcd7e03f 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -21,7 +21,8 @@ CalendarRenderer.include({ var createView = testUtils.createView; -var initialDate = new Date("2016-12-12T08:00:00Z"); +var initialDate = new Date(2016, 11, 12, 8, 0, 0); +initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000); QUnit.module('Views', { diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 1b32857c951..e4e33af0a8f 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5355,6 +5355,53 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('buttons with "confirm" attribute save before calling the method', function (assert) { + assert.expect(9); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '
' + + '
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + assert.step(args.method); + return this._super.apply(this, arguments); + }, + intercepts: { + execute_action: function (event) { + assert.step('execute_action'); + }, + }, + }); + + // click on button, and cancel in confirm dialog + form.$('.o_statusbar_buttons button').click(); + assert.ok(form.$('.o_statusbar_buttons button').prop('disabled'), + 'button should be disabled'); + $('.modal .modal-footer button.btn-default').click(); + assert.ok(!form.$('.o_statusbar_buttons button').prop('disabled'), + 'button should no longer be disabled'); + + assert.verifySteps(['default_get']); + + // click on button, and click on ok in confirm dialog + form.$('.o_statusbar_buttons button').click(); + assert.verifySteps(['default_get']); + $('.modal .modal-footer button.btn-primary').click(); + + assert.verifySteps(['default_get', 'create', 'read', 'execute_action']); + + form.destroy(); + }); + QUnit.test('buttons are disabled until action is resolved (in dialogs)', function (assert) { assert.expect(3); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 67566692f92..32e3ad0046a 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -560,7 +560,7 @@ QUnit.module('Views', { }); QUnit.test('aggregates are computed correctly', function (assert) { - assert.expect(3); + assert.expect(4); var list = createView({ View: ListView, @@ -581,6 +581,11 @@ QUnit.module('Views', { $thead_selector.click(); assert.strictEqual(list.$('tfoot td:nth(2)').text(), "32", "total should be 32 as all records are selected"); + + // Let's update the view to dislay NO records + list.update({domain: ['&', ['bar', '=', false], ['int_field', '>', 0]]}); + assert.strictEqual(list.$('tfoot td:nth(2)').text(), "0", "total should have been recomputed to 0"); + list.destroy(); }); diff --git a/addons/web_editor/static/src/js/widgets/ace.js b/addons/web_editor/static/src/js/widgets/ace.js index ff55bb968ed..2112468fa95 100644 --- a/addons/web_editor/static/src/js/widgets/ace.js +++ b/addons/web_editor/static/src/js/widgets/ace.js @@ -385,7 +385,8 @@ var ViewEditor = Widget.extend({ * @returns {integer|string} view ID or less file URL */ _getSelectedResource: function () { - return this.$lists[this.currentType].select2('val'); + var value = this.$lists[this.currentType].select2('val'); + return parseInt(value, 10) || value; }, /** * Loads data the ace editor will vizualize and process it. Default behavior diff --git a/addons/website_event/static/src/js/website_event.js b/addons/website_event/static/src/js/website_event.js index 3b74ecb676b..c677b0bda2c 100644 --- a/addons/website_event/static/src/js/website_event.js +++ b/addons/website_event/static/src/js/website_event.js @@ -31,6 +31,7 @@ var EventRegistrationForm = Widget.extend({ .off('click') .removeClass('a-submit') .click(function (ev) { + $(this).attr('disabled', true); self.on_click(ev); }); }); @@ -52,7 +53,8 @@ var EventRegistrationForm = Widget.extend({ } else { return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) { var $modal = $(modal); - $modal.appendTo($form).modal(); + $modal.find('.modal-body > div').removeClass('container'); // retrocompatibility - REMOVE ME in master / saas-19 + $modal.after($form).modal(); $modal.on('click', '.js_goto_event', function () { $modal.modal('hide'); }); diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index 3781a94640a..e75627f7ee2 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -532,7 +532,7 @@