merge upstream

This commit is contained in:
Christophe Matthieu 2014-06-19 18:46:49 +02:00
commit 5165b52b43
136 changed files with 518 additions and 344 deletions

View File

@ -295,7 +295,8 @@ class account_invoice(osv.osv):
},
multi='all'),
'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}, track_visibility='always'),
'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}),
'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]},
domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]"),
'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}),
'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), readonly=True, states={'draft':[('readonly',False)]}),
'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean',

View File

@ -741,6 +741,8 @@ class account_move_line(osv.osv):
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None:
context = {}
if context.get('fiscalyear'):
args.append(('period_id.fiscalyear_id', '=', context.get('fiscalyear', False)))
if context and context.get('next_partner_only', False):
if not context.get('partner_id', False):
partner = self.list_partners_to_reconcile(cr, uid, context=context)
@ -823,7 +825,7 @@ class account_move_line(osv.osv):
'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge)
}, context=context)
move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context)
return True
return r_id
def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None):
account_obj = self.pool.get('account.account')

View File

@ -351,7 +351,7 @@
<act_window
id="action_account_items"
name="Journal Items"
context="{'search_default_account_id': [active_id]}"
context="{'search_default_account_id': [active_id], 'fiscalyear': context.get('fiscalyear')}"
res_model="account.move.line"
src_model="account.account"
key2="tree_but_open"/>

View File

@ -99,7 +99,7 @@
</tr>
<tr t-foreach="get_lines_with_out_partner(data['form'])" t-as="not_partner">
<td>
<span t-esc="partner['name']"/>
<span t-esc="not_partner['name']"/>
</td>
<td class="text-right">
<span t-esc="formatLang(not_partner['direction'], currency_obj=res_company.currency_id)"/>

View File

