[MERGE] forward port of branch saas-5 up to b1c0bc0

This commit is contained in:
Christophe Simonis 2014-06-03 20:02:00 +02:00
commit a5908c5812
40 changed files with 297 additions and 111 deletions

4
.gitignore vendored
View File

@ -3,6 +3,7 @@ _build/
# dotfiles
.*
!.gitignore
# compiled python files
*.py[co]
# setup.py egg_info
@ -12,7 +13,8 @@ _build/
# hg stuff
*.orig
status
# odoo filestore
openerp/filestore
# generated for windows installer?
install/win32/*.bat
install/win32/meta.py

View File

@ -11,6 +11,7 @@ from openerp import SUPERUSER_ID
from openerp import http
from openerp.http import request
from openerp.addons.web.controllers.main import db_monodb, ensure_db, set_cookie_and_redirect, login_and_redirect
from openerp.addons.auth_signup.controllers.main import AuthSignupHome as Home
from openerp.modules.registry import RegistryManager
from openerp.tools.translate import _
@ -44,7 +45,7 @@ def fragment_to_query_string(func):
#----------------------------------------------------------
# Controller
#----------------------------------------------------------
class OAuthLogin(openerp.addons.web.controllers.main.Home):
class OAuthLogin(Home):
def list_providers(self):
try:
provider_obj = request.registry.get('auth.oauth.provider')
@ -67,10 +68,13 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home):
return providers
def get_state(self, provider):
redirect = request.params.get('redirect', 'web')
if not redirect.startswith(('//', 'http://', 'https://')):
redirect = '%s%s' % (request.httprequest.url_root, redirect)
state = dict(
d=request.session.db,
p=provider['id'],
r=request.httprequest.full_path
r=redirect,
)
token = request.params.get('token')
if token:
@ -141,8 +145,7 @@ class OAuthController(http.Controller):
menu = state.get('m')
redirect = state.get('r')
url = '/web'
if redirect and not redirect.startswith('/auth_oauth/signin') and \
(not redirect.startswith('/web/login') or 'redirect' in urlparse.urlsplit(redirect).query):
if redirect:
url = redirect
elif action:
url = '/web#action=%s' % action

View File

@ -6,6 +6,7 @@ import urllib2
import simplejson
import openerp
from openerp.addons.auth_signup.res_users import SignupError
from openerp.osv import osv, fields
from openerp import SUPERUSER_ID
@ -55,14 +56,37 @@ class res_users(osv.Model):
This method can be overridden to add alternative signin methods.
"""
oauth_uid = validation['user_id']
user_ids = self.search(cr, uid, [("oauth_uid", "=", oauth_uid), ('oauth_provider_id', '=', provider)])
if not user_ids:
raise openerp.exceptions.AccessDenied()
assert len(user_ids) == 1
user = self.browse(cr, uid, user_ids[0], context=context)
user.write({'oauth_access_token': params['access_token']})
return user.login
try:
oauth_uid = validation['user_id']
user_ids = self.search(cr, uid, [("oauth_uid", "=", oauth_uid), ('oauth_provider_id', '=', provider)])
if not user_ids:
raise openerp.exceptions.AccessDenied()
assert len(user_ids) == 1
user = self.browse(cr, uid, user_ids[0], context=context)
user.write({'oauth_access_token': params['access_token']})
return user.login
except openerp.exceptions.AccessDenied, access_denied_exception:
if context and context.get('no_user_creation'):
return None
state = simplejson.loads(params['state'])
token = state.get('t')
oauth_uid = validation['user_id']
email = validation.get('email', 'provider_%s_user_%s' % (provider, oauth_uid))
name = validation.get('name', email)
values = {
'name': name,
'login': email,
'email': email,
'oauth_provider_id': provider,
'oauth_uid': oauth_uid,
'oauth_access_token': params['access_token'],
'active': True,
}
try:
_, login, _ = self.signup(cr, uid, values, token, context=context)
return login
except SignupError:
raise access_denied_exception
def auth_oauth(self, cr, uid, provider, params, context=None):
# Advice by Google (to avoid Confused Deputy Problem)

View File

@ -1273,7 +1273,7 @@ class calendar_event(osv.Model):
if data.get('count'):
data['end_type'] = 'count'
else:
data['end_type'] = 'final_date'
data['end_type'] = 'end_date'
return data
def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):

View File

@ -126,10 +126,10 @@
<field name="use_leads"/><label for="use_leads" string="Leads"/>
<field name="use_opportunities" class="oe_inline"/><label for="use_opportunities"/>
</xpath>
<xpath expr="//group/field[@name='active']" position="inside">
<xpath expr="//group[@name='left']" position="after">
<group>
<label for="alias_name" string="Email Alias"
attrs="p'invisible': [('alias_domain', '=', False)]}"/>
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<div name="alias_def"
attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_id" class="oe_read_only oe_inline"

View File

@ -74,7 +74,7 @@ class sale_order(osv.Model):
if not grid_id:
raise osv.except_osv(_('No Grid Available!'), _('No grid matching for this carrier!'))
if order.state != 'draft':
if order.state not in ('draft', 'sent'):
raise osv.except_osv(_('Order not in Draft State!'), _('The order state have to be draft to add delivery lines.'))
grid = grid_obj.browse(cr, uid, grid_id, context=context)

View File

@ -39,16 +39,15 @@ instance.edi.EdiImport = instance.web.Widget.extend({
});
}
else {
$('<div>').dialog({
modal: true,
title: 'Import Successful!',
buttons: {
Ok: function() {
$(this).dialog("close");
window.location = "/";
new instance.web.Dialog(this,{
title: 'Import Successful!',
buttons: {
Ok: function() {
this.parents('.modal').modal('hide');
window.location = "/";
}
}
}
}).html(_t('The document has been successfully imported!'));
},$('<div>').html(_t('The document has been successfully imported!'))).open();
}
},
on_imported_error: function(response){
@ -58,13 +57,12 @@ instance.edi.EdiImport = instance.web.Widget.extend({
msg += "\n " + _t("Reason:") + response.data.message;
}
var params = {error: response, message: msg};
$(instance.web.qweb.render("CrashManager.warning", params)).dialog({
title: _t("Document Import Notification"),
modal: true,
buttons: {
Ok: function() { $(this).dialog("close"); }
}
});
new instance.web.Dialog(this,{
title: _t("Document Import Notification"),
buttons: {
Ok: function() { this.parents('.modal').modal('hide');}
}
},$(instance.web.qweb.render("CrashManager.warning", params))).open();
}
});

View File

@ -159,7 +159,11 @@ class event_event(osv.osv):
help="The maximum registration level is equal to the sum of the maximum registration of event ticket." +
"If you have too much registrations you are not able to confirm your event. (0 to ignore this rule )",
type='integer',
readonly=True),
readonly=True,
store={
'event.event': (lambda self, cr, uid, ids, c = {}: ids, ['event_ticket_ids'], 20),
'event.event.ticket': (_get_ticket_events, ['seats_max'], 10),
}),
'seats_available': fields.function(Event._get_seats, oldname='register_avail', string='Available Seats',
type='integer', multi='seats_reserved',
store={

View File

@ -333,7 +333,8 @@ class gamification_goal(osv.Model):
if definition.computation_mode == 'sum':
field_name = definition.field_id.name
res = obj.read_group(cr, uid, domain, [field_name], [field_name], context=context)
# TODO for master: group on user field in batch mode
res = obj.read_group(cr, uid, domain, [field_name], [], context=context)
new_value = res and res[0][field_name] or 0.0
else: # computation mode = count

View File

@ -141,7 +141,7 @@ class hr_timesheet_sheet(osv.osv):
return {
sheet_id: {
'timesheet_activity_count': Timesheet.search_count(cr,uid, [('sheet_id','=', sheet_id)], context=context),
'attendance_count': Attendance.search_count(cr,uid, [('sheed_id', '=', sheet_id)], context=context)
'attendance_count': Attendance.search_count(cr,uid, [('sheet_id', '=', sheet_id)], context=context)
}
for sheet_id in ids
}

View File

@ -714,7 +714,7 @@ class mail_thread(osv.AbstractModel):
s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context))
def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None):
def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False, context=None):
""" Verify route validity. Check and rules:
1 - if thread_id -> check that document effectively exists; otherwise
fallback on a message_new by resetting thread_id
@ -835,6 +835,9 @@ class mail_thread(osv.AbstractModel):
_create_bounce_email()
return ()
if not model and not thread_id and not alias and not allow_private:
return ()
return (model, thread_id, route[2], route[3], route[4])
def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
@ -886,22 +889,23 @@ class mail_thread(osv.AbstractModel):
thread_references = references or in_reply_to
# 1. message is a reply to an existing message (exact match of message_id)
ref_match = thread_references and tools.reference_re.search(thread_references)
msg_references = thread_references.split()
mail_message_ids = mail_msg_obj.search(cr, uid, [('message_id', 'in', msg_references)], context=context)
if mail_message_ids:
if ref_match and mail_message_ids:
original_msg = mail_msg_obj.browse(cr, SUPERUSER_ID, mail_message_ids[0], context=context)
model, thread_id = original_msg.model, original_msg.res_id
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, model, thread_id, custom_values, uid)
route = self.message_route_verify(
cr, uid, message, message_dict,
(model, thread_id, custom_values, uid, None),
update_author=True, assert_model=True, create_fallback=True, context=context)
return route and [route] or []
update_author=True, assert_model=False, create_fallback=True, context=context)
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, model, thread_id, custom_values, uid)
return [route]
# 2. message is a reply to an existign thread (6.1 compatibility)
ref_match = thread_references and tools.reference_re.search(thread_references)
if ref_match:
reply_thread_id = int(ref_match.group(1))
reply_model = ref_match.group(2) or fallback_model
@ -919,14 +923,15 @@ class mail_thread(osv.AbstractModel):
('res_id', '=', thread_id),
], context=context)
if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, model, thread_id, custom_values, uid)
route = self.message_route_verify(
cr, uid, message, message_dict,
(model, thread_id, custom_values, uid, None),
update_author=True, assert_model=True, create_fallback=True, context=context)
return route and [route] or []
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, model, thread_id, custom_values, uid)
return [route]
# 2. Reply to a private message
if in_reply_to:
@ -936,12 +941,14 @@ class mail_thread(osv.AbstractModel):
], limit=1, context=context)
if mail_message_ids:
mail_message = mail_msg_obj.browse(cr, uid, mail_message_ids[0], context=context)
_logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, mail_message.id, custom_values, uid)
route = self.message_route_verify(cr, uid, message, message_dict,
(mail_message.model, mail_message.res_id, custom_values, uid, None),
update_author=True, assert_model=True, create_fallback=True, context=context)
return route and [route] or []
update_author=True, assert_model=True, create_fallback=True, allow_private=True, context=context)
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, mail_message.id, custom_values, uid)
return [route]
# 3. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
@ -969,11 +976,12 @@ class mail_thread(osv.AbstractModel):
user_id = uid
_logger.info('No matching user_id for the alias %s', alias.alias_name)
route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)
_logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
email_from, email_to, message_id, route)
route = self.message_route_verify(cr, uid, message, message_dict, route,
update_author=True, assert_model=True, create_fallback=True, context=context)
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
email_from, email_to, message_id, route)
routes.append(route)
return routes
@ -987,15 +995,16 @@ class mail_thread(osv.AbstractModel):
thread_id = int(thread_id)
except:
thread_id = False
_logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
route = self.message_route_verify(cr, uid, message, message_dict,
(fallback_model, thread_id, custom_values, uid, None),
update_author=True, assert_model=True, context=context)
if route:
_logger.info(
'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
return [route]
# AssertionError if no routes found and if no bounce occured
# ValueError if no routes found and if no bounce occured
raise ValueError(
'No possible route found for incoming message from %s to %s (Message-Id %s:). '
'Create an appropriate mail.alias or force the destination model.' %
@ -1165,7 +1174,14 @@ class mail_thread(osv.AbstractModel):
body = u''
if save_original:
attachments.append(('original_email.eml', message.as_string()))
if not message.is_multipart() or 'text/' in message.get('content-type', ''):
# Be careful, content-type may contain tricky content like in the
# following example so test the MIME type with startswith()
#
# Content-Type: multipart/related;
# boundary="_004_3f1e4da175f349248b8d43cdeb9866f1AMSPR06MB343eurprd06pro_";
# type="text/html"
if not message.is_multipart() or message.get('content-type', '').startswith("text/"):
encoding = message.get_content_charset()
body = message.get_payload(decode=True)
body = tools.ustr(body, encoding, errors='replace')

View File

@ -267,7 +267,10 @@ class mail_compose_message(osv.TransientModel):
# mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model:
# always keep a copy, reset record name (avoid browsing records)
mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False)
mail_values.update(notification=True, record_name=False)
if hasattr(self.pool[wizard.model], 'message_new'):
mail_values['model'] = wizard.model
mail_values['res_id'] = res_id
# auto deletion of mail_mail
if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete')

View File

@ -67,7 +67,8 @@ class MassMailController(http.Controller):
contact_ids = Contacts.search(cr, SUPERUSER_ID, [('list_id', '=', int(list_id)), ('email', '=', email)], context=context)
if not contact_ids:
Contacts.name_create(cr, SUPERUSER_ID, email, context=context)
contact_ng = Contacts.name_create(cr, SUPERUSER_ID, email, context=context)
Contacts.write(cr, SUPERUSER_ID, [contact_ng[0]], {'list_id': int(list_id)}, context=context)
# add email to session
request.session['mass_mailing_email'] = email
return True

View File

@ -31,6 +31,63 @@
<field name="opt_out" eval="True"/>
</record>
<!-- Demo newsletter template -->
<!--Email template -->
<record id="newsletter_template" model="email.template">
<field name="name">Newsletter</field>
<field name="subject">Newsletter</field>
<field name="model_id" ref="mass_mailing.model_mail_mass_mailing_contact"/>
<field name="use_default_to" eval="True"/>
<field name="auto_delete" eval="True"/>
<field name="body_html"><![CDATA[<div data-snippet-id="big-picture" style="padding:0px; margin:0px">
<table cellpadding="0" cellspacing="0" style="margin:10px 0px 0px;vertical-align:top;padding:0px;font-family:arial;font-size:12px;color:rgb(51,51,51)">
<tbody>
<tr>
<td style="width:600px" valign="top">
<h2 style="text-align: center; padding:0px 5px">A Punchy Headline</h2>
</td>
</tr>
<tr>
<td style="width:600px" valign="top"><img src="/website/static/src/img/big_picture.png" style="display:block;border:none;min-height:250px;margin:0 auto;" width="500"></td>
</tr>
<tr>
<td style="width:600px" valign="top">
<p style="text-align: center; overflow:hidden"></p>
<h3 style="text-align: center; padding:0px 5px">A Small Subtitle for ${object.name}</h3>
<p></p>
<p style="text-align: center; overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
</tr>
</tbody>
</table>
</div>
<div data-snippet-id="three-columns" style="padding:0px; margin:0px">
<table cellpadding="0" cellspacing="0" style="margin:10px 0px 0px;vertical-align:top;padding:0px;font-family:arial;font-size:12px;color:rgb(51,51,51)">
<tbody>
<tr>
<td style="width:300px" valign="top"><img src="/website/static/src/img/desert_thumb.jpg" style="display:block;border:none;min-height:50px" width="275"></td>
<td style="width:300px" valign="top"><img src="/website/static/src/img/deers_thumb.jpg" style="display:block;border:none;min-height:50px" width="275"></td>
</tr>
<tr>
<td style="width:300px" valign="top">
<h3 style="text-align: center; padding:0px 5px">Feature One</h3>
<p style="overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
<td style="width:300px" valign="top">
<h3 style="text-align: center; padding:0px 5px">Feature Two</h3>
<p style="overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
</tr>
</tbody>
</table>
</div>]]></field>
</record>
<!-- Create campaign and mailings -->
<record id="mass_mail_category_1" model="mail.mass_mailing.category">
<field name="name">Marketing</field>

View File

@ -532,8 +532,12 @@ class MassMailing(osv.Model):
#------------------------------------------------------
def get_recipients(self, cr, uid, mailing, context=None):
domain = eval(mailing.mailing_domain)
res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
if mailing.mailing_domain:
domain = eval(mailing.mailing_domain)
res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
else:
res_ids = []
domain = [('id', 'in', res_ids)]
# randomly choose a fragment
if mailing.contact_ab_pc < 100:
@ -567,6 +571,7 @@ class MassMailing(osv.Model):
'composition_mode': 'mass_mail',
'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
'same_thread': mailing.reply_to_mode == 'thread',
}
if mailing.reply_to_mode == 'email':
composer_values['reply_to'] = mailing.reply_to

View File

@ -18,9 +18,12 @@
.attr("disabled", data.is_subscriber && data.email.length ? "disabled" : false);
self.$target.attr("data-subscribe", data.is_subscriber ? 'on' : 'off');
self.$target.find('a.js_subscribe_btn')
.val(data.email ? data.email : "")
.attr("disabled", data.is_subscriber && data.email.length ? "disabled" : false);
self.$target.removeClass("hidden");
if (data.is_subscriber) {
self.$target.find('.js_subscribe_btn').addClass('hidden');
self.$target.find('.js_subscribed_btn').removeClass('hidden');
}
});
// not if editable mode to allow designer to edit alert field

View File

@ -20,6 +20,7 @@
placeholder="your email..."/>
<span class="input-group-btn">
<a href="#" class="btn btn-primary js_subscribe_btn">Subscribe</a>
<a href="#" class="btn btn-success js_subscribed_btn hidden" disabled="disabled">Thanks</a>
</span>
<div class="alert alert-success hidden">Thanks for your subscription!</div>
</div>

View File

@ -43,10 +43,15 @@ class MailComposeMessage(osv.TransientModel):
}, context=context)
mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context)
for res_id in res_ids:
res[res_id]['mailing_id'] = mass_mailing.id
res[res_id]['statistics_ids'] = [(0, 0, {
'model': wizard.model,
'res_id': res_id,
'mass_mailing_id': mass_mailing.id,
})]
res[res_id].update({
'mailing_id': mass_mailing.id,
'statistics_ids': [(0, 0, {
'model': wizard.model,
'res_id': res_id,
'mass_mailing_id': mass_mailing.id,
})],
# email-mode: keep original message for routing
'notification': mass_mailing.reply_to_mode == 'thread',
'auto_delete': True,
})
return res

View File

@ -276,7 +276,8 @@ class procurement_order(osv.osv):
@param cr: The current row, from the database cursor,
@param uid: The current user ID for security checks
@param ids: List of selected IDs
@param use_new_cursor: False or the dbname
@param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
This is appropriate for batch jobs only.
@param context: A standard dictionary for contextual values
@return: Dictionary of values
'''
@ -284,7 +285,7 @@ class procurement_order(osv.osv):
context = {}
try:
if use_new_cursor:
cr = openerp.registry(use_new_cursor).cursor()
cr = openerp.registry(cr.dbname).cursor()
# Run confirmed procurements
while True:

View File

@ -74,7 +74,7 @@
<field name="sequence"/>
<field colspan="4" name="name"/>
<field name="product_id"/>
<field name="product_tmpl_id" groups="product.group_product_variant"/>
<field name="product_tmpl_id"/>
<field name="categ_id"/>
<field name="min_quantity"/>
<field name="base"/>
@ -92,7 +92,7 @@
<group col="4">
<field name="product_id" on_change="product_id_change(product_id)"/>
<field name="product_tmpl_id" groups="product.group_product_variant"/>
<field name="product_tmpl_id"/>
<field name="categ_id"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<field name="min_quantity"/>

View File

@ -202,6 +202,8 @@ class product_uom(osv.osv):
return {}
def write(self, cr, uid, ids, vals, context=None):
if isinstance(ids, (int, long)):
ids = [ids]
if 'category_id' in vals:
for uom in self.browse(cr, uid, ids, context=context):
if uom.category_id.id != vals['category_id']:
@ -344,6 +346,8 @@ class product_attribute_value(osv.osv):
return result
def _set_price_extra(self, cr, uid, id, name, value, args, context=None):
if context is None:
context = {}
if 'active_id' not in context:
return None
p_obj = self.pool['product.attribute.price']
@ -351,7 +355,7 @@ class product_attribute_value(osv.osv):
if p_ids:
p_obj.write(cr, uid, p_ids, {'price_extra': value}, context=context)
else:
p_obj.create(cr, uid, p_ids, {
p_obj.create(cr, uid, {
'product_tmpl_id': context['active_id'],
'value_id': id,
'price_extra': value,
@ -458,6 +462,8 @@ class product_template(osv.osv):
def _set_standard_price(self, cr, uid, product_tmpl_id, value, context=None):
''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
if context is None:
context = {}
price_history_obj = self.pool['product.price.history']
user_company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
company_id = context.get('force_company', user_company)
@ -533,7 +539,6 @@ class product_template(osv.osv):
"resized as a 64x64px image, with aspect ratio preserved. "\
"Use this field anywhere a small image is required."),
'packaging_ids' : fields.one2many('product.packaging', 'product_tmpl_id', 'Logistical Units',
help="Gives the different ways to package the same product. This has no impact on the picking order and is mainly used if you use the EDI module."),
'seller_ids': fields.one2many('product.supplierinfo', 'product_tmpl_id', 'Supplier'),
'seller_delay': fields.related('seller_ids','delay', type='integer', string='Supplier Lead Time',
help="This is the average delay in days between the purchase order confirmation and the reception of goods for this product and for the default supplier. It is used by the scheduler to order requests based on reordering delays."),
@ -674,7 +679,7 @@ class product_template(osv.osv):
def write(self, cr, uid, ids, vals, context=None):
''' Store the standard price change in order to be able to retrieve the cost of a product template for a given date'''
if isinstance(id, (int, long)):
if isinstance(ids, (int, long)):
ids = [ids]
if 'uom_po_id' in vals:
new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_po_id'], context=context)

View File

@ -213,6 +213,10 @@ FaceTime HD Camera, 1.2 MP Photos</field>
<field name="attribute_line_ids" eval="[(6,0,[ref('product.product_attribute_line_1'), ref('product.product_attribute_line_2'), ref('product.product_attribute_line_3')])]"/>
</record>
<record id="product_product_4d" model="product.product">
<field name="active" eval="False"/>
</record>
<record id="product_attribute_price_1" model="product.attribute.price">
<field name="product_tmpl_id" ref="product_product_4_product_template"/>
<field name="value_id" ref="product_attribute_value_2"/>

View File

@ -270,7 +270,7 @@
</form>
<field name="name" position="replace">
<field name="name" attrs="{'invisible': [('id', '!=', False)]}"/>
<field name="product_tmpl_id" class="oe_inline" readonly="1" attrs="{'invisible': [('id', '=', False)]}"/>
<field name="product_tmpl_id" class="oe_inline" readonly="1" attrs="{'invisible': [('id', '=', False)], 'required': [('id', '!=', False)]}"/>
</field>
<xpath expr="//div[@class='oe_title']" position="inside">
<field name="attribute_value_ids" widget="many2many_tags"/>

View File

@ -661,6 +661,8 @@ class purchase_order(osv.osv):
_('You must first cancel all invoices related to this purchase order.'))
self.pool.get('account.invoice') \
.signal_invoice_cancel(cr, uid, map(attrgetter('id'), purchase.invoice_ids))
self.pool['purchase.order.line'].write(cr, uid, [l.id for l in purchase.order_line],
{'state': 'cancel'})
self.write(cr, uid, ids, {'state': 'cancel'})
self.set_order_line_status(cr, uid, ids, 'cancel', context=context)
self.signal_purchase_cancel(cr, uid, ids)
@ -982,6 +984,9 @@ class purchase_order_line(osv.osv):
return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
def unlink(self, cr, uid, ids, context=None):
for line in self.browse(cr, uid, ids, context=context):
if line.state not in ['draft', 'cancel']:
raise osv.except_osv(_('Invalid Action!'), _('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
procurement_obj = self.pool.get('procurement.order')
procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context)
if procurement_ids_to_cancel:

View File

@ -538,14 +538,18 @@ class sale_order(osv.osv):
if grouped:
res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
invoice_ref = ''
origin_ref = ''
for o, l in val:
invoice_ref += o.name + '|'
invoice_ref += (o.client_order_ref or o.name) + '|'
origin_ref += (o.origin or o.name) + '|'
self.write(cr, uid, [o.id], {'state': 'progress'})
cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
#remove last '|' in invoice_ref
if len(invoice_ref) >= 1:
if len(invoice_ref) >= 1:
invoice_ref = invoice_ref[:-1]
invoice.write(cr, uid, [res], {'origin': invoice_ref, 'name': invoice_ref})
if len(origin_ref) >= 1:
origin_ref = origin_ref[:-1]
invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
else:
for order, il in val:
res = self._make_invoice(cr, uid, order, il, context=context)

View File

@ -126,7 +126,7 @@
<div name="options_active"></div>
</div>
<group>
<group>
<group name="left">
<field name="user_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'base.group_sale_salesman_all_leads']}"/>
<field name="code"/>
<field name="parent_id"/>

View File

@ -271,7 +271,8 @@ class procurement_order(osv.osv):
@param cr: The current row, from the database cursor,
@param uid: The current user ID for security checks
@param ids: List of selected IDs
@param use_new_cursor: False or the dbname
@param use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
This is appropriate for batch jobs only.
@param context: A standard dictionary for contextual values
@return: Dictionary of values
'''
@ -280,7 +281,7 @@ class procurement_order(osv.osv):
context = {}
try:
if use_new_cursor:
cr = openerp.registry(use_new_cursor).cursor()
cr = openerp.registry(cr.dbname).cursor()
move_obj = self.pool.get('stock.move')
@ -333,15 +334,14 @@ class procurement_order(osv.osv):
def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
'''
Create procurement based on Orderpoint
use_new_cursor: False or the dbname
@return: Dictionary of values
"""
:param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement.
This is appropriate for batch jobs only.
'''
if context is None:
context = {}
if use_new_cursor:
cr = openerp.registry(use_new_cursor).db.cursor()
cr = openerp.registry(cr.dbname).db.cursor()
orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
procurement_obj = self.pool.get('procurement.order')

View File

@ -2276,6 +2276,9 @@ class stock_move(osv.osv):
#Check moves that were pushed
if move.move_dest_id.state in ('waiting', 'confirmed'):
# FIXME is opw 607970 still present with new WMS?
# (see commits 1ef2c181033bd200906fb1e5ce35e234bf566ac6
# and 41c5ceb8ebb95c1b4e98d8dd1f12b8e547a24b1d)
other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
('move_dest_id', '=', move.move_dest_id.id)], context=context)
#If no other moves for the move that got pushed:

View File

@ -779,9 +779,9 @@ instance.web_kanban.KanbanGroup = instance.web.Widget.extend({
});
var am = instance.webclient.action_manager;
var form = am.dialog_widget.views.form.controller;
form.on("on_button_cancel", am.dialog, am.dialog.close);
form.on("on_button_cancel", am.dialog, function() { return am.dialog.$dialog_box.modal('hide'); });
form.on('record_saved', self, function() {
am.dialog.close();
am.dialog.$dialog_box.modal('hide');
self.view.do_reload();
});
},

View File

@ -17,3 +17,8 @@
transform-origin: 0 0;
margin: 0 0px -300px 0;
}
.o_mail_body {
width: 620px;
margin-left: 16px;
}

View File

@ -14,6 +14,9 @@
$('#email_designer').show();
$('#email_template').hide();
$(".js_content", $(this).parent()).children().clone().appendTo('#email_body');
$(".js_content", $(this).parent()).children().clone().appendTo('#email_body_html');
$('#email_body').addClass('oe_dirty');
$('#email_body_html').addClass('oe_dirty');
openerp.website.editor_bar.edit();
event.preventDefault();

View File

@ -64,11 +64,11 @@
</div>
<hr/>
<!-- body fields -->
<div t-if="body_field == 'body_html'">
<div t-field="record.body_html" id="email_body_html"/>
<div t-if="body_field == 'body_html'" class="col-sm-offset-2">
<div t-field="record.body_html" id="email_body_html" class="o_mail_body"/>
</div>
<div t-if="body_field == 'body'">
<div t-field="record.body" id="email_body"/>
<div t-if="body_field == 'body'" class="col-sm-offset-2">
<div t-field="record.body" id="email_body" class="o_mail_body"/>
</div>
</div>
</div>

View File

@ -19,6 +19,7 @@
#
##############################################################################
from openerp import tools
from openerp.osv import osv, fields
class product_style(osv.Model):

View File

@ -289,6 +289,13 @@
.css_attribute_color input {
margin: 8px;
height: 13px;
opacity: 0;
}
.css_attribute_color.active {
border: 3px ridge #66ee66;
}
.css_attribute_color.active input {
margin: 6px;
}
.js_add_cart_variants option.css_not_available {

View File

@ -251,6 +251,12 @@
input
margin: 8px
height: 13px
opacity: 0
&.active
border: 3px ridge #66ee66
&.active input
margin: 6px
.js_add_cart_variants
option.css_not_available
color: #ccc

View File

@ -62,6 +62,11 @@ $(document).ready(function () {
var dec = value % 1;
$price.html(value + (dec < 0.01 ? ".00" : (dec < 1 ? "0" : "") ));
});
// hightlight selected color
$('.css_attribute_color input').on('change', function (ev) {
$('.css_attribute_color').removeClass("active");
$('.css_attribute_color:has(input:checked)').addClass("active");
});
var $form_var = $('form.js_add_cart_variants');
var variant_ids = $form_var.data("attribute_value_ids");

View File

@ -297,8 +297,8 @@
</t>
<t t-if="a.type == 'color'">
<t t-foreach="a.value_ids" t-as="v">
<label class="css_attribute_color"
t-attf-style="background-color:#{v.color or v.name}">
<label t-attf-style="background-color:#{v.color or v.name}"
t-attf-class="css_attribute_color #{'active' if v.id in attrib_set else ''}">
<input type="checkbox"
name="attrib"
t-att-value="'%s,%s' % (a.id,v.id)"
@ -481,8 +481,8 @@
<ul class="nav nav-pills nav-stacked">
<t t-set="inc" t-value="0"/>
<t t-foreach="variant_id.value_ids" t-as="value_id">
<label class="css_attribute_color"
t-attf-style="background-color:#{value_id.color or value_id.name}">
<label t-attf-style="background-color:#{value_id.color or value_id.name}"
t-attf-class="css_attribute_color #{'active' if not inc else ''}">
<input type="radio"
t-att-checked="'checked' if not inc else ''"
t-att-name="'attribute-%s' % variant_id.attribute_id.id"
@ -529,13 +529,12 @@
<template id="product_attributes" inherit_id="website_sale.product" optional="enabled" name="Product attributes">
<xpath expr="//p[@t-field='product.description_sale']" position="after">
<hr t-if="product.attribute_line_ids"/>
<hr t-if="sum([(1 if len(l.value_ids)==1 else 0) for l in product.attribute_line_ids])"/>
<p class="text-muted">
<t t-foreach="product.attribute_line_ids" t-as="variant_id">
<t t-set="inc" t-value="0"/>
<span t-field="variant_id.attribute_id"/>:
<t t-foreach="variant_id.value_ids" t-as="value_id"><t t-if="inc">,</t> <span t-field="value_id.name"/><t t-set="inc" t-value="inc+1"/></t>
<br/>
<t t-if="len(variant_id.value_ids)==1">
<span t-field="variant_id.attribute_id"/>: <span t-field="variant_id.value_ids[0].name"/><br/>
</t>
</t>
</p>
</xpath>

View File

@ -108,6 +108,7 @@
"access_multi_company_default manager","multi_company_default Manager","model_multi_company_default","group_erp_manager",1,1,1,1
"access_ir_filter all","ir_filters all","model_ir_filters",,1,1,1,1
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
"access_ir_config_parameter_system","ir_config_parameter_system","model_ir_config_parameter","group_system",1,1,1,1
"access_ir_mail_server","ir_mail_server","model_ir_mail_server","group_system",1,1,1,1
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
"access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
108 access_multi_company_default manager multi_company_default Manager model_multi_company_default group_erp_manager 1 1 1 1
109 access_ir_filter all ir_filters all model_ir_filters 1 1 1 1
110 access_ir_config_parameter ir_config_parameter model_ir_config_parameter 1 0 0 0
111 access_ir_config_parameter_system ir_config_parameter_system model_ir_config_parameter group_system 1 1 1 1
112 access_ir_mail_server ir_mail_server model_ir_mail_server group_system 1 1 1 1
113 access_ir_actions_client ir_actions_client all model_ir_actions_client 1 0 0 0
114 access_ir_needaction_mixin ir_needaction_mixin model_ir_needaction_mixin 1 1 1 1

View File

@ -665,35 +665,49 @@ class EndPoint(object):
def routing_map(modules, nodb_only, converters=None):
routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
def get_subclasses(klass):
def valid(c):
return c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules
subclasses = klass.__subclasses__()
result = []
for subclass in subclasses:
if valid(subclass):
result.extend(get_subclasses(subclass))
if not result and valid(klass):
result = [klass]
return result
uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
for module in modules:
if module not in controllers_per_module:
continue
for _, cls in controllers_per_module[module]:
subclasses = cls.__subclasses__()
subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
subclasses = uniq(c for c in get_subclasses(cls) if c is not cls)
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
if inspect.ismethod(mv) and hasattr(mv, 'routing'):
members = inspect.getmembers(o, inspect.ismethod)
for _, mv in members:
if hasattr(mv, 'routing'):
routing = dict(type='http', auth='user', methods=None, routes=None)
methods_done = list()
routing_type = None
# update routing attributes from subclasses(auth, methods...)
for claz in reversed(mv.im_class.mro()):
fn = getattr(claz, mv.func_name, None)
if fn and hasattr(fn, 'routing') and fn not in methods_done:
methods_done.append(fn)
routing.update(fn.routing)
if not nodb_only or nodb_only == (routing['auth'] == "none"):
if not nodb_only or routing['auth'] == "none":
assert routing['routes'], "Method %r has not route defined" % mv
endpoint = EndPoint(mv, routing)
for url in routing['routes']:
if routing.get("combine", False):
# deprecated
# deprecated v7 declaration
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
if url.endswith("/") and len(url) > 1:
url = url[: -1]

View File

@ -2449,7 +2449,7 @@ class BaseModel(object):
fetched_data = cr.dictfetchall()
if not groupby_fields:
return {r.pop('id'): r for r in fetched_data}
return fetched_data
many2onefields = [gb['field'] for gb in annotated_groupbys if gb['type'] == 'many2one']
if many2onefields: