[MERGE] forward port of branch saas-6 up to dade7e3

This commit is contained in:
Christophe Simonis
2015-11-25 13:26:02 +01:00
51 changed files with 394 additions and 228 deletions
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_analytic_default account.analytic.default model_account_analytic_default account.group_account_user 1 0 0 0
3 access_account_analytic_default_analytic account.analytic.default analytic model_account_analytic_default analytic.group_analytic_accounting 1 0 0 0
4 access_account_analytic_default_invoice account.analytic.default invoice model_account_analytic_default account.group_account_invoice 1 1 1 1
5 access_account_analytic_default_salesman account.analytic.default.salesman model_account_analytic_default base.group_sale_salesman 1 1 1 1
@@ -105,6 +105,7 @@ class ir_module_module(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():
+9 -7
View File
@@ -3,8 +3,7 @@
import pytz
from openerp import _, api, fields, models
from openerp.exceptions import UserError
from openerp.exceptions import AccessError, UserError
class event_type(models.Model):
""" Event Type """
@@ -371,11 +370,14 @@ class event_registration(models.Model):
@api.multi
def message_get_suggested_recipients(self):
recipients = super(event_registration, self).message_get_suggested_recipients()
for attendee in self:
if attendee.email:
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
if attendee.partner_id:
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
try:
for attendee in self:
if attendee.partner_id:
attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer'))
elif attendee.email:
attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email'))
except AccessError: # no read access rights -> ignore suggested recipients
pass
return recipients
@api.multi
+1 -5
View File
@@ -166,11 +166,7 @@
<field name="name"/>
</strong>
<ul>
<li id="last_login">
<span t-if="record.last_login.raw_value &amp;&amp; record.last_login.raw_value.is().today()" class="oe_kanban_button" style="font-size: 100%%">
<t t-esc="moment(record.last_login.raw_value).format('LT')"/>
</span>
</li>
<li id="last_login"/>
<li t-if="record.job_id.raw_value"><field name="job_id"/></li>
<li t-if="record.work_location.raw_value"><field name="work_location"/></li>
<li t-if="record.work_email.raw_value"><i class="fa fa-envelope"></i> <field name="work_email"/></li>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_resource_manager hr.employee.resource.manager resource.model_resource_resource base.group_hr_manager 1 1 1 1
3 access_hr_resource_calendar_user hr.employee.resource.calendar.user resource.model_resource_calendar base.group_hr_user 1 1 1 1
4 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
5 access_hr_contract_type_manager hr.contract.type.manager model_hr_contract_type base.group_hr_manager 1 1 1 1
6 access_hr_contract_manager hr.contract.manager model_hr_contract base.group_hr_manager 1 1 1 1
+17 -11
View File
@@ -23,18 +23,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):
+3 -1
View File
@@ -56,7 +56,9 @@ class publisher_warranty_contract(AbstractModel):
"apps": [app['name'] for app in apps],
"enterprise_code": enterprise_code,
}
msg.update(self.pool.get("res.company").read(cr, uid, [1], ["name", "email", "phone"])[0])
if user.partner_id.company_id:
company_id = user.partner_id.company_id.id
msg.update(self.pool["res.company"].read(cr, uid, [company_id], ["name", "email", "phone"])[0])
return msg
def _get_sys_logs(self, cr, uid):
+1 -1
View File
@@ -109,7 +109,7 @@ class MailMail(osv.Model):
def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
if mail_sent is True and mail.statistics_ids:
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'sent': fields.datetime.now()}, context=context)
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'sent': fields.datetime.now(), 'exception': False}, context=context)
elif mail_sent is False and mail.statistics_ids:
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'exception': fields.datetime.now()}, context=context)
return super(MailMail, self)._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
@@ -48,7 +48,6 @@ var PosBaseWidget = Widget.extend({
amount = amount.toFixed(decimals);
return amount;
};
if (typeof amount === 'number') {
amount = round_di(amount,decimals).toFixed(decimals);
amount = formats.format_value(round_di(amount, decimals), { type: 'float', digits: [69, decimals]});
+4 -4
View File
@@ -41,8 +41,8 @@
% endif
</p>
<% set signup_url = object.get_signup_url() %>
% if signup_url and object.amount_total > 0 :
% set signup_url = object.get_signup_url()
% if signup_url and object.amount_total:
<p>
You can access this document and pay online via our Customer Portal:
</p>
@@ -50,7 +50,7 @@
href="${signup_url}">View ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'}</a>
% endif
% if object.paypal_url and object.amount_total > 0 :
% if object.paypal_url and object.amount_total:
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${object.paypal_url}">
@@ -117,7 +117,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:
@@ -76,6 +76,7 @@
cursor: pointer;
box-shadow: 0px 3px rgba(0,0,0,0.07);
transition: background, background-color 300ms ease-in-out;
overflow: hidden;
}
.floor-map .table .table-cover {
display: block;
@@ -240,7 +241,7 @@
left: 50%;
background: black;
width: 20px;
margin-top: -10px;
margin-top: 1px;
margin-left: -10px;
height: 20px;
line-height: 20px;
@@ -30,7 +30,7 @@
<t t-if='!widget.selected'>
<div class='table' t-att-style='widget.table_style_str()'>
<span
t-if="widget.table.shape !== 'round'"
t-if="widget.table.shape"
t-att-class='"table-cover " + (widget.fill >= 1 ? "full" : "")'
t-att-style='"height: " + Math.ceil(widget.fill * 100) + "%;"'
></span>
+8
View File
@@ -349,6 +349,14 @@ class product_attribute_line(osv.osv):
'value_ids': fields.many2many('product.attribute.value', id1='line_id', id2='val_id', string='Attribute Values'),
}
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'])
]
def name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100):
# TDE FIXME: currently overriding the domain; however as it includes a
# search on a m2o and one on a m2m, probably this will quickly become
+1
View File
@@ -265,6 +265,7 @@
<field name="project_id" domain="[('state','not in', ('close', 'cancelled'))]" on_change="onchange_project(project_id)" context="{'default_use_tasks':1}"/>
<field name="user_id"
options='{"no_open": True}'
on_change="onchange_user_id(user_id, context)"
context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/>
<field name="planned_hours" widget="float_time"
groups="project.group_time_work_estimation_tasks"
+6 -4
View File
@@ -216,7 +216,7 @@ class project_issue(osv.Model):
context = dict(context or {})
if vals.get('project_id') and not context.get('default_project_id'):
context['default_project_id'] = vals.get('project_id')
if vals.get('user_id'):
if vals.get('user_id') and not vals.get('date_open'):
vals['date_open'] = fields.datetime.now()
if 'stage_id' in vals:
vals.update(self.onchange_stage_id(cr, uid, None, vals.get('stage_id'), context=context)['value'])
@@ -232,8 +232,8 @@ class project_issue(osv.Model):
vals['date_last_stage_update'] = fields.datetime.now()
if 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
# user_id change: update date_start
if vals.get('user_id'):
# user_id change: update date_open
if vals.get('user_id') and 'date_open' not in vals:
vals['date_open'] = fields.datetime.now()
return super(project_issue, self).write(cr, uid, ids, vals, context)
@@ -427,7 +427,9 @@ class project(osv.Model):
_inherit = "project.project"
def _get_alias_models(self, cr, uid, context=None):
return [('project.task', "Tasks"), ("project.issue", "Issues")]
res = super(project, self)._get_alias_models(cr, uid, context=context)
res.append(("project.issue", "Issues"))
return res
def _issue_count(self, cr, uid, ids, field_name, arg, context=None):
Issue = self.pool['project.issue']
@@ -263,6 +263,9 @@
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="use_tasks"]' position='attributes'>
<attribute name="on_change">on_change_use_tasks_or_issues(use_tasks, use_issues)</attribute>
</xpath>
<xpath expr='//div[@name="options_active"]' position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
+2 -1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import SUPERUSER_ID
from openerp import api, models
from openerp.osv import fields, osv
from openerp.exceptions import UserError
@@ -124,7 +125,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. """
+2 -1
View File
@@ -1528,7 +1528,8 @@ class stock_picking(models.Model):
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."""
-1
View File
@@ -1501,7 +1501,6 @@
<field name="picking_id" string="Reference" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="origin"/>
<field name="partner_id" string="Vendor"/>
<field name="product_id"/>
<field name="product_uom_qty"/>
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
@@ -135,7 +135,7 @@ class survey_mail_compose_message(osv.TransientModel):
'body': wizard.body.replace("__URL__", url),
'body_html': wizard.body.replace("__URL__", url),
'parent_id': None,
'attachment_ids': wizard.attachment_ids or None,
'attachment_ids': wizard.attachment_ids and [(6, 0, wizard.attachment_ids.ids)] or None,
'email_from': wizard.email_from or None,
'auto_delete': True,
}
+2 -2
View File
@@ -317,10 +317,10 @@ var py = {};
} else if (token in symbols) {
var symbol;
// transform 'not in' and 'is not' in a single token
if (token === 'in' && tokens[tokens.length-1].id === 'not') {
if (token === 'in' && tokens.length > 1 && tokens[tokens.length-1].id === 'not') {
symbol = symbols['not in'];
tokens.pop();
} else if (token === 'not' && tokens.length > 0 && tokens[tokens.length-1].id === 'is') {
} else if (token === 'not' && tokens.length > 1 && tokens[tokens.length-1].id === 'is') {
symbol = symbols['is not'];
tokens.pop();
} else {
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="fixed-literal">
<Año t-att-falló="'agüero'"/>
</t>
<result id="fixed-literal"><![CDATA[<Año falló="agüero"></Año>]]></result>
</templates>
+10 -1
View File
@@ -322,7 +322,16 @@ function round_precision (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;
}
+7
View File
@@ -1,3 +1,10 @@
odoo.define_section('eval.basics', ['web.pyeval'], function(test, mock) {
test('not prefix', function (assert) {
assert.ok(py.eval('not False'));
assert.ok(py.eval('not foo', {foo: false}));
assert.ok(py.eval('not a in b', {a: 3, b: [1, 2, 4, 8]}));
});
});
odoo.define_section('eval.types', ['web.pyeval'], function (test, mock) {
function makeTimeCheck (assert, pyeval) {
@@ -532,6 +532,9 @@ var CalendarView = View.extend({
else if (value instanceof Array) {
temp_ret[fieldname] = value[1]; // no name_get to make
}
else if (_.contains(["date", "datetime"], self.fields[fieldname].type)) {
temp_ret[fieldname] = instance.web.format_value(value, self.fields[fieldname]);
}
else {
throw new Error("Incomplete data received from dataset for record " + evt.id);
}
+1 -1
View File
@@ -51,7 +51,7 @@ class Website(openerp.addons.web.controllers.main.Home):
@http.route(website=True, auth="public")
def web_login(self, redirect=None, *args, **kw):
r = super(Website, self).web_login(redirect=redirect, *args, **kw)
if request.params['login_success'] and not redirect:
if not redirect and request.params['login_success']:
if request.registry['res.users'].has_group(request.cr, request.uid, 'base.group_user'):
redirect = '/web?' + request.httprequest.query_string
else:
+2 -1
View File
@@ -275,7 +275,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:
+1 -1
View File
@@ -126,7 +126,7 @@ class view(osv.osv):
qcontext.update(values)
# in edit mode ir.ui.view will tag nodes
if not qcontext.get('translatable'):
if not qcontext.get('translatable') and not qcontext.get('rendering_bundle'):
if qcontext.get('editable'):
context = dict(context, inherit_branding=True)
elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'):
@@ -213,7 +213,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"/>
@@ -276,7 +276,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">
@@ -6,7 +6,7 @@ from openerp.http import request
class MassMailController(http.Controller):
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='none')
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
if mailing.exists():
@@ -19,72 +19,6 @@
<!-- Mails & quote templates is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<record id="sale.email_template_edi_sale" model="mail.template">
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>Hello ${object.partner_id.name},</p>
<p>Here is your ${object.state in ('draft', 'sent') and 'quotation' or 'order confirmation'} from ${object.company_id.name}: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Order number: <strong>${object.name}</strong><br />
&nbsp;&nbsp;Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
&nbsp;&nbsp;Order date: ${object.date_order}<br />
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
% if object.client_order_ref:
&nbsp;&nbsp;Your reference: ${object.client_order_ref}<br />
% endif
% if object.user_id:
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Order%20${object.name}">${object.user_id.name}</a>
% endif
</p>
<p>
You can view your quotation online:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="/quote/${object.id}/${object.access_token}">View ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'}</a>
% if object.paypal_url:
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${object.paypal_url}">
<img class="oe_edi_paypal_button" src="/sale/static/img/btn_paynowcc_lg.gif"/>
</a>
% endif
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
${object.company_id.partner_id.sudo().with_context(show_address=True, html_format=True).name_get()[0][1] | safe}
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
</div>
]]></field>
</record>
<record id="website_quote_template_default" model="sale.quote.template">
<field name="name">Default Template</field>
<field name="number_of_days">30</field>
@@ -189,17 +123,13 @@
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Order%20${object.name}">${object.user_id.name}</a>
% endif
</p>
<% 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:
You can view your quotation online:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="${signup_url}">View ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'}</a>
% endif
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="/quote/${object.id}/${object.access_token}">View ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'}</a>
% if object.paypal_url and object.amount_total > 0 :
% if object.paypal_url and object.amount_total:
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${object.paypal_url}">
+9 -2
View File
@@ -145,7 +145,7 @@ class sale_order(osv.osv):
_columns = {
'access_token': fields.char('Security Token', required=True, copy=False),
'template_id': fields.many2one('sale.quote.template', 'Quotation Template', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
'website_description': fields.html('Description'),
'website_description': fields.html('Description', translate=True),
'options' : fields.one2many('sale.order.option', 'order_id', 'Optional Products Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
'amount_undiscounted': fields.function(_get_total, string='Amount Before Discount', type="float", digits=0),
'quote_viewed': fields.boolean('Quotation Viewed'),
@@ -396,14 +396,17 @@ class sale_order_option(osv.osv):
domain = {'uom_id': [('category_id', '=', product_obj.uom_id.category_id.id)]}
return {'value': vals, 'domain': domain}
# TODO master: to remove, replaced by onchange of the new api
def product_uom_change(self, cr, uid, ids, product, uom_id, context=None):
context = context or {}
if not uom_id:
return {'value': {'price_unit': 0.0, 'uom_id': False}}
return self.on_change_product_id(cr, uid, ids, product, uom_id=uom_id, context=context)
@api.onchange('product_id')
@api.onchange('product_id', 'uom_id')
def _onchange_product_id(self):
if not self.product_id:
return
product = self.product_id.with_context(lang=self.order_id.partner_id.lang)
self.price_unit = product.list_price
self.website_description = product.quote_description or product.website_description
@@ -415,6 +418,10 @@ class sale_order_option(osv.osv):
partner_id = self.order_id.partner_id.id
pricelist = self.order_id.pricelist_id.id
self.price_unit = self.order_id.pricelist_id.price_get(product.id, self.quantity, partner_id)[pricelist]
if self.uom_id and self.uom_id != self.product_id.uom_id:
self.price_unit = self.product_id.uom_id._compute_price(self.price_unit, self.uom_id.id)
domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
return {'domain': domain}
class product_template(osv.Model):
+3 -1
View File
@@ -23,6 +23,8 @@ class PaymentTransaction(orm.Model):
if hasattr(self, tx_find_method_name):
tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context)
if tx and tx.state == 'done' and tx.acquirer_id.auto_confirm == 'at_pay_confirm' and tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']:
self.pool['sale.order'].action_confirm(cr, SUPERUSER_ID, [tx.sale_order_id.id], context=context)
self.pool['sale.order'].action_confirm(cr, SUPERUSER_ID, [tx.sale_order_id.id], context=dict(context, send_email=True))
elif tx and tx.state not in ['cancel'] and tx.sale_order_id and tx.sale_order_id.state in ['draft']:
self.pool['sale.order'].force_quotation_send(cr, SUPERUSER_ID, [tx.sale_order_id.id], context=context)
return res
@@ -28,7 +28,7 @@
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id" groups="product.group_uom" on_change="product_uom_change(product_id, uom_id)"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="website_description" invisible="1"/>
@@ -115,10 +115,10 @@
<page string="Suggested Products">
<field name="options">
<tree string="Sales Quotation Template Lines" editable="bottom">
<field name="product_id" on_change="on_change_product_id(product_id, uom_id)"/>
<field name="product_id"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id" groups="product.group_uom" on_change="product_uom_change(product_id, uom_id)"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="website_description" invisible="1"/>
@@ -1,6 +1,13 @@
odoo.define('website_sale.cart', function (require) {
"use strict";
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", "/web/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);
}
var core = require('web.core');
var _t = core._t;
@@ -241,9 +248,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", "/web/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) {
@@ -279,10 +284,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", "/web/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 () {
+1
View File
@@ -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
+2 -1
View File
@@ -6,9 +6,10 @@ I declare that I am authorized and able to make this agreement and sign this dec
Signed,
Alan Ramos alan.ramos@jarsa.com.mx https://github.com/alan16
Alan Ramos alan.ramos@jarsa.com.mx https://github.com/alan196
List of contributors:
Alan Ramos alan.ramos@jarsa.com.mx https://github.com/alan196
Luis Triana luis.triana@jarsa.com.mx https://github.com/xluiisx
Luis Triana luistriana.28@gmail.com https://github.com/xluiisx
+19
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
Thailand, 2015-10-1
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Khwunchai Jaengsawang khwunchai.j@ku.th https://github.com/khwunchai
+11
View File
@@ -0,0 +1,11 @@
Germany, 01.10.2015
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Michael H. Walther, michaelhwalther@gmail.com https://github.com/michaelhwalther
+11
View File
@@ -0,0 +1,11 @@
Italy, 2015-09-14
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Alex Comba alex.comba@agilebg.com https://github.com/tafaRU
+11
View File
@@ -0,0 +1,11 @@
Belgium, 2015-09-09
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Panagiotis Issaris takis@issaris.org https://github.com/takis
+24
View File
@@ -1634,6 +1634,30 @@ http://localhost:8069/report/html/account.report_invoice/1 (if ``account`` is
installed) and the PDF version through
http://localhost:8069/report/pdf/account.report_invoice/1.
.. _reference/backend/reporting/printed-reports/pdf-without-styles:
.. danger::
If it appears that your PDF report is missing the styles (i.e. the text
appears but the style/layout is different from the html version), probably
your wkhtmltopdf_ process cannot reach your web server to download them.
If you check your server logs and see that the CSS styles are not being
downloaded when generating a PDF report, most surely this is the problem.
The wkhtmltopdf_ process will use the ``web.base.url`` system parameter as
the *root path* to all linked files, but this parameter is automatically
updated each time the Administrator is logged in. If your server resides
behind some kind of proxy, that could not be reachable. You can fix this by
adding one of these system parameters:
- ``report.url``, pointing to an URL reachable from your server
(probably ``http://localhost:8069`` or something similar). It will be
used for this particular purpose only.
- ``web.base.url.freeze``, when set to ``True``, will stop the
automatic updates to ``web.base.url``.
.. exercise:: Create a report for the Session model
For each session, it should display session's name, its start and end,
+6 -5
View File
@@ -216,19 +216,20 @@ Useful Remarks
* Twitter Bootstrap and FontAwesome classes can be used in your report
template
* Local CSS can be put directly in the template
* Global CSS can be inserted in the main report layout by inheriting its
template and inserting your CSS::
<template id="report_saleorder_style" inherit_id="report.layout">
<xpath expr="//style" position="after">
<style type="text/css">
<template id="report_saleorder_style" inherit_id="report.style">
<xpath expr=".">
<t>
.example-css-class {
background-color: red;
}
</style>
</t>
</xpath>
</template>
* If it appears that your PDF report is missing the styles, please check
:ref:`these instructions <reference/backend/reporting/printed-reports/pdf-without-styles>`.
.. _reference/reports/paper_formats:
+4
View File
@@ -8,6 +8,7 @@ import os
import time
import datetime
import dateutil
import pytz
import openerp
from openerp import SUPERUSER_ID
@@ -80,6 +81,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):
+7 -5
View File
@@ -130,8 +130,9 @@ class QWeb(orm.AbstractModel):
_name = 'ir.qweb'
_void_elements = frozenset([
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
u'area', u'base', u'br', u'col', u'embed', u'hr', u'img', u'input',
u'keygen', u'link', u'menuitem', u'meta', u'param', u'source',
u'track', u'wbr'])
_format_regex = re.compile(
'(?:'
# ruby-style pattern
@@ -264,7 +265,7 @@ class QWeb(orm.AbstractModel):
_logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
for (attribute_name, attribute_value) in element.attrib.iteritems():
attribute_name = str(attribute_name)
attribute_name = unicode(attribute_name)
if attribute_name == "groups":
cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
@@ -308,7 +309,7 @@ class QWeb(orm.AbstractModel):
# generated_attributes: generated attributes
# qwebcontext: values
# inner: optional innerXml
name = str(element.tag)
name = unicode(element.tag)
if inner:
g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
else:
@@ -340,7 +341,7 @@ class QWeb(orm.AbstractModel):
for qwebcontext in (name, generated_attributes, inner, name)
)
else:
return "<%s%s/>" % (name, generated_attributes)
return "<%s%s/>" % (name.encode("utf-8"), generated_attributes)
def render_attribute(self, element, name, value, qwebcontext):
return _build_attribute(name, value)
@@ -1119,6 +1120,7 @@ class AssetsBundle(object):
context = self.context.copy()
context['inherit_branding'] = False
context['inherit_branding_auto'] = False
context['rendering_bundle'] = True
self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
self.parse()
+9
View File
@@ -66,6 +66,15 @@ class TestAPI(common.TransactionCase):
self.assertIsRecordset(partners2, 'res.partner')
self.assertEqual(list(partners1), list(partners2))
@mute_logger('openerp.models')
def test_04_query_count(self):
""" Test the search method with count=True. """
count1 = self.registry('res.partner').search(self.cr, self.uid, [], count=True)
count2 = self.env['res.partner'].search([], count=True)
self.assertIsInstance(count1, (int, long))
self.assertIsInstance(count2, (int, long))
self.assertEqual(count1, count2)
@mute_logger('openerp.models')
def test_05_immutable(self):
""" Check that a recordset remains the same, even after updates. """
@@ -1,6 +1,7 @@
import unittest
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
@@ -500,6 +501,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')
+51 -36
View File
@@ -191,13 +191,19 @@ def depends(*args):
return lambda method: decorate(method, '_depends', args)
def returns(model, downgrade=None):
def returns(model, downgrade=None, upgrade=None):
""" Return a decorator for methods that return instances of ``model``.
:param model: a model name, or ``'self'`` for the current model
:param downgrade: a function ``downgrade(value)`` to convert the
record-style ``value`` to a traditional-style output
:param downgrade: a function ``downgrade(self, value, *args, **kwargs)``
to convert the record-style ``value`` to a traditional-style output
:param upgrade: a function ``upgrade(self, value, *args, **kwargs)``
to convert the traditional-style ``value`` to a record-style output
The arguments ``self``, ``*args`` and ``**kwargs`` are the ones passed
to the method in the record-style.
The decorator adapts the method output to the api style: ``id``, ``ids`` or
``False`` for the traditional style, and recordset for the record style::
@@ -219,7 +225,7 @@ def returns(model, downgrade=None):
a decorated existing method will be decorated with the same
``@returns(model)``.
"""
return lambda method: decorate(method, '_returns', (model, downgrade))
return lambda method: decorate(method, '_returns', (model, downgrade, upgrade))
def make_wrapper(decorator, method, old_api, new_api):
@@ -242,30 +248,39 @@ def make_wrapper(decorator, method, old_api, new_api):
def get_downgrade(method):
""" Return a function `downgrade(value)` that adapts ``value`` from
record-style to traditional-style, following the convention of ``method``.
""" Return a function `downgrade(self, value, *args, **kwargs)` that adapts
``value`` from record-style to traditional-style, following the
convention of ``method``.
"""
spec = getattr(method, '_returns', None)
if spec:
model, downgrade = spec
return downgrade or (lambda value: value.ids)
_, downgrade, _ = spec
if downgrade and len(getargspec(downgrade).args) > 1:
return downgrade
elif downgrade:
return lambda self, *args, **kwargs: downgrade(args[0])
else:
return lambda self, *args, **kwargs: args[0].ids
else:
return lambda value: value
return lambda self, *args, **kwargs: args[0]
def get_upgrade(method):
""" Return a function `upgrade(self, value)` that adapts ``value`` from
traditional-style to record-style, following the convention of ``method``.
""" Return a function `upgrade(self, value, *args, **kwargs)` that adapts
``value`` from traditional-style to record-style, following the
convention of ``method``.
"""
spec = getattr(method, '_returns', None)
if spec:
model, downgrade = spec
if model == 'self':
return lambda self, value: self.browse(value)
model, _, upgrade = spec
if upgrade:
return upgrade
elif model == 'self':
return lambda self, *args, **kwargs: self.browse(args[0])
else:
return lambda self, value: self.env[model].browse(value)
return lambda self, *args, **kwargs: self.env[model].browse(args[0])
else:
return lambda self, value: value
return lambda self, *args, **kwargs: args[0]
def get_aggregate(method):
@@ -275,7 +290,7 @@ def get_aggregate(method):
spec = getattr(method, '_returns', None)
if spec:
# value is a list of instances, concatenate them
model, downgrade = spec
model, _, _ = spec
if model == 'self':
return lambda self, value: sum(value, self.browse())
else:
@@ -325,7 +340,7 @@ def model(method):
context, args, kwargs = split(args, kwargs)
recs = self.browse(cr, uid, [], context)
result = method(recs, *args, **kwargs)
return downgrade(result)
return downgrade(recs, result, *args, **kwargs)
return make_wrapper(model, method, old_api, method)
@@ -352,7 +367,7 @@ def multi(method):
context, args, kwargs = split(args, kwargs)
recs = self.browse(cr, uid, ids, context)
result = method(recs, *args, **kwargs)
return downgrade(result)
return downgrade(recs, result, *args, **kwargs)
return make_wrapper(multi, method, old_api, method)
@@ -392,7 +407,7 @@ def one(method):
context, args, kwargs = split(args, kwargs)
recs = self.browse(cr, uid, ids, context)
result = new_api(recs, *args, **kwargs)
return downgrade(result)
return downgrade(recs, result, *args, **kwargs)
def new_api(self, *args, **kwargs):
result = [method(rec, *args, **kwargs) for rec in self]
@@ -415,7 +430,7 @@ def cr(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
result = method(self._model, cr, *args, **kwargs)
return upgrade(self, result)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr, method, method, new_api)
@@ -426,9 +441,9 @@ def cr_context(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
kwargs['context'] = context
result = method(self._model, cr, *args, **kwargs)
return upgrade(self, result)
old_kwargs = dict(kwargs, context=context)
result = method(self._model, cr, *args, **old_kwargs)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_context, method, method, new_api)
@@ -440,7 +455,7 @@ def cr_uid(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
result = method(self._model, cr, uid, *args, **kwargs)
return upgrade(self, result)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_uid, method, method, new_api)
@@ -459,9 +474,9 @@ def cr_uid_context(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
kwargs['context'] = context
result = method(self._model, cr, uid, *args, **kwargs)
return upgrade(self, result)
old_kwargs = dict(kwargs, context=context)
result = method(self._model, cr, uid, *args, **old_kwargs)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_uid_context, method, method, new_api)
@@ -476,7 +491,7 @@ def cr_uid_id(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
return upgrade(self, result)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_uid_id, method, method, new_api)
@@ -500,9 +515,9 @@ def cr_uid_id_context(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
kwargs['context'] = context
result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
return upgrade(self, result)
old_kwargs = dict(kwargs, context=context)
result = [method(self._model, cr, uid, id, *args, **old_kwargs) for id in self.ids]
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_uid_id_context, method, method, new_api)
@@ -517,7 +532,7 @@ def cr_uid_ids(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
result = method(self._model, cr, uid, self.ids, *args, **kwargs)
return upgrade(self, result)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_uid_ids, method, method, new_api)
@@ -543,9 +558,9 @@ def cr_uid_ids_context(method):
def new_api(self, *args, **kwargs):
cr, uid, context = self.env.args
kwargs['context'] = context
result = method(self._model, cr, uid, self.ids, *args, **kwargs)
return upgrade(self, result)
old_kwargs = dict(kwargs, context=context)
result = method(self._model, cr, uid, self.ids, *args, **old_kwargs)
return upgrade(self, result, *args, **kwargs)
return make_wrapper(cr_uid_ids_context, method, method, new_api)
+27 -1
View File
@@ -1636,7 +1636,9 @@ class BaseModel(object):
return len(res)
return res
@api.returns('self')
@api.returns('self',
upgrade=lambda self, value, args, offset=0, limit=None, order=None, count=False: value if count else self.browse(value),
downgrade=lambda self, value, args, offset=0, limit=None, order=None, count=False: value if count else value.ids)
def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
""" search(args[, offset=0][, limit=None][, order=None][, count=False])
@@ -5377,6 +5379,11 @@ class BaseModel(object):
""" Returns a new version of this recordset attached to the provided
environment
.. warning::
The new environment will not benefit from the current
environment's data cache, so later data access may incur extra
delays while re-fetching from the database.
:type env: :class:`~openerp.api.Environment`
"""
return self._browse(env, self._ids)
@@ -5386,6 +5393,25 @@ class BaseModel(object):
Returns a new version of this recordset attached to the provided
user.
By default this returns a `SUPERUSER` recordset, where access control
and record rules are bypassed.
.. note::
Using `sudo` could cause data access to cross the boundaries of
record rules, possibly mixing records that are meant to be
isolated (e.g. records from different companies in multi-company
environments).
It may lead to un-intuitive results in methods which select one
record among many - for example getting the default company, or
selecting a Bill of Materials.
.. note::
Because the record rules and access control will have to be
re-evaluated, the new recordset will not benefit from the current
environment's data cache, so later data access may incur extra
delays while re-fetching from the database.
"""
return self.with_env(self.env(user=user))
+51 -44
View File
@@ -149,6 +149,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)
@@ -245,51 +265,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
# --------------------------------------------------
@@ -706,6 +711,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)]