[MERGE] forward port of branch saas-6 up to dade7e3
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -166,11 +166,7 @@
|
||||
<field name="name"/>
|
||||
</strong>
|
||||
<ul>
|
||||
<li id="last_login">
|
||||
<span t-if="record.last_login.raw_value && 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
|
||||
|
||||
|
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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. """
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;">
|
||||
<strong>REFERENCES</strong><br />
|
||||
Order number: <strong>${object.name}</strong><br />
|
||||
Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
|
||||
Order date: ${object.date_order}<br />
|
||||
% if object.origin:
|
||||
Order reference: ${object.origin}<br />
|
||||
% endif
|
||||
% if object.client_order_ref:
|
||||
Your reference: ${object.client_order_ref}<br />
|
||||
% endif
|
||||
% if object.user_id:
|
||||
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: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <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 @@
|
||||
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}">
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user