[MERGE] forward port branch saas-16 up to 8fdac4b6b2
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="property_account_payable_id" ref="chart2111_en"/>
|
||||
<field name="property_account_income_categ_id" ref="chart411_en"/>
|
||||
<field name="property_account_expense_categ_id" ref="chart5111_en"/>
|
||||
<field name="property_stock_account_input_categ_id" ref="chart2141_en"/>
|
||||
<field name="property_stock_account_input_categ_id" ref="chart2171_en"/>
|
||||
<field name="property_stock_account_output_categ_id" ref="chart1145_en"/>
|
||||
<field name="property_stock_valuation_account_id" ref="chart1141_en"/>
|
||||
<field name="income_currency_exchange_account_id" ref="chart42_en"/>
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
|
||||
<record id="chart2171_en" model="account.account.template">
|
||||
<field name="code">2171</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="name">Stock Received But Not Billed</field>
|
||||
<field name="chart_template_id" ref="ca_en_chart_template_en"/>
|
||||
</record>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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', {
|
||||
'<field name="document" widget="image" options="{\'size\': [90, 90]}"/> ' +
|
||||
'</form>',
|
||||
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');
|
||||
|
||||
@@ -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: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree><field name="turtles"/></tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
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', {
|
||||
'</form>',
|
||||
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) {
|
||||
|
||||
@@ -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"};
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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: '<form string="Partners">' +
|
||||
'<header>' +
|
||||
'<button name="post" class="p" string="Confirm" type="object" ' +
|
||||
'confirm="Very dangerous. U sure?"/>' +
|
||||
'</header>' +
|
||||
'<sheet>' +
|
||||
'<field name="foo"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -532,7 +532,7 @@
|
||||
<template id="registration_attendee_details" name="Registration Attendee Details">
|
||||
<div id="modal_attendees_registration" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<form id="attendee_registration" t-attf-action="/event/#{slug(event)}/registration/confirm" method="post">
|
||||
<form id="attendee_registration" t-attf-action="/event/#{slug(event)}/registration/confirm" method="post" class="js_website_submit_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -540,7 +540,7 @@
|
||||
<h4 class="modal-title" id="myModalLabel"><strong>Attendees</strong></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="">
|
||||
<t t-set="counter_type" t-value="1"/>
|
||||
<t t-set="counter" t-value="0"/>
|
||||
<t t-foreach="tickets" t-as="ticket">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</xpath>
|
||||
|
||||
<!-- Generic questions -->
|
||||
<xpath expr="//div[hasclass('container')]/t[last()]" position="after">
|
||||
<xpath expr="//div[hasclass('modal-body')]/div/t[last()]" position="after">
|
||||
<t t-if="event.general_question_ids">
|
||||
<h4 class="page-header mt8"><strong>Questions</strong></h4>
|
||||
<div>
|
||||
|
||||
@@ -228,15 +228,19 @@ class MailGroup(http.Controller):
|
||||
def confirm_unsubscribe(self, channel, partner_id, token, **kw):
|
||||
subscriber = request.env['mail.channel.partner'].search([('channel_id', '=', channel.id), ('partner_id', '=', partner_id)])
|
||||
if not subscriber:
|
||||
partner = request.env['res.partner'].browse(partner_id).sudo().exists()
|
||||
# FIXME: remove try/except in master
|
||||
try:
|
||||
return request.render(
|
||||
'website_mail_channel.not_subscribed', {
|
||||
'partner_id': partner_id
|
||||
})
|
||||
response = request.render(
|
||||
'website_mail_channel.not_subscribed',
|
||||
{'partner_id': partner})
|
||||
# make sure the rendering (and thus error if template is
|
||||
# missing) happens inside the try block
|
||||
response.flatten()
|
||||
return response
|
||||
except ValueError:
|
||||
return _("The address %s is already unsubscribed or was never subscribed to any mailing list") % (
|
||||
partner_id.email
|
||||
partner.email
|
||||
)
|
||||
|
||||
subscriber_token = channel._generate_action_token(partner_id, action='unsubscribe')
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_unsubscribe
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import requests
|
||||
|
||||
from odoo.api import Environment
|
||||
from odoo.tests import common
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
|
||||
class TestConfirmUnsubscribe(common.HttpCase):
|
||||
def setUp(self):
|
||||
super(TestConfirmUnsubscribe, self).setUp()
|
||||
|
||||
cr = self.registry.cursor()
|
||||
# apparently HttpCase does not properly update self.env?
|
||||
self.env2 = env = Environment(cr, self.uid, {})
|
||||
self.partner = env['res.partner'].create({
|
||||
'name': 'Bob',
|
||||
'email': 'bob@bob.bob'
|
||||
})
|
||||
self.mailing_list = env['mail.channel'].create({
|
||||
'name': 'Test Mailing List',
|
||||
'public': 'public',
|
||||
})
|
||||
self.token = self.mailing_list._generate_action_token(self.partner.id, action='unsubscribe')
|
||||
|
||||
def test_not_subscribed(self):
|
||||
"""Test warning works"""
|
||||
self.env2.cr.release()
|
||||
|
||||
self._unsubscribe_check("The address %s is already unsubscribed" % self.partner.email)
|
||||
|
||||
@mute_logger('odoo.addons.website.models.ir_ui_view')
|
||||
def test_not_subscribed_no_template(self):
|
||||
""" Test warning works on db without template (code update w/o module update) """
|
||||
self.env2.ref('website_mail_channel.not_subscribed').unlink()
|
||||
self.assertEquals(
|
||||
self.env2['ir.model.data'].search_count([
|
||||
('module', '=', 'website_mail_channel'),
|
||||
('name', '=', 'not_subscribed'),
|
||||
]), 0, 'XID for template should have been deleted')
|
||||
self.env2.cr.release()
|
||||
|
||||
self._unsubscribe_check("The address %s is already unsubscribed or was never subscribed to any mailing list" % self.partner.email)
|
||||
|
||||
def test_wrong_token(self):
|
||||
self.mailing_list.sudo().write({
|
||||
'channel_partner_ids': [(4, self.partner.id, False)]
|
||||
})
|
||||
self.env2.cr.release()
|
||||
self.token = 'XXX'
|
||||
|
||||
self._unsubscribe_check("Invalid or expired confirmation link.")
|
||||
|
||||
def test_successful_unsubscribe(self):
|
||||
self.mailing_list.sudo().write({
|
||||
'channel_partner_ids': [(4, self.partner.id, False)]
|
||||
})
|
||||
self.env2.cr.release()
|
||||
|
||||
self._unsubscribe_check("You have been correctly unsubscribed")
|
||||
|
||||
def _unsubscribe_check(self, text):
|
||||
url = "/groups/unsubscribe/{}/{}/{}".format(
|
||||
self.mailing_list.id, self.partner.id,
|
||||
self.token
|
||||
)
|
||||
# FIXME: may need to be altered during 11.0 forward port
|
||||
r = self.url_open(url)
|
||||
body = r.read().decode('utf-8')
|
||||
# normalize space to make matching simpler
|
||||
self.assertIn(text, u' '.join(body.split()))
|
||||
Reference in New Issue
Block a user