summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenis Ledoux <dle@odoo.com>2015-11-17 13:19:14 +0100
committerDenis Ledoux <dle@odoo.com>2015-11-17 13:19:14 +0100
commitd47ee18fefed98d6ec07a6fa86443317eab5f4b8 (patch)
tree92d2e24cd6452fcaf8931ab9edf5a0df68444adb
parent238b2027b0628db19fca68acaa6762088e772378 (diff)
parentc9fce1a40441a559c8a36d4ca1d6b2deaf9cae1f (diff)
[MERGE] forward port of branch 8.0 up to c9fce1a
-rw-r--r--addons/account_analytic_default/security/ir.model.access.csv1
-rw-r--r--addons/account_check_writing/report/check_print.py2
-rw-r--r--addons/account_voucher/account_voucher.py2
-rw-r--r--addons/base_import_module/models/ir_module.py1
-rw-r--r--addons/hr_contract/security/ir.model.access.csv1
-rw-r--r--addons/hr_timesheet_sheet/hr_timesheet_sheet.py28
-rw-r--r--addons/mrp/report/price.py12
-rw-r--r--addons/portal_sale/portal_sale_data.xml4
-rw-r--r--addons/product/product.py8
-rw-r--r--addons/sale_service/models/sale_service.py3
-rw-r--r--addons/sale_stock/sale_stock.py2
-rw-r--r--addons/stock/stock.py3
-rw-r--r--addons/web/static/src/js/data.js5
-rw-r--r--addons/web/static/src/js/formats.js11
-rw-r--r--addons/web/static/src/js/view_form.js4
-rw-r--r--addons/web_calendar/static/src/js/web_calendar.js3
-rw-r--r--addons/website/models/ir_http.py3
-rw-r--r--addons/website_mail_group/views/website_mail_group.xml4
-rw-r--r--addons/website_quote/data/website_quotation_data.xml2
-rw-r--r--addons/website_sale/static/src/js/website_sale.js17
-rw-r--r--addons/website_sale_options/views/templates.xml9
-rw-r--r--doc/cla/corporate/adhoc.md1
-rw-r--r--doc/cla/corporate/sodexis.md19
-rw-r--r--openerp/addons/base/ir/ir_actions.py4
-rw-r--r--openerp/addons/base/tests/test_expression.py20
-rw-r--r--openerp/osv/expression.py97
26 files changed, 180 insertions, 86 deletions
diff --git a/addons/account_analytic_default/security/ir.model.access.csv b/addons/account_analytic_default/security/ir.model.access.csv
index cdfebbbb156..dc2ecfdb076 100644
--- a/addons/account_analytic_default/security/ir.model.access.csv
+++ b/addons/account_analytic_default/security/ir.model.access.csv
@@ -1,4 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_analytic_default,account.analytic.default,model_account_analytic_default,account.group_account_user,1,0,0,0
+access_account_analytic_default_analytic,account.analytic.default analytic,model_account_analytic_default,analytic.group_analytic_accounting,1,0,0,0
access_account_analytic_default_invoice,account.analytic.default invoice,model_account_analytic_default,account.group_account_invoice,1,1,1,1
access_account_analytic_default_salesman,account.analytic.default.salesman,model_account_analytic_default,base.group_sale_salesman,1,1,1,1
diff --git a/addons/account_check_writing/report/check_print.py b/addons/account_check_writing/report/check_print.py
index e67d334ae34..f5c304a90b7 100644
--- a/addons/account_check_writing/report/check_print.py
+++ b/addons/account_check_writing/report/check_print.py
@@ -36,7 +36,7 @@ class report_print_check(report_sxw.rml_parse):
})
def fill_stars(self, amount):
- if len(amount) < 100:
+ if amount and len(amount) < 100:
stars = 100 - len(amount)
return ' '.join([amount,'*'*stars])
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index 32baf5b302a..344bf902d11 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -1290,7 +1290,7 @@ class account_voucher(osv.osv):
'move_id': move_id,
'partner_id': line.voucher_id.partner_id.id,
'currency_id': line.move_line_id.currency_id.id,
- 'amount_currency': -1 * foreign_currency_diff,
+ 'amount_currency': (-1 if line.type == 'cr' else 1) * foreign_currency_diff,
'quantity': 1,
'credit': 0.0,
'debit': 0.0,
diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index cbaaa77bd38..05ccdc384d1 100644
--- a/addons/base_import_module/models/ir_module.py
+++ b/addons/base_import_module/models/ir_module.py
@@ -102,6 +102,7 @@ class view(osv.osv):
self.import_module(cr, uid, mod_name, path, force=force, context=context)
success.append(mod_name)
except Exception, e:
+ _logger.exception('Error while importing module')
errors[mod_name] = exception_to_unicode(e)
r = ["Successfully imported module '%s'" % mod for mod in success]
for mod, error in errors.items():
diff --git a/addons/hr_contract/security/ir.model.access.csv b/addons/hr_contract/security/ir.model.access.csv
index 5e35e147230..820935111f0 100644
--- a/addons/hr_contract/security/ir.model.access.csv
+++ b/addons/hr_contract/security/ir.model.access.csv
@@ -1,5 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_resource_manager,hr.employee.resource.manager,resource.model_resource_resource,base.group_hr_manager,1,1,1,1
access_hr_resource_calendar_user,hr.employee.resource.calendar.user,resource.model_resource_calendar,base.group_hr_user,1,1,1,1
+access_hr_resource_calendar_attendance_user,hr.employee.resource.calendar.attendance.user,resource.model_resource_calendar_attendance,base.group_hr_user,1,1,1,1
access_hr_contract_type_manager,hr.contract.type.manager,model_hr_contract_type,base.group_hr_manager,1,1,1,1
access_hr_contract_manager,hr.contract.manager,model_hr_contract,base.group_hr_manager,1,1,1,1
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
index 6be3ba3b885..90265a055fd 100644
--- a/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
+++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet.py
@@ -48,18 +48,24 @@ class hr_timesheet_sheet(osv.osv):
""" Compute the attendances, analytic lines timesheets and differences between them
for all the days of a timesheet and the current day
"""
+ res = dict.fromkeys(ids, {
+ 'total_attendance': 0.0,
+ 'total_timesheet': 0.0,
+ 'total_difference': 0.0,
+ })
+
+ cr.execute("""
+ SELECT sheet_id as id,
+ sum(total_attendance) as total_attendance,
+ sum(total_timesheet) as total_timesheet,
+ sum(total_difference) as total_difference
+ FROM hr_timesheet_sheet_sheet_day
+ WHERE sheet_id IN %s
+ GROUP BY sheet_id
+ """, (tuple(ids),))
+
+ res.update(dict((x.pop('id'), x) for x in cr.dictfetchall()))
- res = {}
- for sheet in self.browse(cr, uid, ids, context=context or {}):
- res.setdefault(sheet.id, {
- 'total_attendance': 0.0,
- 'total_timesheet': 0.0,
- 'total_difference': 0.0,
- })
- for period in sheet.period_ids:
- res[sheet.id]['total_attendance'] += period.total_attendance
- res[sheet.id]['total_timesheet'] += period.total_timesheet
- res[sheet.id]['total_difference'] += period.total_attendance - period.total_timesheet
return res
def check_employee_attendance_state(self, cr, uid, sheet_id, context=None):
diff --git a/addons/mrp/report/price.py b/addons/mrp/report/price.py
index 87387280c95..1b0f7f589f2 100644
--- a/addons/mrp/report/price.py
+++ b/addons/mrp/report/price.py
@@ -42,6 +42,8 @@ class report_custom(report_rml):
rml_obj.localcontext.update({'lang':context.get('lang',False)})
company_currency = user_pool.browse(cr, uid, uid).company_id.currency_id
company_currency_symbol = company_currency.symbol or company_currency.name
+ product_uom_digits = rml_obj.get_digits(dp='Product Unit of Measure')
+ purchase_price_digits = rml_obj.get_digits(dp='Product Price')
def process_bom(bom, currency_id, factor=1):
xml = '<row>'
sum = 0
@@ -63,7 +65,7 @@ class report_custom(report_rml):
'uom': prod.uom_po_id.id,
'date': time.strftime('%Y-%m-%d'),
})[pricelist.id]
- main_sp_price = """<b>"""+rml_obj.formatLang(price)+' '+ (company_currency_symbol)+"""</b>\r\n"""
+ main_sp_price = """<b>"""+rml_obj.formatLang(price, digits=purchase_price_digits)+' '+ (company_currency_symbol)+"""</b>\r\n"""
sum += prod_qtty*price
std_price = product_uom_pool._compute_price(cr, uid, prod.uom_id.id, prod.standard_price, to_uom_id=product_uom.id)
main_strd_price = str(std_price) + '\r\n'
@@ -78,11 +80,11 @@ class report_custom(report_rml):
'uom': prod.uom_po_id.id,
'date': time.strftime('%Y-%m-%d'),
})[pricelist.id]
- sellers_price += """<i>"""+rml_obj.formatLang(price) +' '+ (company_currency_symbol) +"""</i>\r\n"""
+ sellers_price += """<i>"""+rml_obj.formatLang(price, digits=purchase_price_digits) +' '+ (company_currency_symbol) +"""</i>\r\n"""
xml += """<col para='yes'> """+ prod_name +""" </col>
<col para='yes'> """+ main_sp_name + sellers + """ </col>
- <col f='yes'>"""+ rml_obj.formatLang(prod_qtty) +' '+ product_uom_name +"""</col>
- <col f='yes'>"""+ rml_obj.formatLang(float(main_strd_price)) +' '+ (company_currency_symbol) +"""</col>
+ <col f='yes'>"""+ rml_obj.formatLang(prod_qtty, digits=product_uom_digits) +' '+ product_uom_name +"""</col>
+ <col f='yes'>"""+ rml_obj.formatLang(float(main_strd_price), digits=purchase_price_digits) +' '+ (company_currency_symbol) +"""</col>
<col f='yes'>""" + main_sp_price + sellers_price + """</col>'"""
xml += '</row>'
@@ -140,8 +142,6 @@ class report_custom(report_rml):
</row>
""" % (_('Components'), _('Components suppliers'), _('Quantity'),_('Cost Price per Unit of Measure'), _('Supplier Price per Unit of Measure'))
- purchase_price_digits = rml_obj.get_digits(dp='Product Price')
-
for product in product_pool.browse(cr, uid, ids, context=context):
product_uom_name = to_xml(product.uom_id.name)
bom_id = bom_pool._bom_find(cr, uid, product_id=product.id, context=context)
diff --git a/addons/portal_sale/portal_sale_data.xml b/addons/portal_sale/portal_sale_data.xml
index 77cb4f3b2dc..fb611c845a1 100644
--- a/addons/portal_sale/portal_sale_data.xml
+++ b/addons/portal_sale/portal_sale_data.xml
@@ -41,7 +41,7 @@
% endif
</p>
- <% set signup_url = object.get_signup_url() %>
+ % set signup_url = object.get_signup_url()
% if signup_url and object.amount_total > 0 :
<p>
You can access this document and pay online via our Customer Portal:
@@ -128,7 +128,7 @@
% endif
</p>
- <% set signup_url = object.get_signup_url() %>
+ % set signup_url = object.get_signup_url()
% if signup_url:
<p>
You can access the invoice document and pay online via our Customer Portal:
diff --git a/addons/product/product.py b/addons/product/product.py
index 874eb872a79..e68df7014a3 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -424,6 +424,14 @@ class product_attribute_line(osv.osv):
'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Product Attribute Value(s)'),
}
+ def _check_valid_attribute(self, cr, uid, ids, context=None):
+ obj_pal = self.browse(cr, uid, ids[0], context=context)
+ return obj_pal.value_ids <= obj_pal.attribute_id.value_ids
+
+ _constraints = [
+ (_check_valid_attribute, 'Error ! You cannot use this attribute with the following value.', ['attribute_id'])
+ ]
+
#----------------------------------------------------------
# Products
diff --git a/addons/sale_service/models/sale_service.py b/addons/sale_service/models/sale_service.py
index 74540d43547..02d86af6a81 100644
--- a/addons/sale_service/models/sale_service.py
+++ b/addons/sale_service/models/sale_service.py
@@ -19,6 +19,7 @@
#
##############################################################################
+from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools.translate import _
@@ -127,7 +128,7 @@ class project_task(osv.osv):
proc_obj = self.pool.get("procurement.order")
for task in self.browse(cr, uid, ids, context=context):
if task.procurement_id:
- proc_obj.check(cr, uid, [task.procurement_id.id], context=context)
+ proc_obj.check(cr, SUPERUSER_ID, [task.procurement_id.id], context=context)
def write(self, cr, uid, ids, values, context=None):
""" When closing tasks, validate subflows. """
diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py
index 872ebce9a9d..8f2c32a2cb5 100644
--- a/addons/sale_stock/sale_stock.py
+++ b/addons/sale_stock/sale_stock.py
@@ -488,7 +488,7 @@ class stock_picking(osv.osv):
def _get_invoice_vals(self, cr, uid, key, inv_type, journal_id, move, context=None):
inv_vals = super(stock_picking, self)._get_invoice_vals(cr, uid, key, inv_type, journal_id, move, context=context)
sale = move.picking_id.sale_id
- if sale:
+ if sale and inv_type in ('out_invoice', 'out_refund'):
inv_vals.update({
'fiscal_position': sale.fiscal_position.id,
'payment_term': sale.payment_term.id,
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 4aafa8abfe6..701494ac3ae 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -1400,7 +1400,8 @@ class stock_picking(osv.osv):
This can be used to provide a button that rereserves taking into account the existing pack operations
"""
for pick in self.browse(cr, uid, ids, context=context):
- self.rereserve_quants(cr, uid, pick, move_ids = [x.id for x in pick.move_lines], context=context)
+ self.rereserve_quants(cr, uid, pick, move_ids = [x.id for x in pick.move_lines
+ if x.state not in ('done', 'cancel')], context=context)
def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
""" Unreserve quants then try to reassign quants."""
diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js
index c00cedc620b..b15780d252a 100644
--- a/addons/web/static/src/js/data.js
+++ b/addons/web/static/src/js/data.js
@@ -1047,8 +1047,11 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
return this._super(id, signal);
},
alter_ids: function(n_ids) {
+ var dirty = !_.isEqual(this.ids, n_ids);
this._super(n_ids);
- this.trigger("dataset_changed", n_ids);
+ if(dirty) {
+ this.trigger("dataset_changed", n_ids);
+ }
},
});
instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/;
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
index b716f0f0ef3..3461134d725 100644
--- a/addons/web/static/src/js/formats.js
+++ b/addons/web/static/src/js/formats.js
@@ -312,7 +312,16 @@ instance.web.round_precision = function(value, precision){
var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
var epsilon = Math.pow(2, epsilon_magnitude - 53);
normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
- var rounded_value = Math.round(normalized_value);
+
+ /**
+ * Javascript performs strictly the round half up method, which is asymmetric. However, in
+ * Python, the method is symmetric. For example:
+ * - In JS, Math.round(-0.5) is equal to -0.
+ * - In Python, round(-0.5) is equal to -1.
+ * We want to keep the Python behavior for consistency.
+ */
+ var sign = normalized_value < 0 ? -1.0 : 1.0;
+ var rounded_value = sign * Math.round(Math.abs(normalized_value));
return rounded_value * precision;
};
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index 929db311e30..04f6511a1de 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -4413,10 +4413,6 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
}));
this.on('edit:after', this, this.proxy('_after_edit'));
this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
-
- this.records
- .bind('add', this.proxy("changed_records"))
- .bind('remove', this.proxy("changed_records"));
this.on('save:after', this, this.proxy("changed_records"));
},
start: function () {
diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js
index 4da7e37a2fe..dd91ca775ac 100644
--- a/addons/web_calendar/static/src/js/web_calendar.js
+++ b/addons/web_calendar/static/src/js/web_calendar.js
@@ -532,6 +532,9 @@ openerp.web_calendar = function(instance) {
throw new Error("Incomplete data received from dataset for record " + evt.id);
}
}
+ else if (_.contains(["date", "datetime"], self.fields[fieldname].type)) {
+ temp_ret[fieldname] = instance.web.format_value(value, self.fields[fieldname]);
+ }
else {
temp_ret[fieldname] = value;
}
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 11db1070ea2..9af3f55df3c 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -274,7 +274,8 @@ class ir_http(orm.AbstractModel):
logger.error("500 Internal Server Error:\n\n%s", values['traceback'])
if 'qweb_exception' in values:
view = request.registry.get("ir.ui.view")
- views = view._views_get(request.cr, request.uid, exception.qweb['template'], request.context)
+ views = view._views_get(request.cr, request.uid, exception.qweb['template'],
+ context=request.context)
to_reset = [v for v in views if v.model_data_id.noupdate is True and not v.page]
values['views'] = to_reset
elif code == 403:
diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml
index 6617e3d7895..28ab10b4ea9 100644
--- a/addons/website_mail_group/views/website_mail_group.xml
+++ b/addons/website_mail_group/views/website_mail_group.xml
@@ -190,7 +190,7 @@
}'/>
</t>
<t t-if="not message.author_id"><t t-esc="message.email_from"/></t>
- - <i class="fa fa-calendar"/> <t t-esc="message.date"/>
+ - <i class="fa fa-calendar"/> <span t-field="message.date"/>
</small>
<div t-raw="message.body"/>
@@ -262,7 +262,7 @@
}'/>
</t>
<t t-if="not thread.author_id"><t t-esc="thread.email_from"/></t>
- - <i class="fa fa-calendar"/> <t t-esc="thread.date"/>
+ - <i class="fa fa-calendar"/> <span t-field="thread.date"/>
- <i class="fa fa-paperclip"/> <t t-esc="len(thread.attachment_ids)"/>
</small>
<p t-if="thread.child_ids" class="mt8">
diff --git a/addons/website_quote/data/website_quotation_data.xml b/addons/website_quote/data/website_quotation_data.xml
index 56a8810e76b..9466aebee95 100644
--- a/addons/website_quote/data/website_quotation_data.xml
+++ b/addons/website_quote/data/website_quotation_data.xml
@@ -201,7 +201,7 @@
% endif
</p>
- <% set signup_url = object.get_signup_url() %>
+ % set signup_url = object.get_signup_url()
% if signup_url and object.amount_total > 0 :
<p>
You can access this document and pay online via our Customer Portal:
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 03b913fff1c..67c17e86fec 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -1,4 +1,12 @@
$(document).ready(function () {
+
+function update_product_image(event_source, product_id) {
+ var $img = $(event_source).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
+ $img.attr("src", "/website/image/product.product/" + product_id + "/image");
+ $img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
+ .data('oe-model', 'product.product').data('oe-id', product_id);
+}
+
$('.oe_website_sale').each(function () {
var oe_website_sale = this;
@@ -157,9 +165,7 @@ $('.oe_website_sale').each(function () {
var $parent = $(this).closest('.js_product');
$parent.find(".oe_default_price:first .oe_currency_value").html( price_to_str(+$(this).data('lst_price')) );
$parent.find(".oe_price:first .oe_currency_value").html(price_to_str(+$(this).data('price')) );
-
- var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
- $img.attr("src", "/website/image/product.product/" + $(this).val() + "/image");
+ update_product_image(this, +$(this).val());
});
$(oe_website_sale).on('change', 'input.js_variant_change, select.js_variant_change, ul[data-attribute_value_ids]', function (ev) {
@@ -195,10 +201,7 @@ $('.oe_website_sale').each(function () {
}
if (product_id) {
- var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
- $img.attr("src", "/website/image/product.product/" + product_id + "/image");
- $img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
- .data('oe-model', 'product.product').data('oe-id', product_id);
+ update_product_image(this, product_id);
}
$parent.find("input.js_variant_change:radio, select.js_variant_change").each(function () {
diff --git a/addons/website_sale_options/views/templates.xml b/addons/website_sale_options/views/templates.xml
index 411ce05942b..f3e8b7f7a1a 100644
--- a/addons/website_sale_options/views/templates.xml
+++ b/addons/website_sale_options/views/templates.xml
@@ -141,5 +141,14 @@
</div>
</template>
+<template id="products_add_to_cart" inherit_id="website_sale.products_add_to_cart">
+ <xpath expr="//div[@class='product_price']/t[1]" position="attributes">
+ <attribute name="t-if">len(product.product_variant_ids) == 1 and not len(product.optional_product_ids) &gt;= 1</attribute>
+ </xpath>
+ <xpath expr="//div[@class='product_price']/t[2]" position="attributes">
+ <attribute name="t-if">len(product.product_variant_ids) &gt; 1 or len(product.optional_product_ids) &gt;= 1</attribute>
+ </xpath>
+</template>
+
</data>
</openerp>
diff --git a/doc/cla/corporate/adhoc.md b/doc/cla/corporate/adhoc.md
index b43d8ef0e93..627065bafab 100644
--- a/doc/cla/corporate/adhoc.md
+++ b/doc/cla/corporate/adhoc.md
@@ -12,3 +12,4 @@ List of contributors:
Damián Soriano ds@adhoc.com.ar https://github.com/damiansoriano
Juan José Scarafía jjs@adhoc.com.ar https://github.com/jjscarafia
+Nicolás Mac Rouillon nmr@adhoc.com.ar https://github.com/nicomacr
diff --git a/doc/cla/corporate/sodexis.md b/doc/cla/corporate/sodexis.md
new file mode 100644
index 00000000000..2a642474497
--- /dev/null
+++ b/doc/cla/corporate/sodexis.md
@@ -0,0 +1,19 @@
+United States, 2015-09-18
+
+Sodexis, Inc. agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Stephan Keller skeller@sodexis.com https://github.com/stephankeller
+
+List of contributors:
+
+Stephan Keller skeller@sodexis.com https://github.com/stephankeller
+Suganthi Karunanithi ksuganthi@sodexis.com https://github.com/suganthikarunanithi
+Xavier Dass xavier@sodexis.com https://github.com/xavier-dass
+Atchuthan atchuthan@sodexis.com https://github.com/atchuthan
+Dhinesh dhinesh@sodexis.com https://github.com/dvdhinesh
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index b1f1abb5060..c1fbd1a0b22 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -26,6 +26,7 @@ import os
import time
import datetime
import dateutil
+import pytz
import openerp
from openerp import SUPERUSER_ID
@@ -97,6 +98,9 @@ class actions(osv.osv):
'time': time,
'datetime': datetime,
'dateutil': dateutil,
+ # NOTE: only `timezone` function. Do not provide the whole `pytz` module as users
+ # will have access to `pytz.os` and `pytz.sys` to do nasty things...
+ 'timezone': pytz.timezone,
}
class ir_actions_report_xml(osv.osv):
diff --git a/openerp/addons/base/tests/test_expression.py b/openerp/addons/base/tests/test_expression.py
index 43bd7e33394..7750a8d82fb 100644
--- a/openerp/addons/base/tests/test_expression.py
+++ b/openerp/addons/base/tests/test_expression.py
@@ -1,6 +1,7 @@
import unittest2
import openerp
+import openerp.osv.expression as expression
from openerp.osv.expression import get_unaccent_wrapper
from openerp.osv.orm import BaseModel
import openerp.tests.common as common
@@ -502,6 +503,25 @@ class test_expression(common.TransactionCase):
norm_domain = ['&', '&', '&'] + domain
assert norm_domain == expression.normalize_domain(domain), "Non-normalized domains should be properly normalized"
+ def test_40_negating_long_expression(self):
+ source = ['!','&',('user_id','=',4),('partner_id','in',[1,2])]
+ expect = ['|',('user_id','!=',4),('partner_id','not in',[1,2])]
+ self.assertEqual(expression.distribute_not(source), expect,
+ "distribute_not on expression applied wrongly")
+
+ pos_leaves = [[('a', 'in', [])], [('d', '!=', 3)]]
+ neg_leaves = [[('a', 'not in', [])], [('d', '=', 3)]]
+
+ source = expression.OR([expression.AND(pos_leaves)] * 1000)
+ expect = source
+ self.assertEqual(expression.distribute_not(source), expect,
+ "distribute_not on long expression without negation operator should not alter it")
+
+ source = ['!'] + source
+ expect = expression.AND([expression.OR(neg_leaves)] * 1000)
+ self.assertEqual(expression.distribute_not(source), expect,
+ "distribute_not on long expression applied wrongly")
+
def test_translate_search(self):
Country = self.registry('res.country')
be = self.ref('base.be')
diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py
index 4ad8b9d3051..ea2815a0226 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -167,6 +167,26 @@ TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
# legal in the processed term.
NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in')
+# Negation of domain expressions
+DOMAIN_OPERATORS_NEGATION = {
+ AND_OPERATOR: OR_OPERATOR,
+ OR_OPERATOR: AND_OPERATOR,
+}
+TERM_OPERATORS_NEGATION = {
+ '<': '>=',
+ '>': '<=',
+ '<=': '>',
+ '>=': '<',
+ '=': '!=',
+ '!=': '=',
+ 'in': 'not in',
+ 'like': 'not like',
+ 'ilike': 'not ilike',
+ 'not in': 'in',
+ 'not like': 'like',
+ 'not ilike': 'ilike',
+}
+
TRUE_LEAF = (1, '=', 1)
FALSE_LEAF = (0, '=', 1)
@@ -263,51 +283,36 @@ def distribute_not(domain):
['|',('user_id','!=',4),('partner_id','not in',[1,2])]
"""
- def negate(leaf):
- """Negates and returns a single domain leaf term,
- using the opposite operator if possible"""
- left, operator, right = leaf
- mapping = {
- '<': '>=',
- '>': '<=',
- '<=': '>',
- '>=': '<',
- '=': '!=',
- '!=': '=',
- }
- if operator in ('in', 'like', 'ilike'):
- operator = 'not ' + operator
- return [(left, operator, right)]
- if operator in ('not in', 'not like', 'not ilike'):
- operator = operator[4:]
- return [(left, operator, right)]
- if operator in mapping:
- operator = mapping[operator]
- return [(left, operator, right)]
- return [NOT_OPERATOR, (left, operator, right)]
-
- def distribute_negate(domain):
- """Negate the domain ``subtree`` rooted at domain[0],
- leaving the rest of the domain intact, and return
- (negated_subtree, untouched_domain_rest)
- """
- if is_leaf(domain[0]):
- return negate(domain[0]), domain[1:]
- if domain[0] == AND_OPERATOR:
- done1, todo1 = distribute_negate(domain[1:])
- done2, todo2 = distribute_negate(todo1)
- return [OR_OPERATOR] + done1 + done2, todo2
- if domain[0] == OR_OPERATOR:
- done1, todo1 = distribute_negate(domain[1:])
- done2, todo2 = distribute_negate(todo1)
- return [AND_OPERATOR] + done1 + done2, todo2
- if not domain:
- return []
- if domain[0] != NOT_OPERATOR:
- return [domain[0]] + distribute_not(domain[1:])
- if domain[0] == NOT_OPERATOR:
- done, todo = distribute_negate(domain[1:])
- return done + distribute_not(todo)
+
+ # This is an iterative version of a recursive function that split domain
+ # into subdomains, processes them and combine the results. The "stack" below
+ # represents the recursive calls to be done.
+ result = []
+ stack = [False]
+
+ for token in domain:
+ negate = stack.pop()
+ # negate tells whether the subdomain starting with token must be negated
+ if is_leaf(token):
+ if negate:
+ left, operator, right = token
+ if operator in TERM_OPERATORS_NEGATION:
+ result.append((left, TERM_OPERATORS_NEGATION[operator], right))
+ else:
+ result.append(NOT_OPERATOR)
+ result.append(token)
+ else:
+ result.append(token)
+ elif token == NOT_OPERATOR:
+ stack.append(not negate)
+ elif token in DOMAIN_OPERATORS_NEGATION:
+ result.append(DOMAIN_OPERATORS_NEGATION[token] if negate else token)
+ stack.append(negate)
+ stack.append(negate)
+ else:
+ result.append(token)
+
+ return result
# --------------------------------------------------
@@ -724,6 +729,8 @@ class expression(object):
""" Return a domain implementing the child_of operator for [(left,child_of,ids)],
either as a range using the parent_left/right tree lookup fields
(when available), or as an expanded [(left,in,child_ids)] """
+ if not ids:
+ return FALSE_DOMAIN
if left_model._parent_store and (not left_model.pool._init):
# TODO: Improve where joins are implemented for many with '.', replace by:
# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]