@ -62,9 +62,10 @@ class account_chart(osv.osv_memory):
ORDER BY p.date_stop DESC
LIMIT 1) AS period_stop''', (fiscalyear_id, fiscalyear_id))
periods = [i[0] for i in cr.fetchall()]
if periods and len(periods) > 1:
if periods:
start_period = periods[0]
end_period = periods[1]
if len(periods) > 1:
end_period = periods[1]
res['value'] = {'period_from': start_period, 'period_to': end_period}
else:
res['value'] = {'period_from': False, 'period_to': False}

View File

@ -22,7 +22,6 @@ from dateutil.relativedelta import relativedelta
import datetime
import logging
import time
import traceback
from openerp.osv import osv, fields
from openerp.osv.orm import intersect, except_orm
@ -73,6 +72,7 @@ class account_analytic_invoice_line(osv.osv):
result = {}
res = self.pool.get('product.product').browse(cr, uid, product, context=local_context)
price = False
if price_unit is not False:
price = price_unit
elif pricelist_id:
@ -746,29 +746,32 @@ class account_analytic_account(osv.osv):
contract_ids = ids
else:
contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True), ('type', '=', 'contract')])
for contract in self.browse(cr, uid, contract_ids, context=context):
try:
invoice_values = self._prepare_invoice(cr, uid, contract, context=context)
invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context))
next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
interval = contract.recurring_interval
if contract.recurring_rule_type == 'daily':
new_date = next_date+relativedelta(days=+interval)
elif contract.recurring_rule_type == 'weekly':
new_date = next_date+relativedelta(weeks=+interval)
elif contract.recurring_rule_type == 'monthly':
new_date = next_date+relativedelta(months=+interval)
else:
new_date = next_date+relativedelta(years=+interval)
self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
if automatic:
cr.commit()
except Exception:
if automatic:
cr.rollback()
_logger.error(traceback.format_exc())
else:
raise
if contract_ids:
cr.execute('SELECT company_id, array_agg(id) as ids FROM account_analytic_account WHERE id IN %s GROUP BY company_id', (tuple(contract_ids),))
for company_id, ids in cr.fetchall():
for contract in self.browse(cr, uid, ids, context=dict(context, company_id=company_id, force_company=company_id)):
try:
invoice_values = self._prepare_invoice(cr, uid, contract, context=context)
invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context))
next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d")
interval = contract.recurring_interval
if contract.recurring_rule_type == 'daily':
new_date = next_date+relativedelta(days=+interval)
elif contract.recurring_rule_type == 'weekly':
new_date = next_date+relativedelta(weeks=+interval)
elif contract.recurring_rule_type == 'monthly':
new_date = next_date+relativedelta(months=+interval)
else:
new_date = next_date+relativedelta(years=+interval)
self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context)
if automatic:
cr.commit()
except Exception:
if automatic:
cr.rollback()
_logger.exception('Fail to create recurring invoice for contract %s', contract.code)
else:
raise
return invoice_ids
class account_analytic_account_summary_user(osv.osv):

View File

@ -248,7 +248,7 @@
<page string="Bill Information">
<field name="line_dr_ids" on_change="onchange_price(line_dr_ids, tax_id, partner_id)" context="{'journal_id':journal_id,'partner_id':partner_id}">
<tree string="Expense Lines" editable="bottom">
<field name="account_id" widget="selection" domain="[('user_type.report_type','=','expense'), ('type','!=','view')]"/>
<field name="account_id" domain="[('user_type.report_type','=','expense'), ('type','!=','view')]"/>
<field name="name"/>
<field name="amount"/>
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"/>

View File

@ -1,137 +1,41 @@
#
# Implements encrypting functions.
#
# Copyright (c) 2008, F S 3 Consulting Inc.
#
# Maintainer:
# Alec Joseph Rivera (agi<at>fs3.ph)
# refactored by Antony Lesuisse <al<at>openerp.com>
#
import hashlib
import hmac
import logging
from random import sample
from string import ascii_letters, digits
from passlib.context import CryptContext
import openerp
from openerp.osv import fields, osv
_logger = logging.getLogger(__name__)
magic_md5 = '$1$'
magic_sha256 = '$5$'
def gen_salt(length=8, symbols=None):
if symbols is None:
symbols = ascii_letters + digits
return ''.join(sample(symbols, length))
def md5crypt( raw_pw, salt, magic=magic_md5 ):
""" md5crypt FreeBSD crypt(3) based on but different from md5
The md5crypt is based on Mark Johnson's md5crypt.py, which in turn is
based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp.
Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to
Poul and Mark. -agi
Original license:
* "THE BEER-WARE LICENSE" (Revision 42):
*
* <phk@login.dknet.dk> wrote this file. As long as you retain this
* notice you can do whatever you want with this stuff. If we meet some
* day, and you think this stuff is worth it, you can buy me a beer in
* return.
*
* Poul-Henning Kamp
"""
raw_pw = raw_pw.encode('utf-8')
salt = salt.encode('utf-8')
hash = hashlib.md5()
hash.update( raw_pw + magic + salt )
st = hashlib.md5()
st.update( raw_pw + salt + raw_pw)
stretch = st.digest()
for i in range( 0, len( raw_pw ) ):
hash.update( stretch[i % 16] )
i = len( raw_pw )
while i:
if i & 1:
hash.update('\x00')
else:
hash.update( raw_pw[0] )
i >>= 1
saltedmd5 = hash.digest()
for i in range( 1000 ):
hash = hashlib.md5()
if i & 1:
hash.update( raw_pw )
else:
hash.update( saltedmd5 )
if i % 3:
hash.update( salt )
if i % 7:
hash.update( raw_pw )
if i & 1:
hash.update( saltedmd5 )
else:
hash.update( raw_pw )
saltedmd5 = hash.digest()
itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
rearranged = ''
for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] )
for i in range(4):
rearranged += itoa64[v & 0x3f]
v >>= 6
v = ord( saltedmd5[11] )
for i in range( 2 ):
rearranged += itoa64[v & 0x3f]
v >>= 6
return magic + salt + '$' + rearranged
def sh256crypt(cls, password, salt, magic=magic_sha256):
iterations = 1000
# see http://en.wikipedia.org/wiki/PBKDF2
result = password.encode('utf8')
for i in xrange(cls.iterations):
result = hmac.HMAC(result, salt, hashlib.sha256).digest() # uses HMAC (RFC 2104) to apply salt
result = result.encode('base64') # doesnt seem to be crypt(3) compatible
return '%s%s$%s' % (magic_sha256, salt, result)
default_crypt_context = CryptContext(
# kdf which can be verified by the context. The default encryption kdf is
# the first of the list
['pbkdf2_sha512', 'md5_crypt'],
# deprecated algorithms are still verified as usual, but ``needs_update``
# will indicate that the stored hash should be replaced by a more recent
# algorithm. Passlib 1.6 supports an `auto` value which deprecates any
# algorithm but the default, but Debian only provides 1.5 so...
deprecated=['md5_crypt'],
)
class res_users(osv.osv):
_inherit = "res.users"
def init(self, cr):
_logger.info("Hashing passwords, may be slow for databases with many users...")
cr.execute("SELECT id, password FROM res_users"
" WHERE password IS NOT NULL"
" AND password != ''")
for uid, pwd in cr.fetchall():
self._set_password(cr, openerp.SUPERUSER_ID, uid, pwd)
def set_pw(self, cr, uid, id, name, value, args, context):
if value:
encrypted = md5crypt(value, gen_salt())
cr.execute("update res_users set password='', password_crypt=%s where id=%s", (encrypted, id))
del value
self._set_password(cr, uid, id, value, context=context)
def get_pw( self, cr, uid, ids, name, args, context ):
cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),))
stored_pws = cr.fetchall()
res = {}
for id, stored_pw in stored_pws:
res[id] = stored_pw
return res
return dict(cr.fetchall())
_columns = {
'password': fields.function(get_pw, fnct_inv=set_pw, type='char', string='Password', invisible=True, store=True),
@ -141,27 +45,51 @@ class res_users(osv.osv):
def check_credentials(self, cr, uid, password):
# convert to base_crypt if needed
cr.execute('SELECT password, password_crypt FROM res_users WHERE id=%s AND active', (uid,))
encrypted = None
if cr.rowcount:
stored_password, stored_password_crypt = cr.fetchone()
if stored_password and not stored_password_crypt:
salt = gen_salt()
stored_password_crypt = md5crypt(stored_password, salt)
cr.execute("UPDATE res_users SET password='', password_crypt=%s WHERE id=%s", (stored_password_crypt, uid))
stored, encrypted = cr.fetchone()
if stored and not encrypted:
self._set_password(cr, uid, uid, stored)
try:
return super(res_users, self).check_credentials(cr, uid, password)
except openerp.exceptions.AccessDenied:
# check md5crypt
if stored_password_crypt:
if stored_password_crypt[:len(magic_md5)] == magic_md5:
salt = stored_password_crypt[len(magic_md5):11]
if stored_password_crypt == md5crypt(password, salt):
return
elif stored_password_crypt[:len(magic_md5)] == magic_sha256:
salt = stored_password_crypt[len(magic_md5):11]
if stored_password_crypt == md5crypt(password, salt):
return
# Reraise password incorrect
if encrypted:
valid_pass, replacement = self._crypt_context(cr, uid, uid)\
.verify_and_update(password, encrypted)
if replacement is not None:
self._set_encrypted_password(cr, uid, uid, replacement)
if valid_pass:
return
raise
def _set_password(self, cr, uid, id, password, context=None):
""" Encrypts then stores the provided plaintext password for the user
``id``
"""
encrypted = self._crypt_context(cr, uid, id, context=context).encrypt(password)
self._set_encrypted_password(cr, uid, id, encrypted, context=context)
def _set_encrypted_password(self, cr, uid, id, encrypted, context=None):
""" Store the provided encrypted password to the database, and clears
any plaintext password
:param uid: id of the current user
:param id: id of the user on which the password should be set
"""
cr.execute(
"UPDATE res_users SET password='', password_crypt=%s WHERE id=%s",
(encrypted, id))
def _crypt_context(self, cr, uid, id, context=None):
""" Passlib CryptContext instance used to encrypt and verify
passwords. Can be overridden if technical, legal or political matters
require different kdfs than the provided default.
Requires a CryptContext as deprecation and upgrade notices are used
internally
"""
return default_crypt_context
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -74,7 +74,7 @@ class OAuthLogin(Home):
state = dict(
d=request.session.db,
p=provider['id'],
r=redirect,
r=werkzeug.url_quote_plus(redirect),
)
token = request.params.get('token')
if token:
@ -143,7 +143,7 @@ class OAuthController(http.Controller):
cr.commit()
action = state.get('a')
menu = state.get('m')
redirect = state.get('r')
redirect = werkzeug.url_unquote_plus(state['r']) if state.get('r') else False
url = '/web'
if redirect:
url = redirect

View File

@ -102,7 +102,7 @@ openerp.calendar = function(instance) {
var self = this;
var action_url = '';
action_url = _.str.sprintf('/?db=%s#id=%s&view_type=form&model=calendar.event', db, meeting_id);
action_url = _.str.sprintf('/web?db=%s#id=%s&view_type=form&model=calendar.event', db, meeting_id);
var reload_page = function(){
return location.replace(action_url);

View File

@ -32,7 +32,7 @@ class report_event_registration(osv.osv):
'draft_state': fields.integer(' # No of Draft Registrations', size=20),
'confirm_state': fields.integer(' # No of Confirmed Registrations', size=20),
'seats_max': fields.integer('Max Seats'),
'nbevent': fields.integer('Number Of Events'),
'nbevent': fields.integer('Number of Registrations'),
'event_type': fields.many2one('event.type', 'Event Type'),
'registration_state': fields.selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Attended'), ('cancel', 'Cancelled')], 'Registration State', readonly=True, required=True),
'event_state': fields.selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Event State', readonly=True, required=True),
@ -59,7 +59,7 @@ class report_event_registration(osv.osv):
r.name AS name_registration,
e.company_id AS company_id,
e.date_begin AS event_date,
count(e.id) AS nbevent,
count(r.id) AS nbevent,
CASE WHEN r.state IN ('draft') THEN r.nb_register ELSE 0 END AS draft_state,
CASE WHEN r.state IN ('open','done') THEN r.nb_register ELSE 0 END AS confirm_state,
e.type AS event_type,

View File

@ -1,7 +1,7 @@
.oe_event_date{
border-top-left-radius:3px;
border-top-right-radius:3px;
font-size: 48px;
font-size: 36px;
height: auto;
font-weight: bold;
text-align: center;

View File

@ -245,7 +245,8 @@ class event_ticket(osv.osv):
]
def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
return {'value': {'price': self.pool.get("product.product").browse(cr, uid, product_id).list_price or 0}}
price = self.pool.get("product.product").browse(cr, uid, product_id).list_price if product_id else 0
return {'value': {'price': price}}
class event_registration(osv.osv):

View File

@ -58,7 +58,7 @@ def start_end_date_for_period(period, default_start_date=False, default_end_date
end_date = default_end_date
if start_date and end_date:
return (start_date.strftime(DF), end_date.strftime(DF))
return (datetime.strftime(start_date, DF), datetime.strftime(end_date, DF))
else:
return (start_date, end_date)

View File

@ -699,7 +699,7 @@ class google_calendar(osv.AbstractModel):
for att in att_obj.browse(cr, uid, my_att_ids, context=context):
event = att.event_id
base_event_id = att.google_internal_event_id.split('_')[0]
base_event_id = att.google_internal_event_id.rsplit('_', 1)[0]
if base_event_id not in event_to_synchronize:
event_to_synchronize[base_event_id] = {}
@ -721,7 +721,7 @@ class google_calendar(osv.AbstractModel):
for event in all_event_from_google.values():
event_id = event.get('id')
base_event_id = event_id.split('_')[0]
base_event_id = event_id.rsplit('_', 1)[0]
if base_event_id not in event_to_synchronize:
event_to_synchronize[base_event_id] = {}
@ -786,7 +786,7 @@ class google_calendar(osv.AbstractModel):
if actSrc == 'OE':
self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'GG':
new_google_event_id = event.GG.event['id'].split('_')[1]
new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1]
if 'T' in new_google_event_id:
new_google_event_id = new_google_event_id.replace('T', '')[:-1]
else:
@ -795,7 +795,8 @@ class google_calendar(osv.AbstractModel):
if event.GG.status:
parent_event = {}
if not event_to_synchronize[base_event][0][1].OE.event_id:
event_to_synchronize[base_event][0][1].OE.event_id = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].split('_')[0])], ['event_id'], context=context_novirtual)[0].get('event_id')[0]
main_ev = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], fields=['event_id'], context=context_novirtual)
event_to_synchronize[base_event][0][1].OE.event_id = main_ev[0].get('event_id')[0]
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)

View File

@ -225,7 +225,7 @@ class hr_employee(osv.osv):
"resized as a 128x128px image, with aspect ratio preserved. "\
"Use this field in form views or some kanban views."),
'image_small': fields.function(_get_image, fnct_inv=_set_image,
string="Smal-sized photo", type="binary", multi="_get_image",
string="Small-sized photo", type="binary", multi="_get_image",
store = {
'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
},

View File

@ -40,7 +40,9 @@
<page string="Public Information">
<group>
<group string="Contact Information">
<field name="address_id" on_change="onchange_address_id(address_id)" context="{'show_address': 1}" options='{"always_reload": True, "highlight_first_line": True}'/>
<field name="address_id" on_change="onchange_address_id(address_id)"
context="{'show_address': 1, 'default_customer': False}"
options='{"always_reload": True, "highlight_first_line": True}'/>
<field name="mobile_phone"/>
<field name="work_location"/>
</group>
@ -68,7 +70,9 @@
<field name="otherid" groups="base.group_hr_user"/>
</group>
<group string="Contact Information">
<field name="address_home_id" context="{'show_address': 1}" options='{"always_reload": True, "highlight_first_line": True}'/>
<field name="address_home_id"
context="{'show_address': 1, 'default_customer': False}"
options='{"always_reload": True, "highlight_first_line": True}'/>
</group>
<group string="Status">
<field name="gender"/>

View File

@ -99,7 +99,7 @@ class hr_holidays_status(osv.osv):
for record in self.browse(cr, uid, ids, context=context):
name = record.name
if not record.limit:
name = name + (' (%d/%d)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0))
name = name + (' (%g/%g)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0))
res.append((record.id, name))
return res

View File

@ -141,7 +141,7 @@ class account_invoice(osv.osv):
elif algorithm == 'random':
if not self.check_bbacomm(reference):
base = random.randint(1, 9999999999)
bbacomm = str(base).rjust(7, '0')
bbacomm = str(base).rjust(10, '0')
base = int(bbacomm)
mod = base % 97 or 97
mod = str(mod).rjust(2, '0')

View File

@ -32,7 +32,7 @@ This is the latest UK OpenERP localisation necessary to run OpenERP accounting f
- a few other adaptations""",
'author': 'SmartMode LTD',
'website': 'http://www.smartmode.co.uk',
'depends': ['base_iban', 'base_vat', 'account_chart'],
'depends': ['base_iban', 'base_vat', 'account_chart', 'account_anglo_saxon'],
'data': [
'data/account.account.type.csv',
'data/account.account.template.csv',

View File

@ -28,7 +28,7 @@ United States - Chart of accounts.
==================================
""",
'website': 'http://www.openerp.com',
'depends': ['account_chart'],
'depends': ['account_chart', 'account_anglo_saxon'],
'data': [
'l10n_us_account_type.xml',
'account_chart_template.xml',

View File

@ -176,7 +176,7 @@ class mail_notification(osv.Model):
references = message.parent_id.message_id if message.parent_id else False
# create email values
max_recipients = 100
max_recipients = 50
chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)]
email_ids = []
for chunk in chunks:
@ -188,7 +188,7 @@ class mail_notification(osv.Model):
'references': references,
}
email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context))
if force_send and len(chunks) < 6: # for more than 500 followers, use the queue system
if force_send and len(chunks) < 2: # for more than 50 followers, use the queue system
self.pool.get('mail.mail').send(cr, uid, email_ids, context=context)
return True

View File

@ -211,3 +211,16 @@ class mail_group(osv.Model):
return []
else:
return super(mail_group, self).get_suggested_thread(cr, uid, removed_suggested_threads, context)
def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
res = super(mail_group, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context)
group = self.browse(cr, uid, id, context=context)
res.update({
'headers': {
'Precedence': 'list',
}
})
if group.alias_domain:
res['headers']['List-Id'] = '%s.%s' % (group.alias_name, group.alias_domain)
res['headers']['List-Post'] = '<mailto:%s@%s>' % (group.alias_name, group.alias_domain)
return res

View File

@ -204,12 +204,15 @@ class mail_mail(osv.Model):
"""
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
return {
res = {
'body': body,
'body_alternative': body_alternative,
'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
}
if mail.model and mail.res_id and self.pool.get(mail.model) and hasattr(self.pool[mail.model], 'message_get_email_values'):
res.update(self.pool[mail.model].message_get_email_values(cr, uid, mail.res_id, mail, context=context))
return res
def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
""" Sends the selected emails immediately, ignoring their current
@ -268,6 +271,9 @@ class mail_mail(osv.Model):
# build an RFC2822 email.message.Message object and send it without queuing
res = None
for email in email_list:
email_headers = dict(headers)
if email.get('headers'):
email_headers.update(email['headers'])
msg = ir_mail_server.build_email(
email_from=mail.email_from,
email_to=email.get('email_to'),
@ -282,7 +288,7 @@ class mail_mail(osv.Model):
object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype='html',
subtype_alternative='plain',
headers=headers)
headers=email_headers)
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id,
context=context)

View File

@ -34,6 +34,7 @@ import pytz
import socket
import time
import xmlrpclib
import re
from email.message import Message
from urllib import urlencode
@ -48,6 +49,8 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
mail_header_msgid_re = re.compile('<[^<>]+>')
def decode_header(message, header, separator=' '):
return separator.join(map(decode, filter(None, message.get_all(header, []))))
@ -694,6 +697,16 @@ class mail_thread(osv.AbstractModel):
if record.alias_domain and record.alias_name else False
for record in self.browse(cr, SUPERUSER_ID, ids, context=context)]
def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
""" Temporary method to create custom notification email values for a given
model and document. This should be better to have a headers field on
the mail.mail model, computed when creating the notification email, but
this cannot be done in a stable version.
TDE FIXME: rethink this ulgy thing. """
res = dict()
return res
#------------------------------------------------------
# Mail gateway
#------------------------------------------------------
@ -1301,13 +1314,13 @@ class mail_thread(osv.AbstractModel):
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
if message.get('In-Reply-To'):
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))])
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To'].strip()))])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]
if message.get('References') and 'parent_id' not in msg_dict:
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in',
[x.strip() for x in decode(message['References']).split()])])
msg_list = mail_header_msgid_re.findall(decode(message['References']))
parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', [x.strip() for x in msg_list])])
if parent_ids:
msg_dict['parent_id'] = parent_ids[0]

View File

@ -267,10 +267,7 @@ 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, record_name=False)
if hasattr(self.pool[wizard.model], 'message_new'):
mail_values['model'] = wizard.model
mail_values['res_id'] = res_id
mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False)
# auto deletion of mail_mail
if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete')

View File

@ -84,7 +84,8 @@ class MailMail(osv.Model):
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
res = super(MailMail, self).send_get_email_dict(cr, uid, mail, partner, context=context)
if mail.mailing_id and res.get('body') and res.get('email_to'):
email_to = tools.email_split(res.get('email_to')[0])
emails = tools.email_split(res.get('email_to')[0])
email_to = emails and emails[0] or False
unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context)
if unsubscribe_url:
res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p')

View File

@ -591,6 +591,7 @@
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="sent"/>
<field name="exception"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>

View File

@ -1071,8 +1071,8 @@ class mrp_production(osv.osv):
return False
# Take routing location as a Source Location.
source_location_id = production.location_src_id.id
if production.bom_id.routing_id and production.bom_id.routing_id.location_id:
source_location_id = production.bom_id.routing_id.location_id.id
if production.routing_id and production.routing_id.location_id:
source_location_id = production.routing_id.location_id.id
destination_location_id = production.product_id.property_stock_production.id
if not source_location_id:

View File

@ -678,6 +678,17 @@ class product_template(osv.osv):
if not context or "create_product_product" not in context:
self.create_variant_ids(cr, uid, [product_template_id], context=context)
self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context)
# TODO: this is needed to set given values to first variant after creation
# these fields should be moved to product as lead to confusion
related_vals = {}
if vals.get('ean13'):
related_vals['ean13'] = vals['ean13']
if vals.get('default_code'):
related_vals['default_code'] = vals['default_code']
if related_vals:
self.write(cr, uid, product_template_id, related_vals, context=context)
return product_template_id
def write(self, cr, uid, ids, vals, context=None):

View File

@ -27,7 +27,7 @@ class account_invoice(osv.Model):
template_values = Composer.onchange_template_id(
cr, uid, composer_id, line.product_id.email_template_id.id, 'comment', 'account.invoice', invoice.id
)['value']
template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', '[]')]
template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', [])]
Composer.write(cr, uid, [composer_id], template_values, context=context)
Composer.send_mail(cr, uid, [composer_id], context=context)
return True

View File

@ -50,8 +50,8 @@
<field name="user_id" ref="base.user_demo"/>
<field name="alias_model">project.task</field>
<field name="message_follower_ids" eval="[(6, 0, [
ref('base.user_root'),
ref('base.user_demo')])]"/>
ref('base.partner_root'),
ref('base.partner_demo')])]"/>
</record>
<!-- We assign after so that default values applies -->

View File

@ -27,11 +27,11 @@ class project_configuration(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
'module_project_mrp': fields.boolean('Generate tasks from sale orders',
'module_sale_service': fields.boolean('Generate tasks from sale orders',
help='This feature automatically creates project tasks from service products in sale orders. '
'More precisely, tasks are created for procurement lines with product of type \'Service\', '
'procurement method \'Make to Order\', and supply method \'Manufacture\'.\n'
'-This installs the module project_mrp.'),
'-This installs the module sale_service.'),
'module_pad': fields.boolean("Use integrated collaborative note pads on task",
help='Lets the company customize which Pad installation should be used to link to new pads '
'(for example: http://ietherpad.com/).\n'

View File

@ -33,8 +33,8 @@
<label for="module_project_timesheet"/>
</div>
<div>
<field name="module_project_mrp" class="oe_inline"/>
<label for="module_project_mrp"/>
<field name="module_sale_service" class="oe_inline"/>
<label for="module_sale_service"/>
</div>
<div>
<field name="module_pad" class="oe_inline"/>

View File

@ -137,7 +137,7 @@ class project_work(osv.osv):
amount = vals_line['unit_amount']
prod_id = vals_line['product_id']
unit = False
timeline_id = timesheet_obj.create(cr, uid, vals=vals_line, context=context)
timeline_id = timesheet_obj.create(cr, uid, vals_line, context=context)
# Compute based on pricetype
amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id,

View File

@ -134,14 +134,15 @@ class Report(osv.Model):
website = None
if request and hasattr(request, 'website'):
website = request.website
values.update({
'time': time,
'translate_doc': translate_doc,
'editable': True, # Will active inherit_branding
'user': user,
'res_company': user.company_id,
'website': website,
})
values.update(
time=time,
translate_doc=translate_doc,
editable=True, # Will active inherit_branding
user=user,
res_company=user.company_id,
website=website,
editable_no_editor=True,
)
return view_obj.render(cr, uid, template, values, context=context)
#--------------------------------------------------------------------------

View File

@ -122,7 +122,7 @@ Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
def onchange_task_work(self, cr, uid, ids, task_work, context=None):
return {'value': {
'module_project_timesheet': task_work,
'module_project_mrp': task_work,
'module_sale_service': task_work,
}}
def onchange_timesheet(self, cr, uid, ids, timesheet, context=None):

View File

@ -686,7 +686,7 @@ class sale_order(osv.osv):
def procurement_needed(self, cr, uid, ids, context=None):
#when sale is installed only, there is no need to create procurements, that's only
#further installed modules (project_mrp, sale_stock) that will change this.
#further installed modules (sale_service, sale_stock) that will change this.
sale_line_obj = self.pool.get('sale.order.line')
res = []
for order in self.browse(cr, uid, ids, context=context):
@ -839,7 +839,7 @@ class sale_order_line(osv.osv):
def need_procurement(self, cr, uid, ids, context=None):
#when sale is installed only, there is no need to create procurements, that's only
#further installed modules (project_mrp, sale_stock) that will change this.
#further installed modules (sale_service, sale_stock) that will change this.
return False
def _amount_line(self, cr, uid, ids, field_name, arg, context=None):

View File

@ -19,6 +19,6 @@
#
##############################################################################
import project_mrp
import models
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -50,8 +50,8 @@ completed.
'website': 'http://www.openerp.com',
'images': ['images/product.jpeg', 'images/task_from_SO.jpeg'],
'depends': ['project', 'procurement', 'sale', 'procurement_jit'],
'data': ['project_mrp_view.xml'], #'process/project_mrp_process.xml'
'demo': ['project_mrp_demo.xml'],
'data': ['views/sale_service_view.xml'],
'demo': ['demo/sale_service_demo.xml'],
'test': ['test/project_task_procurement.yml'],
'installable': True,
'auto_install': False,

View File

@ -0,0 +1 @@
import sale_service

View File

@ -151,7 +151,7 @@ class sale_order_line(osv.osv):
_inherit = 'sale.order.line'
def need_procurement(self, cr, uid, ids, context=None):
#when sale is installed alone, there is no need to create procurements, but with project_mrp
#when sale is installed alone, there is no need to create procurements, but with sale_service
#we must create a procurement for each service that has the auto_create_task boolean set to True.
for line in self.browse(cr, uid, ids, context=context):
if line.product_id and line.product_id.type == 'service' and line.product_id.auto_create_task:

View File

@ -14,7 +14,7 @@
<record id="view_product_task_form" model="ir.ui.view">
<field name="name">product.form.view.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.view_template_property_form"/>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<group name="procurement_uom" position="after">
<group string="Project Management Information" attrs="{'invisible': [('type', '!=', 'service')]}">
@ -34,8 +34,8 @@
</field>
</field>
</record>
<record id="view_project_mrp_inherit_form2" model="ir.ui.view">
<field name="name">project.mrp.form.view.inherit</field>
<record id="view_sale_service_inherit_form2" model="ir.ui.view">
<field name="name">sale.service.form.view.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">

View File

@ -35,7 +35,7 @@ class sale_configuration(osv.osv_memory):
help='Lets you transfer the entries under tasks defined for Project Management to '
'the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways '
'and to automatically creates project tasks from procurement lines.\n'
'-This installs the modules project_timesheet and project_mrp.'),
'-This installs the modules project_timesheet and sale_service.'),
'default_order_policy': fields.selection(
[('manual', 'Invoice based on sales orders'), ('picking', 'Invoice based on deliveries')],
'The default invoicing method is', default_model='sale.order',
@ -50,7 +50,7 @@ class sale_configuration(osv.osv_memory):
implied_group='sale.group_mrp_properties',
help="Allows you to tag sales order lines with properties."),
'module_project_timesheet': fields.boolean("Project Timesheet"),
'module_project_mrp': fields.boolean("Project MRP"),
'module_sale_service': fields.boolean("Sale Service"),
'group_route_so_lines': fields.boolean('Choose MTO, drop shipping,... on sales order lines',
implied_group='sale_stock.group_route_so_lines',
help="Allows you to choose a delivery route on sales order lines"),
@ -63,7 +63,7 @@ class sale_configuration(osv.osv_memory):
def default_get(self, cr, uid, fields, context=None):
res = super(sale_configuration, self).default_get(cr, uid, fields, context)
# task_work, time_unit depend on other fields
res['task_work'] = res.get('module_project_mrp') and res.get('module_project_timesheet')
res['task_work'] = res.get('module_sale_service') and res.get('module_project_timesheet')
return res
def get_default_sale_config(self, cr, uid, ids, context=None):

View File

@ -38,7 +38,7 @@
</xpath>
<group name='default_options' position="after">
<field name="module_project_timesheet" invisible="1"/>
<field name="module_project_mrp" invisible="1"/>
<field name="module_sale_service" invisible="1"/>
</group>
<div name='warehouse_features' position='inside'>
<div name='default_picking_policy' attrs="{'invisible':[('group_invoice_deli_orders','=',False)]}">

View File

@ -78,7 +78,7 @@ class procurement_rule(osv.osv):
class procurement_order(osv.osv):
_inherit = "procurement.order"
_columns = {
'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with project_mrp
'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with sale_service
'partner_dest_id': fields.many2one('res.partner', 'Customer Address', help="In case of dropshipping, we need to know the destination address more precisely"),
'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"),
'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"),

View File

@ -749,7 +749,7 @@
</tree>
</field>
-->
<field name="move_lines" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'view_move_picking_tree', 'default_picking_type_id': picking_type_id,'default_picking_id': active_id}"/>
<field name="move_lines" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'stock.view_move_picking_tree', 'default_picking_type_id': picking_type_id,'default_picking_id': active_id}"/>
<field name="pack_operation_exist" invisible="1"/>
<field name="note" placeholder="Add an internal note..." class="oe_inline"/>
</page>
@ -1288,7 +1288,7 @@
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain" eval="['|','&amp;',('picking_id','=',False),('location_dest_id.usage', 'in', ['customer','supplier']),'&amp;',('picking_id','!=',False),('picking_id.picking_type_id.code','=','outgoing')]"/>
<field name="domain" eval="[('picking_id.picking_type_id.code','=','incoming'), ('location_id.usage','!=','internal'), ('location_dest_id.usage', '=', 'internal')]"/>
<field name="view_id" ref="view_move_tree_reception_picking"/>
<field name="context">{'product_receive': True, 'search_default_future': True}</field>
<field name="help" type="html">

View File

@ -2373,7 +2373,7 @@
}
.openerp .oe_fileupload .oe_add button.oe_attach .oe_e {
position: relative;
top: -1px;
top: -10px;
left: -9px;
}
.openerp .oe_fileupload .oe_add input.oe_form_binary_file {
@ -3324,6 +3324,9 @@ body.oe_single_form .oe_single_form_container {
.openerp_ie ul.oe_form_status li.oe_active > .arrow span, .openerp_ie ul.oe_form_status_clickable li.oe_active > .arrow span {
background-color: #729fcf !important;
}
}
.openerp_ie .oe_webclient {
height: auto !important;
@media print {
.openerp {

View File

@ -1939,7 +1939,7 @@ $sheet-padding: 16px
text-shadow: none
.oe_e
position: relative
top: -1px
top: -10px
left: -9px
input.oe_form_binary_file
display: inline-block
@ -2691,6 +2691,8 @@ body.oe_single_form
> .arrow span
background-color: #729fcf !important
.oe_webclient
height: auto !important
// }}}
// @media print {{{

View File

@ -233,7 +233,8 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
value = value.replace(instance.web._t.database.parameters.thousands_sep, "");
} while(tmp !== value);
tmp = Number(value);
if (isNaN(tmp))
// do not accept not numbers or float values
if (isNaN(tmp) || tmp % 1)
throw new Error(_.str.sprintf(_t("'%s' is not a correct integer"), value));
return tmp;
case 'float':
@ -268,6 +269,11 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
case 'datetime':
var datetime = Date.parseExact(
value, (date_pattern + ' ' + time_pattern));
if (datetime !== null)
return instance.web.datetime_to_str(datetime);
datetime = Date.parseExact(value.replace(/\d+/g, function(m){
return m.length === 1 ? "0" + m : m ;
}), (date_pattern + ' ' + time_pattern));
if (datetime !== null)
return instance.web.datetime_to_str(datetime);
datetime = Date.parse(value);
@ -276,6 +282,11 @@ instance.web.parse_value = function (value, descriptor, value_if_empty) {
throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value));
case 'date':
var date = Date.parseExact(value, date_pattern);
if (date !== null)
return instance.web.date_to_str(date);
date = Date.parseExact(value.replace(/\d+/g, function(m){
return m.length === 1 ? "0" + m : m ;
}), date_pattern);
if (date !== null)
return instance.web.date_to_str(date);
date = Date.parse(value);

Some files were not shown because too many files have changed in this diff Show More