|
-
+
|
diff --git a/addons/account/wizard/account_chart.py b/addons/account/wizard/account_chart.py
index 38df2f7484d..de652a947f0 100644
--- a/addons/account/wizard/account_chart.py
+++ b/addons/account/wizard/account_chart.py
@@ -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}
diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py
index 519660fb708..9550f77bcab 100644
--- a/addons/account_analytic_analysis/account_analytic_analysis.py
+++ b/addons/account_analytic_analysis/account_analytic_analysis.py
@@ -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):
diff --git a/addons/account_voucher/voucher_sales_purchase_view.xml b/addons/account_voucher/voucher_sales_purchase_view.xml
index d0064d6336c..f8c2d65f63b 100644
--- a/addons/account_voucher/voucher_sales_purchase_view.xml
+++ b/addons/account_voucher/voucher_sales_purchase_view.xml
@@ -248,7 +248,7 @@
-
+
diff --git a/addons/auth_crypt/auth_crypt.py b/addons/auth_crypt/auth_crypt.py
index 4651d27fe7d..6c9deb51e92 100644
--- a/addons/auth_crypt/auth_crypt.py
+++ b/addons/auth_crypt/auth_crypt.py
@@ -1,137 +1,41 @@
-#
-# Implements encrypting functions.
-#
-# Copyright (c) 2008, F S 3 Consulting Inc.
-#
-# Maintainer:
-# Alec Joseph Rivera (agifs3.ph)
-# refactored by Antony Lesuisse 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):
- *
- * 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:
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index 99f134a5cbd..7652420d7c6 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -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
diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js
index eed958e20d9..37669840818 100644
--- a/addons/calendar/static/src/js/base_calendar.js
+++ b/addons/calendar/static/src/js/base_calendar.js
@@ -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);
diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py
index 641c490cabb..e38bf973cdf 100644
--- a/addons/crm_claim/crm_claim.py
+++ b/addons/crm_claim/crm_claim.py
@@ -45,13 +45,10 @@ class crm_claim_stage(osv.osv):
help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
'case_default': fields.boolean('Common to All Teams',
help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
- 'fold': fields.boolean('Hide in Views when Empty',
- help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
}
_defaults = {
'sequence': lambda *args: 1,
- 'fold': False,
}
class crm_claim(osv.osv):
diff --git a/addons/crm_claim/crm_claim_data.xml b/addons/crm_claim/crm_claim_data.xml
index fca7ac51178..86cf988ff45 100644
--- a/addons/crm_claim/crm_claim_data.xml
+++ b/addons/crm_claim/crm_claim_data.xml
@@ -61,7 +61,6 @@
Rejected
29
-
diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml
index 43df3d069bf..6180b85660d 100644
--- a/addons/crm_claim/crm_claim_view.xml
+++ b/addons/crm_claim/crm_claim_view.xml
@@ -51,7 +51,6 @@
-
diff --git a/addons/event/report/report_event_registration.py b/addons/event/report/report_event_registration.py
index e76b5c60d1f..c4a4dfd6b2d 100644
--- a/addons/event/report/report_event_registration.py
+++ b/addons/event/report/report_event_registration.py
@@ -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,
diff --git a/addons/event/static/src/css/event.css b/addons/event/static/src/css/event.css
index ff5bfaba19b..53c03cc03f0 100644
--- a/addons/event/static/src/css/event.css
+++ b/addons/event/static/src/css/event.css
@@ -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;
diff --git a/addons/event_sale/event_sale.py b/addons/event_sale/event_sale.py
index ed99e6a39cc..69ad0cb6af6 100644
--- a/addons/event_sale/event_sale.py
+++ b/addons/event_sale/event_sale.py
@@ -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):
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py
index ddc08336293..faed62f8da9 100644
--- a/addons/gamification/models/challenge.py
+++ b/addons/gamification/models/challenge.py
@@ -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)
diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py
index 543c81977aa..dd1d45c05fe 100644
--- a/addons/google_calendar/google_calendar.py
+++ b/addons/google_calendar/google_calendar.py
@@ -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)
diff --git a/addons/hr/hr.py b/addons/hr/hr.py
index 85366adfe3b..f4719f572f0 100644
--- a/addons/hr/hr.py
+++ b/addons/hr/hr.py
@@ -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),
},
diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml
index 09e7373384e..0c356706407 100644
--- a/addons/hr/hr_view.xml
+++ b/addons/hr/hr_view.xml
@@ -40,7 +40,9 @@
-
+
@@ -68,7 +70,9 @@
-
+
diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py
index 3d467e0b065..ed25b75aa83 100644
--- a/addons/hr_holidays/hr_holidays.py
+++ b/addons/hr_holidays/hr_holidays.py
@@ -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
diff --git a/addons/l10n_be_coda/__openerp__.py b/addons/l10n_be_coda/__openerp__.py
index ea1ce59eb88..6ecdb04f1d6 100644
--- a/addons/l10n_be_coda/__openerp__.py
+++ b/addons/l10n_be_coda/__openerp__.py
@@ -59,25 +59,7 @@ A removal of one object in the CODA processing results in the removal of the
associated objects. The removal of a CODA File containing multiple Bank
Statements will also remove those associated statements.
-The following reconciliation logic has been implemented in the CODA processing:
--------------------------------------------------------------------------------
- 1) The Company's Bank Account Number of the CODA statement is compared against
- the Bank Account Number field of the Company's CODA Bank Account
- configuration records (whereby bank accounts defined in type='info'
- configuration records are ignored). If this is the case an 'internal transfer'
- transaction is generated using the 'Internal Transfer Account' field of the
- CODA File Import wizard.
- 2) As a second step the 'Structured Communication' field of the CODA transaction
- line is matched against the reference field of in- and outgoing invoices
- (supported : Belgian Structured Communication Type).
- 3) When the previous step doesn't find a match, the transaction counterparty is
- located via the Bank Account Number configured on the OpenERP Customer and
- Supplier records.
- 4) In case the previous steps are not successful, the transaction is generated
- by using the 'Default Account for Unrecognized Movement' field of the CODA
- File Import wizard in order to allow further manual processing.
-
-In stead of a manual adjustment of the generated Bank Statements, you can also
+Instead of a manual adjustment of the generated Bank Statements, you can also
re-import the CODA after updating the OpenERP database with the information that
was missing to allow automatic reconciliation.
diff --git a/addons/l10n_be_coda/l10n_be_coda.py b/addons/l10n_be_coda/l10n_be_coda.py
index 0dfa9bf9db4..c4cd8ab7c2b 100644
--- a/addons/l10n_be_coda/l10n_be_coda.py
+++ b/addons/l10n_be_coda/l10n_be_coda.py
@@ -28,46 +28,4 @@ class account_bank_statement(osv.osv):
}
-class account_bank_statement_line(osv.osv):
- _inherit = 'account.bank.statement.line'
- _columns = {
- 'coda_account_number': fields.char('Account Number', help="The Counter Party Account Number")
- }
-
- def create(self, cr, uid, data, context=None):
- """
- This function creates a Bank Account Number if, for a bank statement line,
- the partner_id field and the coda_account_number field are set,
- and the account number does not exist in the database
- """
- if 'partner_id' in data and data['partner_id'] and 'coda_account_number' in data and data['coda_account_number']:
- acc_number_ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', data['coda_account_number'])])
- if len(acc_number_ids) == 0:
- try:
- type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
- type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
- self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': data['coda_account_number'], 'partner_id': data['partner_id'], 'state': type_id.code}, context=context)
- except ValueError:
- pass
- return super(account_bank_statement_line, self).create(cr, uid, data, context=context)
-
- def write(self, cr, uid, ids, vals, context=None):
- super(account_bank_statement_line, self).write(cr, uid, ids, vals, context)
- """
- Same as create function above, but for write function
- """
- if 'partner_id' in vals:
- for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context):
- if line.coda_account_number:
- acc_number_ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', line.coda_account_number)])
- if len(acc_number_ids) == 0:
- try:
- type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
- type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
- self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': line.coda_account_number, 'partner_id': vals['partner_id'], 'state': type_id.code}, context=context)
- except ValueError:
- pass
- return True
-
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/l10n_be_coda/l10n_be_coda_demo.xml b/addons/l10n_be_coda/l10n_be_coda_demo.xml
index 63436a60a92..ce44539a715 100644
--- a/addons/l10n_be_coda/l10n_be_coda_demo.xml
+++ b/addons/l10n_be_coda/l10n_be_coda_demo.xml
@@ -32,5 +32,27 @@
+
+
+
+
+
+
+
+ draft
+ out_invoice
+
+
+ bba
+ +++240/2838/42818+++
+
+
+ Otpez Laptop without OS
+
+ 608.89
+ 10
+
+
+
diff --git a/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt b/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt
index bc3af59e322..d0add859663 100644
--- a/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt
+++ b/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt
@@ -4,7 +4,7 @@
2200010000 GKCCBEBB 1 0
2300010000BE41063012345610 PARTNER 1 0 1
3100010001OL44483FW SCTOFBIONLO001010001001PARTNER 1 0 0
-2100020000OL4414AC8BOVSOVSOVERS00000000030444501101110015000002010237 11011113501 0
+2100020000OL4414AC8BOVSOVSOVERS0000000003044450110111001500001101240283842818 11011113501 0
2200020000 BBRUBEBB 1 0
2300020000BE61310126985517 PARTNER 2 0 1
3100020001OL4414AC8BOVSOVSOVERS001500001001PARTNER 2 1 0
diff --git a/addons/l10n_be_coda/wizard/account_coda_import.py b/addons/l10n_be_coda/wizard/account_coda_import.py
index e50f8537881..cac4cce452c 100644
--- a/addons/l10n_be_coda/wizard/account_coda_import.py
+++ b/addons/l10n_be_coda/wizard/account_coda_import.py
@@ -291,79 +291,38 @@ class account_coda_import(osv.osv_memory):
if 'counterpartyAddress' in line and line['counterpartyAddress'] != '':
note.append(_('Counter Party Address') + ': ' + line['counterpartyAddress'])
line['name'] = "\n".join(filter(None, [line['counterpartyName'], line['communication']]))
- partner = None
partner_id = None
- invoice = False
+ structured_com = ""
+ bank_account_id = False
if line['communication_struct'] and 'communication_type' in line and line['communication_type'] == '101':
- ids = self.pool.get('account.invoice').search(cr, uid, [('reference', '=', line['communication']), ('reference_type', '=', 'bba')])
-
-# Gère les communications structurées
-# TODO : à faire primer sur resolution_proposition : si la communication indique une facture, on la sélectionne
-
-# if ids:
-# invoice = self.pool.get('account.invoice').browse(cr, uid, ids[0])
-# partner = invoice.partner_id
-# partner_id = partner.id
-# if invoice.type in ['in_invoice', 'in_refund'] and line['debit'] == '1':
-# line['transaction_type'] = 'supplier'
-# elif invoice.type in ['out_invoice', 'out_refund'] and line['debit'] == '0':
-# line['transaction_type'] = 'customer'
-# line['account'] = invoice.account_id.id
-# line['reconcile'] = False
-# if invoice.type in ['in_invoice', 'out_invoice']:
-# iml_ids = self.pool.get('account.move.line').search(cr, uid, [('move_id', '=', invoice.move_id.id), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)])
-# if iml_ids:
-# line['reconcile'] = iml_ids[0]
-# if line['reconcile']:
-# voucher_vals = {
-# 'type': line['transaction_type'] == 'supplier' and 'payment' or 'receipt',
-# 'name': line['name'],
-# 'partner_id': partner_id,
-# 'journal_id': statement['journal_id'].id,
-# 'account_id': statement['journal_id'].default_credit_account_id.id,
-# 'company_id': statement['journal_id'].company_id.id,
-# 'currency_id': statement['journal_id'].company_id.currency_id.id,
-# 'date': line['entryDate'],
-# 'amount': abs(line['amount']),
-# 'period_id': statement['period_id'],
-# 'invoice_id': invoice.id,
-# }
-# context['invoice_id'] = invoice.id
-# voucher_vals.update(self.pool.get('account.voucher').onchange_partner_id(cr, uid, [],
-# partner_id=partner_id,
-# journal_id=statement['journal_id'].id,
-# amount=abs(line['amount']),
-# currency_id=statement['journal_id'].company_id.currency_id.id,
-# ttype=line['transaction_type'] == 'supplier' and 'payment' or 'receipt',
-# date=line['transactionDate'],
-# context=context
-# )['value'])
-# line_drs = []
-# for line_dr in voucher_vals['line_dr_ids']:
-# line_drs.append((0, 0, line_dr))
-# voucher_vals['line_dr_ids'] = line_drs
-# line_crs = []
-# for line_cr in voucher_vals['line_cr_ids']:
-# line_crs.append((0, 0, line_cr))
-# voucher_vals['line_cr_ids'] = line_crs
-# line['voucher_id'] = self.pool.get('account.voucher').create(cr, uid, voucher_vals, context=context)
+ structured_com = line['communication']
if 'counterpartyNumber' in line and line['counterpartyNumber']:
ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', str(line['counterpartyNumber']))])
- if ids and len(ids) > 0:
- partner = self.pool.get('res.partner.bank').browse(cr, uid, ids[0], context=context).partner_id
- partner_id = partner.id
+ if ids:
+ bank_account_id = ids[0]
+ partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
+ else:
+ #create the bank account, not linked to any partner. The reconciliation will link the partner manually
+ #chosen at the bank statement final confirmation time.
+ try:
+ type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
+ type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
+ bank_code = type_id.code
+ except ValueError:
+ bank_code = 'bank'
+ bank_account_id = self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': str(line['counterpartyNumber']), 'state': bank_code}, context=context)
if 'communication' in line and line['communication'] != '':
note.append(_('Communication') + ': ' + line['communication'])
data = {
'name': line['name'],
- 'note': "\n".join(note),
+ 'note': "\n".join(note),
'date': line['entryDate'],
'amount': line['amount'],
'partner_id': partner_id,
'statement_id': statement['id'],
- 'ref': line['ref'],
+ 'ref': structured_com,
'sequence': line['sequence'],
- 'coda_account_number': line['counterpartyNumber'],
+ 'bank_account_id': bank_account_id,
}
self.pool.get('account.bank.statement.line').create(cr, uid, data, context=context)
if statement['coda_note'] != '':
diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py
index 854593a87cc..499e6e6c4ac 100644
--- a/addons/l10n_be_invoice_bba/invoice.py
+++ b/addons/l10n_be_invoice_bba/invoice.py
@@ -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')
diff --git a/addons/l10n_hn/l10n_hn_base.xml b/addons/l10n_hn/l10n_hn_base.xml
index 8b9acf5c50a..2ea77baab1b 100644
--- a/addons/l10n_hn/l10n_hn_base.xml
+++ b/addons/l10n_hn/l10n_hn_base.xml
@@ -25,7 +25,7 @@
ISV por Cobrar
-
+
percent
@@ -42,7 +42,7 @@
ISV por Pagar
-
+
percent
diff --git a/addons/l10n_uk/__openerp__.py b/addons/l10n_uk/__openerp__.py
index 0590224c364..0556535296f 100644
--- a/addons/l10n_uk/__openerp__.py
+++ b/addons/l10n_uk/__openerp__.py
@@ -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',
diff --git a/addons/l10n_us/__openerp__.py b/addons/l10n_us/__openerp__.py
index 90c0e6fd4ad..ea5792c1dca 100644
--- a/addons/l10n_us/__openerp__.py
+++ b/addons/l10n_us/__openerp__.py
@@ -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',
diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py
index ffc49414b2e..e4028111a73 100644
--- a/addons/mail/mail_followers.py
+++ b/addons/mail/mail_followers.py
@@ -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
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 4ae47a96774..186787c121a 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -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'] = '' % (group.alias_name, group.alias_domain)
+ return res
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py
index a4814140479..cc12bde2436 100644
--- a/addons/mail/mail_mail.py
+++ b/addons/mail/mail_mail.py
@@ -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)
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index dc2b98fdc93..76d3ab744d5 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -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]
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index cb068d566c1..e14a8f3dfe5 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -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')
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index a67f88a2a28..0e44399a256 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -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')
diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml
index 9806022498a..2939ebc3711 100644
--- a/addons/mass_mailing/views/mass_mailing.xml
+++ b/addons/mass_mailing/views/mass_mailing.xml
@@ -591,6 +591,7 @@
+
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index f5b6cf466a5..ca4109c1b9a 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -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:
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index 8aae0062a33..52e0f539b80 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -746,7 +746,7 @@
-
+
@@ -774,7 +774,7 @@
-
+
diff --git a/addons/mrp/wizard/mrp_product_produce_view.xml b/addons/mrp/wizard/mrp_product_produce_view.xml
index 666be7d5114..859bebc2b43 100644
--- a/addons/mrp/wizard/mrp_product_produce_view.xml
+++ b/addons/mrp/wizard/mrp_product_produce_view.xml
@@ -18,16 +18,16 @@
+ groups="stock.group_production_lot"/>
-
+
+ groups="stock.group_production_lot"/>
diff --git a/addons/mrp/wizard/stock_move_view.xml b/addons/mrp/wizard/stock_move_view.xml
index ebd948633c2..86588927c57 100644
--- a/addons/mrp/wizard/stock_move_view.xml
+++ b/addons/mrp/wizard/stock_move_view.xml
@@ -15,7 +15,7 @@
-
diff --git a/addons/mrp_repair/views/report_mrprepairorder.xml b/addons/mrp_repair/views/report_mrprepairorder.xml
index 2897fee1fd9..a6d00ba5b15 100644
--- a/addons/mrp_repair/views/report_mrprepairorder.xml
+++ b/addons/mrp_repair/views/report_mrprepairorder.xml
@@ -30,7 +30,7 @@
Repair Order N°:
- Repair Quotation N°:
+ Repair Quotation N°:
@@ -41,7 +41,9 @@
Lot Number
-
+
+
+
Guarantee Limit:
diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py
index 2ebc800ec04..e45651a30a2 100644
--- a/addons/procurement/procurement.py
+++ b/addons/procurement/procurement.py
@@ -64,7 +64,7 @@ class procurement_group(osv.osv):
}
_defaults = {
'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').get(cr, uid, 'procurement.group') or '',
- 'move_type': lambda self, cr, uid, c: 'one'
+ 'move_type': lambda self, cr, uid, c: 'direct'
}
class procurement_rule(osv.osv):
@@ -267,7 +267,7 @@ class procurement_order(osv.osv):
#
# Scheduler
#
- def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
+ def run_scheduler(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
'''
Call the scheduler to check the procurement order. This is intented to be done for all existing companies at
the same time, so we're running all the methods as SUPERUSER to avoid intercompany and access rights issues.
@@ -288,8 +288,11 @@ class procurement_order(osv.osv):
cr = openerp.registry(cr.dbname).cursor()
# Run confirmed procurements
+ dom = [('state', '=', 'confirmed')]
+ if company_id:
+ dom += [('company_id', '=', company_id)]
while True:
- ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'confirmed')], context=context)
+ ids = self.search(cr, SUPERUSER_ID, dom, context=context)
if not ids:
break
self.run(cr, SUPERUSER_ID, ids, context=context)
@@ -298,8 +301,11 @@ class procurement_order(osv.osv):
# Check if running procurements are done
offset = 0
+ dom = [('state', '=', 'running')]
+ if company_id:
+ dom += [('company_id', '=', company_id)]
while True:
- ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'running')], offset=offset, context=context)
+ ids = self.search(cr, SUPERUSER_ID, dom, offset=offset, context=context)
if not ids:
break
done = self.check(cr, SUPERUSER_ID, ids, context=context)
diff --git a/addons/procurement/wizard/schedulers_all.py b/addons/procurement/wizard/schedulers_all.py
index 5c712b3af38..34d4758c5d5 100644
--- a/addons/procurement/wizard/schedulers_all.py
+++ b/addons/procurement/wizard/schedulers_all.py
@@ -37,8 +37,12 @@ class procurement_compute_all(osv.osv_memory):
"""
proc_obj = self.pool.get('procurement.order')
#As this function is in a new thread, i need to open a new cursor, because the old one may be closed
+
new_cr = self.pool.cursor()
- proc_obj.run_scheduler(new_cr, uid, use_new_cursor=new_cr.dbname, context=context)
+ user = self.pool.get('res.users').browse(new_cr, uid, uid, context=context)
+ comps = [x.id for x in user.company_ids]
+ for comp in comps:
+ proc_obj.run_scheduler(new_cr, uid, use_new_cursor=new_cr.dbname, company_id = comp, context=context)
#close the new cursor
new_cr.close()
return {}
diff --git a/addons/product/product.py b/addons/product/product.py
index eaed55c0b41..6fa0627db2c 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -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):
diff --git a/addons/product_email_template/models/invoice.py b/addons/product_email_template/models/invoice.py
index d9e82e42012..d5876b510f1 100644
--- a/addons/product_email_template/models/invoice.py
+++ b/addons/product_email_template/models/invoice.py
@@ -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
diff --git a/addons/product_expiry/product_expiry.py b/addons/product_expiry/product_expiry.py
index e311356dbbd..6979344bbcc 100644
--- a/addons/product_expiry/product_expiry.py
+++ b/addons/product_expiry/product_expiry.py
@@ -92,7 +92,7 @@ class stock_quant(osv.osv):
def apply_removal_strategy(self, cr, uid, location, product, qty, domain, removal_strategy, context=None):
if removal_strategy == 'fefo':
- order = 'removal_date, id'
+ order = 'removal_date, in_date, id'
return self._quants_get_order(cr, uid, location, product, qty, domain, order, context=context)
return super(stock_quant, self).apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context)
diff --git a/addons/project/project_demo.xml b/addons/project/project_demo.xml
index 54f78ba8c8b..695aa80af2a 100644
--- a/addons/project/project_demo.xml
+++ b/addons/project/project_demo.xml
@@ -50,8 +50,8 @@
project.task
+ ref('base.partner_root'),
+ ref('base.partner_demo')])]"/>
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index bfdd83e0872..09cc4c301a4 100644
--- a/addons/purchase/purchase.py
+++ b/addons/purchase/purchase.py
@@ -700,6 +700,7 @@ class purchase_order(osv.osv):
'origin': order.name,
'route_ids': order.picking_type_id.warehouse_id and [(6, 0, [x.id for x in order.picking_type_id.warehouse_id.route_ids])] or [],
'warehouse_id':order.picking_type_id.warehouse_id.id,
+ 'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none'
}
diff_quantity = order_line.product_qty
@@ -709,9 +710,10 @@ class purchase_order(osv.osv):
tmp.update({
'product_uom_qty': min(procurement_qty, diff_quantity),
'product_uos_qty': min(procurement_qty, diff_quantity),
- 'move_dest_id': procurement.move_dest_id.id, # blabla
- 'group_id': procurement.group_id.id or group_id, # blabla to check ca devrait etre bon et groupé dans le meme picking qd meme
+ 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination
+ 'group_id': procurement.group_id.id or group_id, #move group is same as group of procurements if it exists, otherwise take another group
'procurement_id': procurement.id,
+ 'invoice_state': procurement.rule_id.invoice_state or (procurement.location_id and procurement.location_id.usage == 'customer' and procurement.invoice_state) or (order.invoice_method == 'picking' and '2binvoiced') or 'none', #dropship case takes from sale
})
diff_quantity -= min(procurement_qty, diff_quantity)
res.append(tmp)
@@ -1299,6 +1301,7 @@ class procurement_order(osv.osv):
res[procurement.id] = False
else:
schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context)
+ purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context)
#look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one
available_draft_po_ids = po_obj.search(cr, uid, [
@@ -1306,6 +1309,10 @@ class procurement_order(osv.osv):
('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context)
if available_draft_po_ids:
po_id = available_draft_po_ids[0]
+ po_rec = po_obj.browse(cr, uid, po_id, context=context)
+ #if the product has to be ordered earlier those in the existing PO, we replace the purchase date on the order to avoid ordering it too late
+ if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATE_FORMAT) > purchase_date:
+ po_obj.write(cr, uid, [po_id], {'date_order': purchase_date}, context=context)
#look for any other PO line in the selected PO with same product and UoM to sum quantities instead of creating a new po line
available_po_line_ids = po_line_obj.search(cr, uid, [('order_id', '=', po_id), ('product_id', '=', line_vals['product_id']), ('product_uom', '=', line_vals['product_uom'])], context=context)
if available_po_line_ids:
@@ -1318,7 +1325,6 @@ class procurement_order(osv.osv):
po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context)
linked_po_ids.append(procurement.id)
else:
- purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name
po_vals = {
'name': name,
@@ -1363,6 +1369,13 @@ class product_template(osv.Model):
_name = 'product.template'
_inherit = 'product.template'
+ def _get_buy_route(self, cr, uid, context=None):
+
+ buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy')
+ if buy_route:
+ return [buy_route]
+ return []
+
def _purchase_count(self, cr, uid, ids, field_name, arg, context=None):
res = dict.fromkeys(ids, 0)
for template in self.browse(cr, uid, ids, context=context):
@@ -1374,6 +1387,7 @@ class product_template(osv.Model):
}
_defaults = {
'purchase_ok': 1,
+ 'route_ids': _get_buy_route,
}
class product_product(osv.Model):
@@ -1451,15 +1465,9 @@ class account_invoice_line(osv.Model):
readonly=True),
}
-class product_product(osv.osv):
- _inherit = "product.product"
+class product_template(osv.osv):
+ _inherit = "product.template"
- def _get_buy_route(self, cr, uid, context=None):
- buy_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'purchase', 'route_warehouse0_buy')[1]
- return [buy_route]
- _defaults = {
- 'route_ids': _get_buy_route,
- }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py
index 69d970b7be6..26d2ff1002f 100644
--- a/addons/purchase/stock.py
+++ b/addons/purchase/stock.py
@@ -55,6 +55,33 @@ class stock_move(osv.osv):
})
return super(stock_move, self).copy(cr, uid, id, default, context)
+
+ def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
+ invoice_line_id = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
+ if move.purchase_line_id:
+ purchase_line = move.purchase_line_id
+ self.pool.get('purchase.order.line').write(cr, uid, [purchase_line.id], {
+ 'invoice_lines': [(4, invoice_line_id)]
+ }, context=context)
+ self.pool.get('purchase.order').write(cr, uid, [purchase_line.order_id.id], {
+ 'invoice_ids': [(4, invoice_line_vals['invoice_id'])],
+ })
+ return invoice_line_id
+
+ def _get_master_data(self, cr, uid, move, company, context=None):
+ if move.purchase_line_id:
+ purchase_order = move.purchase_line_id.order_id
+ return purchase_order.partner_id, purchase_order.create_uid.id, purchase_order.pricelist_id.currency_id.id
+ return super(stock_move, self)._get_master_data(cr, uid, move, company, context=context)
+
+ def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None):
+ res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context)
+ if move.purchase_line_id:
+ purchase_line = move.purchase_line_id
+ res['invoice_line_tax_id'] = [(6, 0, [x.id for x in purchase_line.taxes_id])]
+ res['price_unit'] = purchase_line.price_unit
+ return res
+
class stock_picking(osv.osv):
_inherit = 'stock.picking'
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index 964fa1584ba..1f257376331 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -128,13 +128,12 @@ class sale_order(osv.osv):
sale_clause = ''
no_invoiced = False
for arg in args:
- if arg[1] == '=':
- if arg[2]:
- clause += 'AND inv.state = \'paid\''
- else:
- clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
- sale_clause = ', sale_order AS sale '
- no_invoiced = True
+ if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
+ clause += 'AND inv.state = \'paid\''
+ else:
+ clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
+ sale_clause = ', sale_order AS sale '
+ no_invoiced = True
cursor.execute('SELECT rel.order_id ' \
'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py
index b3bbd253392..63f6068f3c2 100644
--- a/addons/sale_stock/sale_stock.py
+++ b/addons/sale_stock/sale_stock.py
@@ -388,7 +388,7 @@ class stock_move(osv.osv):
return super(stock_move, self).action_cancel(cr, uid, ids, context=context)
def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
- invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
+ invoice_line_id = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
if move.procurement_id and move.procurement_id.sale_line_id:
sale_line = move.procurement_id.sale_line_id
self.pool.get('sale.order.line').write(cr, uid, [sale_line.id], {
diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py
index a8fc778f2e6..b8d7a03c9f2 100644
--- a/addons/stock/procurement.py
+++ b/addons/stock/procurement.py
@@ -261,7 +261,7 @@ class procurement_order(osv.osv):
result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]"
return result
- def run_scheduler(self, cr, uid, use_new_cursor=False, context=None):
+ def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
'''
Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules
and the availability of moves. This function is intended to be run for all the companies at the same time, so
@@ -286,8 +286,7 @@ class procurement_order(osv.osv):
move_obj = self.pool.get('stock.move')
#Minimum stock rules
- company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
- self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=False, company_id=company.id, context=context)
+ self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=False, company_id=company_id, context=context)
#Search all confirmed stock_moves and try to assign them
confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context)
@@ -331,7 +330,7 @@ class procurement_order(osv.osv):
[order_point.product_id.id],
context={'location': order_point.location_id.id})[order_point.product_id.id]['virtual_available']
- def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None):
+ def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id = False, context=None):
'''
Create procurement based on Orderpoint
@@ -341,14 +340,15 @@ class procurement_order(osv.osv):
if context is None:
context = {}
if use_new_cursor:
- cr = openerp.registry(cr.dbname).db.cursor()
+ cr = openerp.registry(cr.dbname).cursor()
orderpoint_obj = self.pool.get('stock.warehouse.orderpoint')
procurement_obj = self.pool.get('procurement.order')
offset = 0
ids = [1]
+ dom = company_id and [('company_id', '=', company_id)] or []
while ids:
- ids = orderpoint_obj.search(cr, uid, [('company_id', '=', company_id)], offset=offset, limit=100)
+ ids = orderpoint_obj.search(cr, uid, dom, offset=offset, limit=100)
for op in orderpoint_obj.browse(cr, uid, ids, context=context):
prods = self._product_virtual_get(cr, uid, op)
if prods is None:
diff --git a/addons/stock/product.py b/addons/stock/product.py
index 0cf7df5e836..1cd677706f6 100644
--- a/addons/stock/product.py
+++ b/addons/stock/product.py
@@ -171,13 +171,13 @@ class product_product(osv.osv):
def _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None):
res = {}
for product in self.browse(cr, uid, ids, context=context):
- res[product.id] = str(product.qty_available) + _(" In Stock")
+ res[product.id] = str(product.qty_available) + _(" On Hand")
return res
_columns = {
'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
- 'qty_in_stock': fields.function(_product_available_text, type='char'),
+ 'qty_available_text': fields.function(_product_available_text, type='char'),
'qty_available': fields.function(_product_available, multi='qty_available',
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
string='Quantity On Hand',
@@ -277,6 +277,12 @@ class product_product(osv.osv):
res['fields']['qty_available']['string'] = _('Produced Qty')
return res
+
+ def action_view_routes(self, cr, uid, ids, context=None):
+ template_obj = self.pool.get("product.template")
+ templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)]))
+ return template_obj.action_view_routes(cr, uid, templ_ids, context=context)
+
class product_template(osv.osv):
_name = 'product.template'
_inherit = 'product.template'
diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml
index 2a7ad044fbc..c3941eb1887 100644
--- a/addons/stock/product_view.xml
+++ b/addons/stock/product_view.xml
@@ -193,7 +193,7 @@
-
+
@@ -202,13 +202,13 @@
+ attrs="{'invisible':[('type', '=', 'service')]}" icon="fa-refresh" string="Reordering Rules"/>
diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv
index 9ce558781be..8a94f027408 100644
--- a/addons/stock/security/ir.model.access.csv
+++ b/addons/stock/security/ir.model.access.csv
@@ -60,7 +60,7 @@ access_stock_location_path_internal_user,stock location path internal user,model
access_stock_location_path_sale_manager,stock.location.path partner salemanager,model_stock_location_path,base.group_sale_manager,1,1,1,1
access_stock_location_path_stock_user,stock.location.path stock user,model_stock_location_path,stock.group_stock_user,1,1,1,1
access_stock_location_path,stock.location.path,model_stock_location_path,base.group_sale_salesman,1,0,0,0
-access_stock_location_route,stock.location.route,model_stock_location_route,stock.group_stock_manager,1,1,1,1
+access_stock_location_route_stock_manager,stock.location.route,model_stock_location_route,stock.group_stock_manager,1,1,1,1
access_stock_location_route,stock.location.route,model_stock_location_route,base.group_user,1,0,0,0
access_procurement_rule,procurement.rule.flow,model_procurement_rule,base.group_sale_salesman,1,0,0,0
access_procurement_rule_internal,procurement.rule.flow internal,model_procurement_rule,base.group_user,1,0,0,0
diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml
index f44d354f121..044ff1333cd 100644
--- a/addons/stock/security/stock_security.xml
+++ b/addons/stock/security/stock_security.xml
@@ -50,7 +50,7 @@
['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]
-
+
Stock Picking Type multi-company
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index d6d28c919e6..7e529b9047a 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -272,27 +272,28 @@ class stock_quant(osv.osv):
_columns = {
'name': fields.function(_get_quant_name, type='char', string='Identifier'),
- 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete="restrict"),
- 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="restrict"),
- 'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product"),
- 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant"),
- 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', store=True),
- 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for"),
- 'lot_id': fields.many2one('stock.production.lot', 'Lot'),
+ 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete="restrict", readonly=True),
+ 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="restrict", readonly=True),
+ 'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product", readonly=True),
+ 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant", readonly=True),
+ 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', readonly=True, store=True),
+ 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for", readonly=True),
+ 'lot_id': fields.many2one('stock.production.lot', 'Lot', readonly=True),
'cost': fields.float('Unit Cost'),
- 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant"),
+ 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant", readonly=True),
- 'create_date': fields.datetime('Creation Date'),
- 'in_date': fields.datetime('Incoming Date'),
+ 'create_date': fields.datetime('Creation Date', readonly=True),
+ 'in_date': fields.datetime('Incoming Date', readonly=True),
'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
- 'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True),
+ 'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True, readonly=True),
'inventory_value': fields.function(_calc_inventory_value, string="Inventory Value", type='float', readonly=True),
# Used for negative quants to reconcile after compensated by a new positive one
- 'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from'),
- 'negative_move_id': fields.many2one('stock.move', 'Move Negative Quant', help='If this is a negative quant, this will be the move that caused this negative quant.'),
- 'negative_dest_location_id': fields.related('negative_move_id', 'location_dest_id', type='many2one', relation='stock.location', string="Negative Destination Location", help="Technical field used to record the destination location of a move that created a negative quant"),
+ 'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from', readonly=True),
+ 'negative_move_id': fields.many2one('stock.move', 'Move Negative Quant', help='If this is a negative quant, this will be the move that caused this negative quant.', readonly=True),
+ 'negative_dest_location_id': fields.related('negative_move_id', 'location_dest_id', type='many2one', relation='stock.location', string="Negative Destination Location", readonly=True,
+ help="Technical field used to record the destination location of a move that created a negative quant"),
}
_defaults = {
@@ -606,7 +607,6 @@ class stock_quant(osv.osv):
raise osv.except_osv(_('Error'), _('You cannot move to a location of type view %s.') % (location.name))
return True
-
#----------------------------------------------------------
# Stock Picking
#----------------------------------------------------------
@@ -1248,6 +1248,7 @@ class stock_picking(osv.osv):
'''
move_obj = self.pool.get('stock.move')
operation_obj = self.pool.get('stock.pack.operation')
+ moves = []
for op in picking.pack_operation_ids:
for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items():
if remaining_qty > 0:
@@ -1260,9 +1261,12 @@ class stock_picking(osv.osv):
'product_uom': product.uom_id.id,
'product_uom_qty': remaining_qty,
'name': _('Extra Move: ') + product.name,
- 'state': 'confirmed',
+ 'state': 'draft',
}
- move_obj.create(cr, uid, vals, context=context)
+ moves.append(move_obj.create(cr, uid, vals, context=context))
+ if moves:
+ move_obj.action_confirm(cr, uid, moves, context=context)
+ return moves
def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
""" Unreserve quants then try to reassign quants."""
@@ -1289,11 +1293,13 @@ class stock_picking(osv.osv):
else:
need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, picking, context=context)
#create extra moves in the picking (unexpected product moves coming from pack operations)
+ todo_move_ids = []
if not all_op_processed:
- self._create_extra_moves(cr, uid, picking, context=context)
+ todo_move_ids += self._create_extra_moves(cr, uid, picking, context=context)
+
picking.refresh()
#split move lines eventually
- todo_move_ids = []
+
toassign_move_ids = []
for move in picking.move_lines:
remaining_qty = move.remaining_qty
@@ -1371,7 +1377,7 @@ class stock_picking(osv.osv):
op = operation
if (operation.qty_done < operation.product_qty):
new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context)
- stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0}, context=context)
+ stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0, 'lot_id': False}, context=context)
op = stock_operation_obj.browse(cr, uid, new_operation, context=context)
pack_operation_ids.append(op.id)
for record in op.linked_move_operation_ids:
@@ -1783,7 +1789,7 @@ class stock_move(osv.osv):
'move_dest_id': move.id,
'group_id': group_id,
'route_ids': [(4, x.id) for x in move.route_ids],
- 'warehouse_id': move.warehouse_id and move.warehouse_id.id or False,
+ 'warehouse_id': move.warehouse_id.id or (move.picking_type_id and move.picking_type_id.warehouse_id.id or False),
'priority': move.priority,
}
@@ -3521,7 +3527,6 @@ class stock_location_path(osv.osv):
# -------------------------
from openerp.report import report_sxw
-report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package', 'addons/stock/report/package_barcode.rml')
class stock_package(osv.osv):
"""
@@ -3818,8 +3823,8 @@ class stock_pack_operation(osv.osv):
if pack_op.qty_done < pack_op.product_qty:
# we split the operation in two
op = self.copy(cr, uid, pack_op.id, {'product_qty': pack_op.qty_done, 'qty_done': pack_op.qty_done}, context=context)
- self.write(cr, uid, ids, {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0}, context=context)
- processed_ids.append(op)
+ self.write(cr, uid, [pack_op.id], {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0, 'lot_id': False}, context=context)
+ processed_ids.append(op)
self.write(cr, uid, processed_ids, {'processed': 'true'}, context=context)
def create_and_assign_lot(self, cr, uid, id, name, context=None):
@@ -3828,10 +3833,16 @@ class stock_pack_operation(osv.osv):
obj = self.browse(cr,uid,id,context)
product_id = obj.product_id.id
val = {'product_id': product_id}
+ new_lot_id = False
if name:
+ lots = self.pool.get('stock.production.lot').search(cr, uid, ['&', ('name', '=', name), ('product_id', '=', product_id)], context=context)
+ if lots:
+ new_lot_id = lots[0]
val.update({'name': name})
+
if not obj.lot_id:
- new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, val, context=context)
+ if not new_lot_id:
+ new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, val, context=context)
self.write(cr, uid, id, {'lot_id': new_lot_id}, context=context)
def _search_and_increment(self, cr, uid, picking_id, domain, filter_visible=False, visible_op_ids=False, increment=True, context=None):
@@ -3869,11 +3880,15 @@ class stock_pack_operation(osv.osv):
self.write(cr, uid, [operation_id], {'qty_done': qty}, context=context)
else:
#no existing operation found for the given domain and picking => create a new one
+ picking_obj = self.pool.get("stock.picking")
+ picking = picking_obj.browse(cr, uid, picking_id, context=context)
values = {
'picking_id': picking_id,
'product_qty': 0,
+ 'location_id': picking.location_id.id,
+ 'location_dest_id': picking.location_dest_id.id,
'qty_done': 1,
- }
+ }
for key in domain:
var_name, dummy, value = key
uom_id = False
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index fc7a94fc51f..cb2c2ab31d2 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -355,7 +355,7 @@
{'search_default_productgroup': 1}
[('location_id', 'child_of', active_ids)]
- Quants
+ Current Stock
stock.quant
@@ -749,7 +749,7 @@
-->
-
+
@@ -1288,7 +1288,7 @@
ir.actions.act_window
form
tree,form
-
+
{'product_receive': True, 'search_default_future': True}
@@ -1356,6 +1356,7 @@
stock.picking.type
+
@@ -1794,7 +1795,7 @@
{'search_default_internal_loc': 1, 'search_default_product_id': active_id, 'search_default_locationgroup':1}
- Quants
+ Current Stock
stock.quant
diff --git a/addons/stock/views/report_location_barcode.xml b/addons/stock/views/report_location_barcode.xml
index d9065e0d207..7e6466ed5c8 100644
--- a/addons/stock/views/report_location_barcode.xml
+++ b/addons/stock/views/report_location_barcode.xml
@@ -7,18 +7,15 @@
diff --git a/addons/stock/views/report_stockpicking.xml b/addons/stock/views/report_stockpicking.xml
index 0b67ba94db6..7d2bb125de2 100644
--- a/addons/stock/views/report_stockpicking.xml
+++ b/addons/stock/views/report_stockpicking.xml
@@ -7,15 +7,15 @@
- ![]()
+
-
+
Supplier Address:
-
+
Warehouse Address:
-
+
Customer Address:
@@ -73,7 +73,7 @@
|
-
+
|
|
@@ -104,13 +104,13 @@
-
+
-
+
-
+
|
diff --git a/addons/stock/wizard/orderpoint_procurement.py b/addons/stock/wizard/orderpoint_procurement.py
index 2cbc794cde3..f898dac4305 100644
--- a/addons/stock/wizard/orderpoint_procurement.py
+++ b/addons/stock/wizard/orderpoint_procurement.py
@@ -43,8 +43,10 @@ class procurement_compute(osv.osv_memory):
proc_obj = self.pool.get('procurement.order')
#As this function is in a new thread, I need to open a new cursor, because the old one may be closed
new_cr = self.pool.cursor()
- for proc in self.browse(new_cr, uid, ids, context=context):
- proc_obj._procure_orderpoint_confirm(new_cr, uid, use_new_cursor=new_cr.dbname, context=context)
+ user_obj = self.pool.get('res.users')
+ user = user_obj.browse(new_cr, uid, uid, context=context)
+ for comp in user.company_ids:
+ proc_obj._procure_orderpoint_confirm(new_cr, uid, use_new_cursor=new_cr.dbname, company_id = comp.id, context=context)
#close the new cursor
new_cr.close()
return {}
diff --git a/addons/stock_account/__openerp__.py b/addons/stock_account/__openerp__.py
index a2dab1289f4..5c67002f61f 100644
--- a/addons/stock_account/__openerp__.py
+++ b/addons/stock_account/__openerp__.py
@@ -53,6 +53,7 @@ Dashboard / Reports for Warehouse Management includes:
'wizard/stock_change_standard_price_view.xml',
'wizard/stock_invoice_onshipping_view.xml',
'wizard/stock_valuation_history_view.xml',
+ 'wizard/stock_return_picking_view.xml',
'product_data.xml',
'product_view.xml',
'stock_account_view.xml',
diff --git a/addons/stock_account/product_view.xml b/addons/stock_account/product_view.xml
index 97f51b04ac5..960280154fd 100644
--- a/addons/stock_account/product_view.xml
+++ b/addons/stock_account/product_view.xml
@@ -30,11 +30,7 @@
-
-
-
-
-
+
diff --git a/addons/stock_account/stock.py b/addons/stock_account/stock.py
index db08ba39eb4..56d851cec7a 100644
--- a/addons/stock_account/stock.py
+++ b/addons/stock_account/stock.py
@@ -27,11 +27,10 @@ class stock_location_path(osv.osv):
'invoice_state': fields.selection([
("invoiced", "Invoiced"),
("2binvoiced", "To Be Invoiced"),
- ("none", "Not Applicable")], "Invoice Status",
- required=True,),
+ ("none", "Not Applicable")], "Invoice Status",),
}
_defaults = {
- 'invoice_state': 'none',
+ 'invoice_state': '',
}
#----------------------------------------------------------
@@ -43,11 +42,10 @@ class procurement_rule(osv.osv):
'invoice_state': fields.selection([
("invoiced", "Invoiced"),
("2binvoiced", "To Be Invoiced"),
- ("none", "Not Applicable")], "Invoice Status",
- required=True),
+ ("none", "Not Applicable")], "Invoice Status",),
}
_defaults = {
- 'invoice_state': 'none',
+ 'invoice_state': '',
}
#----------------------------------------------------------
@@ -61,16 +59,16 @@ class procurement_order(osv.osv):
'invoice_state': fields.selection([("invoiced", "Invoiced"),
("2binvoiced", "To Be Invoiced"),
("none", "Not Applicable")
- ], "Invoice Control", required=True),
+ ], "Invoice Control"),
}
def _run_move_create(self, cr, uid, procurement, context=None):
res = super(procurement_order, self)._run_move_create(cr, uid, procurement, context=context)
- res.update({'invoice_state': (procurement.rule_id.invoice_state in ('none', False) and procurement.invoice_state or procurement.rule_id.invoice_state) or 'none'})
+ res.update({'invoice_state': procurement.rule_id.invoice_state or procurement.invoice_state or 'none'})
return res
_defaults = {
- 'invoice_state': 'none'
+ 'invoice_state': ''
}
@@ -92,7 +90,7 @@ class stock_move(osv.osv):
}
def _get_master_data(self, cr, uid, move, company, context=None):
- ''' returns a tupple (browse_record(res.partner), ID(res.users), ID(res.currency)'''
+ ''' returns a tuple (browse_record(res.partner), ID(res.users), ID(res.currency)'''
return move.picking_id.partner_id, uid, company.currency_id.id
def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
@@ -204,7 +202,7 @@ class stock_picking(osv.osv):
for picking in self.browse(cr, uid, ids, context=context):
key = group and picking.id or True
for move in picking.move_lines:
- if move.procurement_id and (move.procurement_id.invoice_state == '2binvoiced') or move.invoice_state == '2binvoiced':
+ if move.invoice_state == '2binvoiced':
if (move.state != 'cancel') and not move.scrapped:
todo.setdefault(key, [])
todo[key].append(move)
@@ -259,12 +257,7 @@ class stock_picking(osv.osv):
invoice_line_vals['origin'] = origin
move_obj._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
-
move_obj.write(cr, uid, move.id, {'invoice_state': 'invoiced'}, context=context)
- if move.procurement_id:
- self.pool.get('procurement.order').write(cr, uid, [move.procurement_id.id], {
- 'invoice_state': 'invoiced',
- }, context=context)
invoice_obj.button_compute(cr, uid, invoices.values(), context=context, set_total=(inv_type in ('in_invoice', 'in_refund')))
return invoices.values()
diff --git a/addons/stock_account/stock_account_view.xml b/addons/stock_account/stock_account_view.xml
index 3b3ab37b212..4f8c2122e55 100644
--- a/addons/stock_account/stock_account_view.xml
+++ b/addons/stock_account/stock_account_view.xml
@@ -33,7 +33,6 @@
-
diff --git a/addons/stock_account/wizard/__init__.py b/addons/stock_account/wizard/__init__.py
index 87f0841e3c5..09d08bb82d1 100644
--- a/addons/stock_account/wizard/__init__.py
+++ b/addons/stock_account/wizard/__init__.py
@@ -22,3 +22,4 @@
import stock_change_standard_price
import stock_invoice_onshipping
import stock_valuation_history
+import stock_return_picking
diff --git a/addons/stock_account/wizard/stock_invoice_onshipping.py b/addons/stock_account/wizard/stock_invoice_onshipping.py
index f600b28f585..1675275632a 100644
--- a/addons/stock_account/wizard/stock_invoice_onshipping.py
+++ b/addons/stock_account/wizard/stock_invoice_onshipping.py
@@ -24,34 +24,48 @@ from openerp.tools.translate import _
class stock_invoice_onshipping(osv.osv_memory):
def _get_journal(self, cr, uid, context=None):
- res = self._get_journal_id(cr, uid, context=context)
- if res:
- return res[0][0]
- return False
-
- def _get_journal_id(self, cr, uid, context=None):
+ journal_obj = self.pool.get('account.journal')
+ journal_type = self._get_journal_type(cr, uid, context=context)
+ journals = journal_obj.search(cr, uid, [('type', '=', journal_type)])
+ return journals and journals[0] or False
+
+ def _get_journal_type(self, cr, uid, context=None):
if context is None:
context = {}
- journal_obj = self.pool.get('account.journal')
- value = journal_obj.search(cr, uid, [('type', 'in',('sale','sale_Refund'))])
+ res_ids = context and context.get('active_ids', [])
+ pick_obj = self.pool.get('stock.picking')
+ pickings = pick_obj.browse(cr, uid, res_ids, context=context)
vals = []
- for jr_type in journal_obj.browse(cr, uid, value, context=context):
- t1 = jr_type.id,jr_type.name
- if t1 not in vals:
- vals.append(t1)
- return vals
+ pick = pickings and pickings[0]
+ if not pick or not pick.move_lines:
+ return 'sale'
+ src_usage = pick.move_lines[0].location_id.usage
+ dest_usage = pick.move_lines[0].location_dest_id.usage
+ type = pick.picking_type_id.code
+ if type == 'outgoing' and dest_usage == 'supplier':
+ journal_type = 'purchase_refund'
+ elif type == 'outgoing' and dest_usage == 'customer':
+ journal_type = 'sale'
+ elif type == 'incoming' and src_usage == 'supplier':
+ journal_type = 'purchase'
+ elif type == 'incoming' and src_usage == 'customer':
+ journal_type = 'sale_refund'
+ else:
+ journal_type = 'sale'
+ return journal_type
_name = "stock.invoice.onshipping"
_description = "Stock Invoice Onshipping"
_columns = {
- 'journal_id': fields.selection(_get_journal_id, 'Destination Journal',required=True),
+ 'journal_id': fields.many2one('account.journal', 'Destination Journal', required=True),
+ 'journal_type': fields.selection([('purchase_refund', 'Refund Purchase'), ('purchase', 'Create Supplier Invoice'),
+ ('sale_refund', 'Refund Sale'), ('sale', 'Create Customer Invoice')], 'Journal Type', readonly=True),
'group': fields.boolean("Group by partner"),
- 'inv_type': fields.selection([('out_invoice','Create Invoice'),('out_refund','Refund Invoice')], "Invoice Type"),
'invoice_date': fields.date('Invoice Date'),
}
_defaults = {
+ 'journal_type': _get_journal_type,
'journal_id' : _get_journal,
- 'inv_type': lambda self,cr,uid,ctx: ctx.get('inv_type', 'out_invoice')
}
def view_init(self, cr, uid, fields_list, context=None):
@@ -71,24 +85,30 @@ class stock_invoice_onshipping(osv.osv_memory):
def open_invoice(self, cr, uid, ids, context=None):
if context is None:
context = {}
+
invoice_ids = self.create_invoice(cr, uid, ids, context=context)
if not invoice_ids:
raise osv.except_osv(_('Error!'), _('No invoice created!'))
- onshipdata_obj = self.read(cr, uid, ids, ['journal_id', 'group', 'invoice_date', 'inv_type'])
- inv_type = onshipdata_obj[0]['inv_type']
+ data = self.browse(cr, uid, ids[0], context=context)
action_model = False
action = {}
-
+
+ journal2type = {'sale':'out_invoice', 'purchase':'in_invoice' , 'sale_refund':'out_refund', 'purchase_refund':'in_refund'}
+ inv_type = journal2type.get(data.journal_type) or 'out_invoice'
data_pool = self.pool.get('ir.model.data')
- if inv_type == "out_refund":
- action_model,action_id = data_pool.get_object_reference(cr, uid, 'account', "action_invoice_tree3")
- elif inv_type == "out_invoice":
- action_model,action_id = data_pool.get_object_reference(cr, uid, 'account', "action_invoice_tree1")
+ if inv_type == "out_invoice":
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree1')
+ elif inv_type == "in_invoice":
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree2')
+ elif inv_type == "out_refund":
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree3')
+ elif inv_type == "in_refund":
+ action_id = data_pool.xmlid_to_res_id(cr, uid, 'account.action_invoice_tree4')
- if action_model:
- action_pool = self.pool[action_model]
+ if action_id:
+ action_pool = self.pool['ir.actions.act_window']
action = action_pool.read(cr, uid, action_id, context=context)
action['domain'] = "[('id','in', ["+','.join(map(str,invoice_ids))+"])]"
return action
@@ -97,18 +117,17 @@ class stock_invoice_onshipping(osv.osv_memory):
def create_invoice(self, cr, uid, ids, context=None):
context = context or {}
picking_pool = self.pool.get('stock.picking')
- onshipdata_obj = self.read(cr, uid, ids, ['journal_id', 'group', 'invoice_date', 'inv_type'])
-
- context['date_inv'] = onshipdata_obj[0]['invoice_date']
- inv_type = onshipdata_obj[0]['inv_type']
+ data = self.browse(cr, uid, ids[0], context=context)
+ journal2type = {'sale':'out_invoice', 'purchase':'in_invoice', 'sale_refund':'out_refund', 'purchase_refund':'in_refund'}
+ context['date_inv'] = data.invoice_date
+ acc_journal = self.pool.get("account.journal")
+ inv_type = journal2type.get(data.journal_type) or 'out_invoice'
context['inv_type'] = inv_type
active_ids = context.get('active_ids', [])
- if isinstance(onshipdata_obj[0]['journal_id'], tuple):
- onshipdata_obj[0]['journal_id'] = onshipdata_obj[0]['journal_id'][0]
res = picking_pool.action_invoice_create(cr, uid, active_ids,
- journal_id = onshipdata_obj[0]['journal_id'],
- group = onshipdata_obj[0]['group'],
+ journal_id = data.journal_id.id,
+ group = data.group,
type = inv_type,
context=context)
return res
diff --git a/addons/stock_account/wizard/stock_invoice_onshipping_view.xml b/addons/stock_account/wizard/stock_invoice_onshipping_view.xml
index e632fc4057b..54cced6de4f 100644
--- a/addons/stock_account/wizard/stock_invoice_onshipping_view.xml
+++ b/addons/stock_account/wizard/stock_invoice_onshipping_view.xml
@@ -7,10 +7,10 @@
-
-
-
).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from openerp.osv import osv, fields
+from openerp.tools.translate import _
+import openerp.addons.decimal_precision as dp
+
+
+class stock_return_picking(osv.osv_memory):
+ _inherit = 'stock.return.picking'
+ _columns = {
+ 'invoice_state': fields.selection([('2binvoiced', 'To be refunded/invoiced'), ('none', 'No invoicing')], 'Invoicing',required=True),
+ }
+
+ def default_get(self, cr, uid, fields, context=None):
+ res = super(stock_return_picking, self).default_get(cr, uid, fields, context=context)
+ record_id = context and context.get('active_id', False) or False
+ pick_obj = self.pool.get('stock.picking')
+ pick = pick_obj.browse(cr, uid, record_id, context=context)
+ if pick:
+ if 'invoice_state' in fields:
+ if pick.invoice_state=='invoiced':
+ res.update({'invoice_state': '2binvoiced'})
+ else:
+ res.update({'invoice_state': 'none'})
+ return res
+
+
+
+ def _create_returns(self, cr, uid, ids, context=None):
+ if context is None:
+ context = {}
+ data = self.browse(cr, uid, ids[0], context=context)
+ new_picking, picking_type_id = super(stock_return_picking, self)._create_returns(cr, uid, ids, context=context)
+ if data.invoice_state == '2binvoiced':
+ pick_obj = self.pool.get("stock.picking")
+ move_obj = self.pool.get("stock.move")
+ move_ids = [x.id for x in pick_obj.browse(cr, uid, new_picking, context=context).move_lines]
+ move_obj.write(cr, uid, move_ids, {'invoice_state': '2binvoiced'})
+ return new_picking, picking_type_id
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/stock_account/wizard/stock_return_picking_view.xml b/addons/stock_account/wizard/stock_return_picking_view.xml
new file mode 100644
index 00000000000..8365c424b95
--- /dev/null
+++ b/addons/stock_account/wizard/stock_return_picking_view.xml
@@ -0,0 +1,14 @@
+
+
+
+ Return lines
+ stock.return.picking
+
+
+
+
+
+
+
+
+
diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py
index 42b40b56432..540a308c536 100644
--- a/addons/stock_account/wizard/stock_valuation_history.py
+++ b/addons/stock_account/wizard/stock_valuation_history.py
@@ -55,12 +55,13 @@ class stock_history(osv.osv):
product_tmpl_obj = self.pool.get("product.template")
lines_rec = self.browse(cr, uid, lines, context=context)
for line_rec in lines_rec:
- if not line_rec.product_id.id in prod_dict:
- if line_rec.product_id.cost_method == 'real':
- prod_dict[line_rec.product_id.id] = line_rec.price_unit_on_quant
- else:
+ if line_rec.product_id.cost_method == 'real':
+ price = line_rec.price_unit_on_quant
+ else:
+ if not line_rec.product_id.id in prod_dict:
prod_dict[line_rec.product_id.id] = product_tmpl_obj.get_history_price(cr, uid, line_rec.product_id.product_tmpl_id.id, line_rec.company_id.id, date=date, context=context)
- inv_value += prod_dict[line_rec.product_id.id] * line_rec.quantity
+ price = prod_dict[line_rec.product_id.id]
+ inv_value += price * line_rec.quantity
line['inventory_value'] = inv_value
return res
diff --git a/addons/stock_dropshipping/stock_dropshipping.xml b/addons/stock_dropshipping/stock_dropshipping.xml
index ede1f00afed..de1de472d20 100644
--- a/addons/stock_dropshipping/stock_dropshipping.xml
+++ b/addons/stock_dropshipping/stock_dropshipping.xml
@@ -14,7 +14,7 @@
Dropship
- incoming
+ outgoing
@@ -37,7 +37,6 @@
make_to_stock
-
-
+
diff --git a/addons/survey/controllers/main.py b/addons/survey/controllers/main.py
index 638b2aefed4..c554558b34f 100644
--- a/addons/survey/controllers/main.py
+++ b/addons/survey/controllers/main.py
@@ -318,8 +318,9 @@ class WebsiteSurvey(http.Controller):
'filter_finish': filter_finish
})
- def prepare_result_dict(self,survey, current_filters=[]):
+ def prepare_result_dict(self,survey, current_filters=None):
"""Returns dictionary having values for rendering template"""
+ current_filters = current_filters if current_filters else []
survey_obj = request.registry['survey.survey']
result = {'survey':survey, 'page_ids': []}
for page in survey.page_ids:
@@ -347,8 +348,10 @@ class WebsiteSurvey(http.Controller):
total = ceil(total_record / float(limit))
return range(1, int(total + 1))
- def get_graph_data(self, question, current_filters=[]):
+ def get_graph_data(self, question, current_filters=None):
'''Returns formatted data required by graph library on basis of filter'''
+ # TODO refactor this terrible method and merge it with prepare_result_dict
+ current_filters = current_filters if current_filters else []
survey_obj = request.registry['survey.survey']
result = []
if question.type == 'multiple_choice':
@@ -360,9 +363,8 @@ class WebsiteSurvey(http.Controller):
data = survey_obj.prepare_result(request.cr, request.uid, question, current_filters, context=request.context)
for answer in data['answers']:
values = []
- for res in data['result']:
- if res[1] == answer:
- values.append({'text': data['rows'][res[0]], 'count': data['result'][res]})
+ for row in data['rows']:
+ values.append({'text': data['rows'].get(row), 'count': data['result'].get((row, answer))})
result.append({'key': data['answers'].get(answer), 'values': values})
return json.dumps(result)
diff --git a/addons/survey/survey.py b/addons/survey/survey.py
index 140e3e97d65..caecd16b7e1 100644
--- a/addons/survey/survey.py
+++ b/addons/survey/survey.py
@@ -26,6 +26,7 @@ from openerp.addons.website.models.website import slug
from urlparse import urljoin
from itertools import product
from collections import Counter
+from collections import OrderedDict
import datetime
import logging
@@ -289,8 +290,7 @@ class survey_survey(osv.Model):
:param finished: True for completely filled survey,Falser otherwise.
:returns list of filtered user_input_ids.
'''
- if context is None:
- context = {}
+ context = context if context else {}
if filters:
input_line_obj = self.pool.get('survey.user_input_line')
domain_filter, choice, filter_display_data = [], [], []
@@ -339,22 +339,27 @@ class survey_survey(osv.Model):
filter_display_data.append({'question_text': question.question, 'labels': [label.value for label in labels]})
return filter_display_data
- def prepare_result(self, cr, uid, question, current_filters=[], context=None):
+ def prepare_result(self, cr, uid, question, current_filters=None, context=None):
''' Compute statistical data for questions by counting number of vote per choice on basis of filter '''
- if context is None:
- context = {}
+ current_filters = current_filters if current_filters else []
+ context = context if context else {}
+
#Calculate and return statistics for choice
if question.type in ['simple_choice', 'multiple_choice']:
- result_summary = {}
- [result_summary.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids]
- for input_line in question.user_input_line_ids:
- if input_line.answer_type == 'suggestion' and result_summary.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters):
- result_summary[input_line.value_suggested.id]['count'] += 1
- result_summary = result_summary.values()
+ result_summary = []
+ for label in question.labels_ids:
+ count = 0
+ for input_line in question.user_input_line_ids:
+ if input_line.answer_type == 'suggestion' and input_line.value_suggested.id == label.id and (not current_filters or input_line.user_input_id.id in current_filters):
+ count = count + 1
+ label_summary = {'text': label.value, 'count': count, 'answer_id': label.id}
+ result_summary = result_summary + [label_summary]
#Calculate and return statistics for matrix
if question.type == 'matrix':
- rows, answers, res = {}, {}, {}
+ rows = OrderedDict()
+ answers = OrderedDict()
+ res = dict()
[rows.update({label.id: label.value}) for label in question.labels_ids_2]
[answers.update({label.id: label.value}) for label in question.labels_ids]
for cell in product(rows.keys(), answers.keys()):
@@ -386,10 +391,10 @@ class survey_survey(osv.Model):
'most_comman': Counter(all_inputs).most_common(5)})
return result_summary
- def get_input_summary(self, cr, uid, question, current_filters=[], context=None):
+ def get_input_summary(self, cr, uid, question, current_filters=None, context=None):
''' Returns overall summary of question e.g. answered, skipped, total_inputs on basis of filter '''
- if context is None:
- context = {}
+ current_filters = current_filters if current_filters else []
+ context = context if context else {}
result = {}
if question.survey_id.user_input_ids:
total_input_ids = current_filters or [input_id.id for input_id in question.survey_id.user_input_ids if input_id.state != 'new']
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css
index b234e2738df..91146789602 100644
--- a/addons/web/static/src/css/base.css
+++ b/addons/web/static/src/css/base.css
@@ -2325,7 +2325,7 @@
.openerp .oe_form .oe_form_field_image .oe_form_field_image_controls {
position: absolute;
top: 1px;
- padding: 4px 0;
+ padding: 6px 0;
width: 100%;
display: none;
text-align: center;
@@ -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 {
@@ -2647,11 +2647,11 @@
.openerp .oe_list_editable .oe_list_content td.oe_list_field_cell {
padding: 4px 6px 3px;
}
-.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell:not(.oe_readonly) {
+.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell {
color: transparent;
text-shadow: none;
}
-.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell:not(.oe_readonly) * {
+.openerp .oe_list.oe_list_editable.oe_editing .oe_edition .oe_list_field_cell * {
visibility: hidden;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_drop_down_button {
@@ -2667,6 +2667,13 @@
min-width: 0;
max-width: none;
}
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_list_field_handle {
+ color: transparent;
+}
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_readonly {
+ padding: 4px 6px 3px;
+ text-align: left;
+}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea {
height: 27px;
-moz-border-radius: 0;
@@ -2678,9 +2685,14 @@
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field select {
min-width: 0;
}
-.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input {
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float.oe_readonly, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer.oe_readonly {
+ padding: 6px 0px 0px;
text-align: right;
+ max-width: 100px;
+}
+.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input {
width: 100% !important;
+ text-align: right;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master {
width: 100% !important;
@@ -3324,6 +3336,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 {
@@ -3447,6 +3462,39 @@ input[type="radio"], input[type="checkbox"] {
opacity: 0.6;
}
+/* ---- EDITOR TOUR ---- {{{ */
+div.tour-backdrop {
+ z-index: 2009;
+}
+
+.popover.tour.orphan .arrow {
+ display: none;
+}
+.popover.tour .popover-navigation {
+ padding: 9px 14px;
+}
+.popover.tour .popover-navigation *[data-role="end"] {
+ float: right;
+}
+.popover.tour .popover-navigation *[data-role="next"], .popover.tour .popover-navigation *[data-role="end"] {
+ cursor: pointer;
+}
+
+.popover.fixed {
+ position: fixed;
+}
+
+.tour-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1100;
+ background-color: black;
+ opacity: 0.8;
+}
+
body {
overflow: auto;
}
diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass
index 093ebf6cb0b..07d683268f1 100644
--- a/addons/web/static/src/css/base.sass
+++ b/addons/web/static/src/css/base.sass
@@ -1902,7 +1902,7 @@ $sheet-padding: 16px
.oe_form_field_image_controls
position: absolute
top: 1px
- padding: 4px 0
+ padding: 6px 0
width: 100%
display: none
text-align: center
@@ -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
@@ -2138,7 +2138,7 @@ $sheet-padding: 16px
.oe_list_editable .oe_list_content td.oe_list_field_cell
padding: 4px 6px 3px
.oe_list.oe_list_editable.oe_editing
- .oe_edition .oe_list_field_cell:not(.oe_readonly)
+ .oe_edition .oe_list_field_cell
*
visibility: hidden
color: transparent
@@ -2150,6 +2150,11 @@ $sheet-padding: 16px
.oe_input_icon
margin-top: 5px
.oe_form_field
+ &.oe_list_field_handle
+ color: transparent
+ &.oe_readonly
+ padding: 4px 6px 3px
+ text-align: left
min-width: 0
max-width: none
input, textarea
@@ -2160,9 +2165,13 @@ $sheet-padding: 16px
input, textarea, select
min-width: 0
&.oe_form_field_float,&.oe_form_view_integer
- input
+ &.oe_readonly
+ padding: 6px 0px 0px
text-align: right
+ max-width: 100px
+ input
width: 100% !important
+ text-align: right
&.oe_form_field_datetime,&.oe_form_field_date
input.oe_datepicker_master
width: 100% !important
@@ -2691,6 +2700,8 @@ body.oe_single_form
> .arrow span
background-color: #729fcf !important
+ .oe_webclient
+ height: auto !important
// }}}
// @media print {{{
@@ -2799,6 +2810,33 @@ input[type="radio"], input[type="checkbox"]
background-color: black
opacity: 0.6000000238418579
+/* ---- EDITOR TOUR ---- {{{ */
+
+div.tour-backdrop
+ z-index: 2009
+.popover.tour
+ &.orphan .arrow
+ display: none
+ .popover-navigation
+ padding: 9px 14px
+ *[data-role="end"]
+ float: right
+ *[data-role="next"],*[data-role="end"]
+ cursor: pointer
+.popover.fixed
+ position: fixed
+.tour-backdrop
+ position: fixed
+ top: 0
+ right: 0
+ bottom: 0
+ left: 0
+ z-index: 1100
+ background-color: #000
+ opacity: 0.8
+
+
+// }}}
body
overflow: auto
diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js
index 3c32dbe0426..f08dc31021a 100644
--- a/addons/web/static/src/js/formats.js
+++ b/addons/web/static/src/js/formats.js
@@ -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);
diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js
index 5653447566b..ac441d8eed6 100644
--- a/addons/web/static/src/js/search.js
+++ b/addons/web/static/src/js/search.js
@@ -346,11 +346,11 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
'keydown .oe_searchview_input, .oe_searchview_facet': function (e) {
switch(e.which) {
case $.ui.keyCode.LEFT:
- this.focusPreceding(this);
+ this.focusPreceding(e.target);
e.preventDefault();
break;
case $.ui.keyCode.RIGHT:
- this.focusFollowing(this);
+ this.focusFollowing(e.target);
e.preventDefault();
break;
}
diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js
new file mode 100644
index 00000000000..961fd63f4cd
--- /dev/null
+++ b/addons/web/static/src/js/tour.js
@@ -0,0 +1,545 @@
+(function () {
+ 'use strict';
+
+// raise an error in test mode if openerp don't exist
+if (typeof openerp === "undefined") {
+ var error = "openerp is undefined"
+ + "\nhref: " + window.location.href
+ + "\nreferrer: " + document.referrer
+ + "\nlocalStorage: " + window.localStorage.getItem("tour");
+ if (typeof $ !== "undefined") {
+ error += '\n\n' + $("body").html();
+ }
+ throw new Error(error);
+}
+
+var website = openerp.website;
+
+// don't rewrite T in test mode
+if (typeof openerp.Tour !== "undefined") {
+ return;
+}
+
+/////////////////////////////////////////////////
+
+
+/* jQuery selector to match exact text inside an element
+ * :containsExact() - case insensitive
+ * :containsExactCase() - case sensitive
+ * :containsRegex() - set by user ( use: $(el).find(':containsRegex(/(red|blue|yellow)/gi)') )
+ */
+$.extend($.expr[':'],{
+ containsExact: function(a,i,m){
+ return $.trim(a.innerHTML.toLowerCase()) === m[3].toLowerCase();
+ },
+ containsExactCase: function(a,i,m){
+ return $.trim(a.innerHTML) === m[3];
+ },
+ // Note all escaped characters need to be double escaped
+ // inside of the containsRegex, so "\(" needs to be "\\("
+ containsRegex: function(a,i,m){
+ var regreg = /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})$/,
+ reg = regreg.exec(m[3]);
+ return reg ? new RegExp(reg[1], reg[2]).test($.trim(a.innerHTML)) : false;
+ }
+});
+$.ajaxSetup({
+ beforeSend:function(){
+ $.ajaxBusy = ($.ajaxBusy|0) + 1;
+ },
+ complete:function(){
+ $.ajaxBusy--;
+ }
+});
+
+/////////////////////////////////////////////////
+
+var localStorage = window.localStorage;
+
+var Tour = {
+ tours: {},
+ defaultDelay: 50,
+ retryRunningDelay: 1000,
+ errorDelay: 5000,
+ state: null,
+ $element: null,
+ timer: null,
+ testtimer: null,
+ currentTimer: null,
+ register: function (tour) {
+ if (tour.mode !== "test") tour.mode = "tutorial";
+ Tour.tours[tour.id] = tour;
+ },
+ run: function (tour_id, mode) {
+ var tour = Tour.tours[tour_id];
+ if (!tour) {
+ Tour.error(null, "Can't run '"+tour_id+"' (tour undefined)");
+ }
+ this.time = new Date().getTime();
+ if (tour.path && !window.location.href.match(new RegExp("("+Tour.getLang()+")?"+tour.path+"#?$", "i"))) {
+ var href = Tour.getLang()+tour.path;
+ console.log("Tour '"+tour_id+"' Begin from run method (redirection to "+href+")");
+ Tour.saveState(tour.id, mode || tour.mode, -1, 0);
+ $(document).one("ajaxStop", Tour.running);
+ window.location.href = href;
+ } else {
+ console.log("Tour '"+tour_id+"' Begin from run method");
+ Tour.saveState(tour.id, mode || tour.mode, 0, 0);
+ Tour.running();
+ }
+ },
+ registerSteps: function (tour, mode) {
+ if (tour.register) {
+ return;
+ }
+ tour.register = true;
+
+ for (var index=0, len=tour.steps.length; index 0 && tour.steps[index-1] &&
+ tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
+ step.waitNot = '.popover.tour.fade.in:visible';
+ }
+ if (!step.waitFor && index > 0 && tour.steps[index-1].snippet) {
+ step.waitFor = '.oe_overlay_options .oe_options:visible';
+ }
+
+
+ var snippet = step.element && step.element.match(/#oe_snippets (.*) \.oe_snippet_thumbnail/);
+ if (snippet) {
+ step.snippet = snippet[1];
+ } else if (step.snippet) {
+ step.element = '#oe_snippets '+step.snippet+' .oe_snippet_thumbnail';
+ }
+
+ if (!step.element) {
+ step.element = "body";
+ step.orphan = true;
+ step.backdrop = true;
+ } else {
+ step.popover = step.popover || {};
+ step.popover.arrow = true;
+ }
+ }
+ if (tour.steps[index-1] &&
+ tour.steps[index-1].popover && tour.steps[index-1].popover.next) {
+ var step = {
+ _title: "close popover and finish",
+ id: index,
+ waitNot: '.popover.tour.fade.in:visible'
+ };
+ tour.steps.push(step);
+ }
+
+ // rendering bootstrap tour and popover
+ if (mode !== "test") {
+ for (var index=0, len=tour.steps.length; index');
+ }
+
+ if (step.backdrop || $element.parents("#website-top-navbar, .oe_navbar, .modal").size()) {
+ $tip.css("z-index", 2010);
+ }
+
+ // button click event
+ $tip.find("button")
+ .one("click", function () {
+ step.busy = true;
+ if (!$(this).is("[data-role='next']")) {
+ clearTimeout(Tour.timer);
+ Tour.endTour();
+ }
+ Tour.closePopover();
+ });
+
+ Tour.repositionPopover();
+ },
+ repositionPopover: function() {
+ var popover = Tour.$element.data("bs.popover");
+ var $tip = Tour.$element.data("bs.popover").tip();
+
+ if (popover.options.orphan) {
+ return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2);
+ }
+
+ var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset;
+ offsetWidth = $tip[0].offsetWidth;
+ offsetHeight = $tip[0].offsetHeight;
+ tipOffset = $tip.offset();
+ originalLeft = tipOffset.left;
+ originalTop = tipOffset.top;
+ offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight();
+ if (offsetBottom < 0) {
+ tipOffset.top = tipOffset.top + offsetBottom;
+ }
+ offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth();
+ if (offsetRight < 0) {
+ tipOffset.left = tipOffset.left + offsetRight;
+ }
+ if (tipOffset.top < 0) {
+ tipOffset.top = 0;
+ }
+ if (tipOffset.left < 0) {
+ tipOffset.left = 0;
+ }
+ $tip.offset(tipOffset);
+ if (popover.options.placement === "bottom" || popover.options.placement === "top") {
+ var left = Tour.$element.offset().left + Tour.$element.outerWidth()/2 - tipOffset.left;
+ $tip.find(".arrow").css("left", left ? left + "px" : "");
+ } else if (popover.options.placement !== "auto") {
+ var top = Tour.$element.offset().top + Tour.$element.outerHeight()/2 - tipOffset.top;
+ $tip.find(".arrow").css("top", top ? top + "px" : "");
+ }
+ },
+ _load_template: false,
+ load_template: function () {
+ // don't need template to use bootstrap Tour in automatic mode
+ Tour._load_template = true;
+ if (typeof QWeb2 === "undefined") return $.when();
+ var def = $.Deferred();
+ openerp.qweb.add_template('/web/static/src/xml/website.tour.xml', function(err) {
+ if (err) {
+ def.reject(err);
+ } else {
+ def.resolve();
+ }
+ });
+ return def;
+ },
+ popoverTitle: function (tour, options) {
+ return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover_title', options) : options.title;
+ },
+ popover: function (options) {
+ return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover', options) : options.title;
+ },
+ getLang: function () {
+ return $("html").attr("lang") ? "/" + $("html").attr("lang").replace(/-/, '_') : "";
+ },
+ getState: function () {
+ var state = JSON.parse(localStorage.getItem("tour") || 'false') || {};
+ if (state) { this.time = state.time; }
+ var tour_id,mode,step_id;
+ if (!state.id && window.location.href.indexOf("#tutorial.") > -1) {
+ state = {
+ "id": window.location.href.match(/#tutorial\.(.*)=true/)[1],
+ "mode": "tutorial",
+ "step_id": 0
+ };
+ window.location.hash = "";
+ console.log("Tour '"+state.id+"' Begin from url hash");
+ Tour.saveState(state.id, state.mode, state.step_id, 0);
+ }
+ if (!state.id) {
+ return;
+ }
+ state.tour = Tour.tours[state.id];
+ state.step = state.tour && state.tour.steps[state.step_id === -1 ? 0 : state.step_id];
+ return state;
+ },
+ error: function (step, message) {
+ var state = Tour.getState();
+ message += '\n tour: ' + state.id
+ + (step ? '\n step: ' + step.id + ": '" + (step._title || step.title) + "'" : '' )
+ + '\n href: ' + window.location.href
+ + '\n referrer: ' + document.referrer
+ + (step ? '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) : '' )
+ + (step ? '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size()) : '' )
+ + (step ? '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size()) : '' )
+ + "\n localStorage: " + JSON.stringify(localStorage)
+ + '\n\n' + $("body").html();
+ Tour.reset();
+ if (state.mode === "test") {
+ throw new Error(message);
+ }
+ },
+ lists: function () {
+ var tour_ids = [];
+ for (var k in Tour.tours) {
+ tour_ids.push(k);
+ }
+ return tour_ids;
+ },
+ saveState: function (tour_id, mode, step_id, number, wait) {
+ localStorage.setItem("tour", JSON.stringify({
+ "id":tour_id,
+ "mode":mode,
+ "step_id":step_id || 0,
+ "time": this.time,
+ "number": number+1,
+ "wait": wait || 0
+ }));
+ },
+ reset: function () {
+ var state = Tour.getState();
+ if (state && state.tour) {
+ for (var k in state.tour.steps) {
+ state.tour.steps[k].busy = false;
+ }
+ }
+ localStorage.removeItem("tour");
+ clearTimeout(Tour.timer);
+ clearTimeout(Tour.testtimer);
+ Tour.closePopover();
+ },
+ running: function () {
+ var state = Tour.getState();
+ if (!state) return;
+ else if (state.tour) {
+ if (!Tour._load_template) {
+ Tour.load_template().then(Tour.running);
+ return;
+ }
+ console.log("Tour '"+state.id+"' is running");
+ Tour.registerSteps(state.tour, state.mode);
+ Tour.nextStep();
+ } else {
+ if (state.mode === "test" && state.wait >= 10) {
+ Tour.error(state.step, "Tour '"+state.id+"' undefined");
+ }
+ Tour.saveState(state.id, state.mode, state.step_id, state.number-1, state.wait+1);
+ console.log("Tour '"+state.id+"' wait for running (tour undefined)");
+ setTimeout(Tour.running, Tour.retryRunningDelay);
+ }
+ },
+ check: function (step) {
+ return (step &&
+ (!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) &&
+ (!step.waitNot || !$(step.waitNot).size()) &&
+ (!step.waitFor || $(step.waitFor).size()));
+ },
+ waitNextStep: function () {
+ var state = Tour.getState();
+ var time = new Date().getTime();
+ var timer;
+ var next = state.tour.steps[state.step.id+1];
+ var overlaps = state.mode === "test" ? Tour.errorDelay : 0;
+
+ window.onbeforeunload = function () {
+ clearTimeout(Tour.timer);
+ clearTimeout(Tour.testtimer);
+ };
+
+ function checkNext () {
+ Tour.autoTogglePopover();
+
+ clearTimeout(Tour.timer);
+ if (Tour.check(next)) {
+ clearTimeout(Tour.currentTimer);
+ // use an other timeout for cke dom loading
+ Tour.saveState(state.id, state.mode, state.step.id, 0);
+ setTimeout(function () {
+ Tour.nextStep(next);
+ }, Tour.defaultDelay);
+ } else if (!overlaps || new Date().getTime() - time < overlaps) {
+ Tour.timer = setTimeout(checkNext, Tour.defaultDelay);
+ } else {
+ Tour.error(next, "Can't reach the next step");
+ }
+ }
+ checkNext();
+ },
+ nextStep: function (step) {
+ var state = Tour.getState();
+
+ if (!state) {
+ return;
+ }
+
+ step = step || state.step;
+ var next = state.tour.steps[step.id+1];
+
+ if (state.mode === "test" && state.number > 3) {
+ Tour.error(next, "Cycling. Can't reach the next step");
+ }
+
+ Tour.saveState(state.id, state.mode, step.id, state.number);
+
+ if (step.id !== state.step_id) {
+ console.log("Tour '"+state.id+"' Step: '" + (step._title || step.title) + "' (" + (new Date().getTime() - this.time) + "ms)");
+ }
+
+ Tour.autoTogglePopover(true);
+
+ if (step.onload) {
+ step.onload();
+ }
+
+ if (next) {
+ setTimeout(function () {
+ if (Tour.getState()) {
+ Tour.waitNextStep();
+ }
+ if (state.mode === "test") {
+ setTimeout(function(){
+ Tour.autoNextStep(state.tour, step);
+ }, Tour.defaultDelay);
+ }
+ }, next.wait || 0);
+ } else {
+ setTimeout(function(){
+ Tour.autoNextStep(state.tour, step);
+ }, Tour.defaultDelay);
+ Tour.endTour();
+ }
+ },
+ endTour: function () {
+ var state = Tour.getState();
+ var test = state.step.id >= state.tour.steps.length-1;
+ Tour.reset();
+ if (test) {
+ console.log("Tour '"+state.id+"' finish: ok");
+ console.log('ok');
+ } else {
+ console.log("Tour '"+state.id+"' finish: error");
+ console.log('error');
+ }
+ },
+ autoNextStep: function (tour, step) {
+ clearTimeout(Tour.testtimer);
+
+ function autoStep () {
+ if (!step) return;
+
+ if (step.autoComplete) {
+ step.autoComplete(tour);
+ }
+
+ $(".popover.tour [data-role='next']").click();
+
+ var $element = $(step.element);
+ if (!$element.size()) return;
+
+ if (step.snippet) {
+
+ Tour.autoDragAndDropSnippet($element);
+
+ } else if ($element.is(":visible")) {
+
+ $element.trigger($.Event("mouseenter", { srcElement: $element[0] }));
+ $element.trigger($.Event("mousedown", { srcElement: $element[0] }));
+
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ $element[0].dispatchEvent(evt);
+
+ // trigger after for step like: mouseenter, next step click on button display with mouseenter
+ setTimeout(function () {
+ $element.trigger($.Event("mouseup", { srcElement: $element[0] }));
+ $element.trigger($.Event("mouseleave", { srcElement: $element[0] }));
+ }, 1000);
+ }
+ if (step.sampleText) {
+
+ $element.trigger($.Event("keydown", { srcElement: $element }));
+ if ($element.is("input") ) {
+ $element.val(step.sampleText);
+ } if ($element.is("select")) {
+ $element.find("[value='"+step.sampleText+"'], option:contains('"+step.sampleText+"')").attr("selected", true);
+ $element.val(step.sampleText);
+ } else {
+ $element.html(step.sampleText);
+ }
+ setTimeout(function () {
+ $element.trigger($.Event("keyup", { srcElement: $element }));
+ $element.trigger($.Event("change", { srcElement: $element }));
+ }, self.defaultDelay<<1);
+
+ }
+ }
+ Tour.testtimer = setTimeout(autoStep, 100);
+ },
+ autoDragAndDropSnippet: function (selector) {
+ var $thumbnail = $(selector).first();
+ var thumbnailPosition = $thumbnail.position();
+ $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top }));
+ $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: document.body.scrollWidth/2, pageY: document.body.scrollHeight/2 }));
+ var $dropZone = $(".oe_drop_zone").first();
+ var dropPosition = $dropZone.position();
+ $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top }));
+ }
+};
+openerp.Tour = Tour;
+
+/////////////////////////////////////////////////
+
+$(document).ready(Tour.running);
+
+}());
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index e276a5e3995..956ec494573 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -2632,6 +2632,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
type_of_date: "datetime",
events: {
'change .oe_datepicker_master': 'change_datetime',
+ 'keypress .oe_datepicker_master': 'change_datetime',
},
init: function(parent) {
this._super(parent);
@@ -2750,8 +2751,8 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
format_client: function(v) {
return instance.web.format_value(v, {"widget": this.type_of_date});
},
- change_datetime: function() {
- if (this.is_valid_()) {
+ change_datetime: function(e) {
+ if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
this.set_value_from_ui_();
this.trigger("datetime_changed");
}
@@ -2809,7 +2810,9 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc
},
set_dimensions: function (height, width) {
this._super(height, width);
- this.datewidget.$input.css('height', height);
+ if (!this.get("effective_readonly")) {
+ this.datewidget.$input.css('height', height);
+ }
}
});
@@ -3509,6 +3512,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
this.floating = false;
this.current_display = null;
this.is_started = false;
+ this.ignore_focusout = false;
},
reinit_value: function(val) {
this.internal_set_value(val);
@@ -3640,6 +3644,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
var ed_delay = 200;
var ed_duration = 15000;
var anyoneLoosesFocus = function (e) {
+ if (self.ignore_focusout) { return; }
var used = false;
if (self.floating) {
if (self.last_search.length > 0) {
@@ -3843,11 +3848,17 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc
_search_create_popup: function() {
this.no_ed = true;
this.ed_def.reject();
- return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
+ this.ignore_focusout = true;
+ this.reinit_value(false);
+ var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
+ this.ignore_focusout = false;
+ this.no_ed = false;
+ return res;
},
set_dimensions: function (height, width) {
this._super(height, width);
- this.$input.css('height', height);
+ if (!this.get("effective_readonly") && this.$input)
+ this.$input.css('height', height);
}
});
@@ -5505,9 +5516,13 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
this._super.apply(this, arguments);
},
initialize_content: function() {
+ var self= this;
this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
+ this.$el.find('.oe_form_binary_file_edit').click(function(event){
+ self.$el.find('input.oe_form_binary_file').click();
+ });
},
on_file_change: function(e) {
var self = this;
@@ -5675,8 +5690,6 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
return;
$img.css("max-width", "" + self.options.size[0] + "px");
$img.css("max-height", "" + self.options.size[1] + "px");
- $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
- $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
});
$img.on('error', function() {
$img.attr('src', self.placeholder);
diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js
index e2c63eb191a..144de8eb21c 100644
--- a/addons/web/static/src/js/view_list_editable.js
+++ b/addons/web/static/src/js/view_list_editable.js
@@ -130,15 +130,7 @@
if (this.editable()) {
this.$el.find('table:first').show();
this.$el.find('.oe_view_nocontent').remove();
- this.start_edition().then(function(){
- var fields = self.editor.form.fields;
- self.editor.form.fields_order.some(function(field){
- if (fields[field].$el.is(':visible')){
- fields[field].$el.find("input").select();
- return true;
- }
- });
- });
+ this.start_edition();
} else {
this._super();
}
@@ -243,6 +235,7 @@
return this.ensure_saved().then(function () {
var $recordRow = self.groups.get_row_for(record);
var cells = self.get_cells_for($recordRow);
+ var fields = {};
self.fields_for_resize.splice(0, self.fields_for_resize.length);
return self.with_event('edit', {
record: record.attributes,
@@ -256,10 +249,16 @@
// FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow
field.$el.attr('data-fieldname', field_name);
+ fields[field_name] = field;
self.fields_for_resize.push({field: field, cell: cell});
}, options).then(function () {
$recordRow.addClass('oe_edition');
self.resize_fields();
+ var focus_field = options && options.focus_field ? options.focus_field : undefined;
+ if (!focus_field){
+ focus_field = _.find(self.editor.form.fields_order, function(field){ return fields[field] && fields[field].$el.is(':visible:has(input)'); });
+ }
+ if (focus_field) fields[focus_field].$el.find('input').select();
return record.attributes;
});
}).fail(function () {
@@ -286,9 +285,7 @@
if (!this.editor.is_editing()) { return; }
for(var i=0, len=this.fields_for_resize.length; i
+
+
+
@@ -2038,4 +2039,5 @@
+
diff --git a/addons/website/static/src/xml/website.tour.xml b/addons/web/static/src/xml/website.tour.xml
similarity index 90%
rename from addons/website/static/src/xml/website.tour.xml
rename to addons/web/static/src/xml/website.tour.xml
index dcecef021bb..cae823bd04e 100644
--- a/addons/website/static/src/xml/website.tour.xml
+++ b/addons/web/static/src/xml/website.tour.xml
@@ -1,8 +1,8 @@
-
+
-
+
diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html
index 5734f60bc62..19ae6a23bb2 100644
--- a/addons/web/views/database_manager.html
+++ b/addons/web/views/database_manager.html
@@ -51,6 +51,7 @@
+
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 2b022d86f30..9de0d51526e 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -22,6 +22,7 @@
+
@@ -246,6 +247,7 @@
diff --git a/addons/website_mail_group/__init__.py b/addons/website_mail_group/__init__.py
index ee5959455ad..9f86759e32b 100644
--- a/addons/website_mail_group/__init__.py
+++ b/addons/website_mail_group/__init__.py
@@ -1 +1,2 @@
import controllers
+import models
diff --git a/addons/website_mail_group/controllers/main.py b/addons/website_mail_group/controllers/main.py
index a28b808d9f2..59478b37fa3 100644
--- a/addons/website_mail_group/controllers/main.py
+++ b/addons/website_mail_group/controllers/main.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
import datetime
+from dateutil import relativedelta
-from openerp import tools
+from openerp import tools, SUPERUSER_ID
from openerp.addons.web import http
from openerp.addons.website.models.website import slug
from openerp.addons.web.http import request
@@ -28,12 +29,23 @@ class MailGroup(http.Controller):
def view(self, **post):
cr, uid, context = request.cr, request.uid, request.context
group_obj = request.registry.get('mail.group')
+ mail_message_obj = request.registry.get('mail.message')
group_ids = group_obj.search(cr, uid, [('alias_id', '!=', False), ('alias_id.alias_name', '!=', False)], context=context)
- values = {'groups': group_obj.browse(cr, uid, group_ids, context)}
+ groups = group_obj.browse(cr, uid, group_ids, context)
+ # compute statistics
+ month_date = datetime.datetime.today() - relativedelta.relativedelta(months=1)
+ group_data = dict.fromkeys(group_ids, dict())
+ for group in groups:
+ group_data[group.id]['monthly_message_nbr'] = mail_message_obj.search(
+ cr, SUPERUSER_ID,
+ [('model', '=', 'mail.group'), ('res_id', '=', group.id), ('date', '>=', month_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT))],
+ count=True, context=context)
+ values = {'groups': groups, 'group_data': group_data}
return request.website.render('website_mail_group.mail_groups', values)
@http.route(["/groups/subscription/"], type='json', auth="user")
def subscription(self, group_id=0, action=False, **post):
+ """ TDE FIXME: seems dead code """
cr, uid, context = request.cr, request.uid, request.context
group_obj = request.registry.get('mail.group')
if action:
diff --git a/addons/website_mail_group/models/__init__.py b/addons/website_mail_group/models/__init__.py
new file mode 100644
index 00000000000..ea8be51acde
--- /dev/null
+++ b/addons/website_mail_group/models/__init__.py
@@ -0,0 +1 @@
+import mail_group
diff --git a/addons/website_mail_group/models/mail_group.py b/addons/website_mail_group/models/mail_group.py
new file mode 100644
index 00000000000..804785b66f9
--- /dev/null
+++ b/addons/website_mail_group/models/mail_group.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from openerp.osv import osv
+
+
+class MailGroup(osv.Model):
+ _inherit = 'mail.group'
+
+ def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
+ res = super(MailGroup, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context)
+ group = self.browse(cr, uid, id, context=context)
+ base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
+ res['headers'].update({
+ 'List-Archive': '<%s/groups/%s>' % (base_url, group.id),
+ 'List-Subscribe': '<%s/groups>' % (base_url),
+ 'List-Unsubscribe': '<%s/groups>' % (base_url),
+ })
+ return res
diff --git a/addons/website_mail_group/static/src/js/website_mail_group.snippet.js b/addons/website_mail_group/static/src/js/website_mail_group.snippet.js
index fd6a2d38cfa..8ad4f9ba796 100644
--- a/addons/website_mail_group/static/src/js/website_mail_group.snippet.js
+++ b/addons/website_mail_group/static/src/js/website_mail_group.snippet.js
@@ -3,8 +3,8 @@
var website = openerp.website;
- website.snippet.animationRegistry.follow = website.snippet.Animation.extend({
- selector: ".js_follow",
+ website.snippet.animationRegistry.follow_alias = website.snippet.Animation.extend({
+ selector: ".js_follow_alias",
start: function (editable_mode) {
var self = this;
this.is_user = false;
@@ -23,8 +23,8 @@
// not if editable mode to allow designer to edit alert field
if (!editable_mode) {
- $('.js_follow > .alert').addClass("hidden");
- $('.js_follow > .input-group-btn.hidden').removeClass("hidden");
+ $('.js_follow_alias > .alert').addClass("hidden");
+ $('.js_follow_alias > .input-group-btn.hidden').removeClass("hidden");
this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) {
event.preventDefault();
self.on_click();
diff --git a/addons/website_mail_group/views/snippets.xml b/addons/website_mail_group/views/snippets.xml
index 452f59f4902..95935cd4998 100644
--- a/addons/website_mail_group/views/snippets.xml
+++ b/addons/website_mail_group/views/snippets.xml
@@ -11,7 +11,7 @@
Discussion Group
-
@@ -38,7 +38,7 @@
diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml
index 8ecbd6c84ed..a1065027748 100644
--- a/addons/website_mail_group/views/website_mail_group.xml
+++ b/addons/website_mail_group/views/website_mail_group.xml
@@ -45,7 +45,7 @@
participants
- messages
+ messages / month
diff --git a/addons/website_report/views/layouts.xml b/addons/website_report/views/layouts.xml
index 9978d994821..03df41266be 100644
--- a/addons/website_report/views/layouts.xml
+++ b/addons/website_report/views/layouts.xml
@@ -62,8 +62,6 @@
-
-
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 23225186dc8..a6bcd15f9e5 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -55,7 +55,7 @@ class table_compute(object):
self.table[(pos/PPR)+y2][(pos%PPR)+x2] = False
self.table[pos/PPR][pos%PPR] = {
'product': p, 'x':x, 'y': y,
- 'class': " ".join(map(lambda x: x.html_class, p.website_style_ids))
+ 'class': " ".join(map(lambda x: x.html_class or '', p.website_style_ids))
}
if index<=PPG:
maxy=max(maxy,y+(pos/PPR))
@@ -169,7 +169,7 @@ class website_sale(http.Controller):
values = {
'search': search,
- 'category': category and int(category),
+ 'category': category,
'attrib_values': attrib_values,
'attrib_set': attrib_set,
'pager': pager,
diff --git a/addons/website_sale/static/src/js/website.tour.sale.js b/addons/website_sale/static/src/js/website.tour.sale.js
index 8cfb4e2cdad..57365a826a6 100644
--- a/addons/website_sale/static/src/js/website.tour.sale.js
+++ b/addons/website_sale/static/src/js/website.tour.sale.js
@@ -1,9 +1,6 @@
(function () {
'use strict';
-
- var website = openerp.website;
-
- website.Tour.register({
+ openerp.Tour.register({
id: 'shop_customize',
name: "Customize the page and search a product",
path: '/shop',
@@ -37,7 +34,7 @@
]
});
- website.Tour.register({
+ openerp.Tour.register({
id: 'shop_buy_product',
name: "Try to buy products",
path: '/shop',
diff --git a/addons/website_sale/static/src/js/website.tour.shop.js b/addons/website_sale/static/src/js/website.tour.shop.js
index 3dd5b54b357..c39a8b2f9e8 100644
--- a/addons/website_sale/static/src/js/website.tour.shop.js
+++ b/addons/website_sale/static/src/js/website.tour.shop.js
@@ -1,10 +1,9 @@
(function () {
'use strict';
- var website = openerp.website;
var _t = openerp._t;
- website.Tour.register({
+ openerp.Tour.register({
id: 'shop',
name: _t("Create a product"),
steps: [
diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py
index b37b514a825..a57a2d64bca 100644
--- a/addons/website_sale/tests/test_sale_process.py
+++ b/addons/website_sale/tests/test_sale_process.py
@@ -3,22 +3,22 @@ import os
import openerp.tests
inject = [
- ("openerp.website.Tour", os.path.join(os.path.dirname(__file__), '../../website/static/src/js/website.tour.js')),
- ("openerp.website.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.js")),
+ ("openerp.Tour", os.path.join(os.path.dirname(__file__), '../../web/static/src/js/tour.js')),
+ ("openerp.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.js")),
]
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestUi(openerp.tests.HttpCase):
def test_01_admin_shop_tour(self):
- self.phantom_js("/", "openerp.website.Tour.run('shop', 'test')", "openerp.website.Tour.tours.shop", login="admin")
+ self.phantom_js("/", "openerp.Tour.run('shop', 'test')", "openerp.Tour.tours.shop", login="admin")
def test_02_admin_checkout(self):
- self.phantom_js("/", "openerp.website.Tour.run('shop_customize', 'test')", "openerp.website.Tour.tours.shop_customize", login="admin", inject=inject)
- self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="admin", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_customize', 'test')", "openerp.Tour.tours.shop_customize", login="admin", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", login="admin", inject=inject)
def test_03_demo_checkout(self):
- self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="demo", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", login="demo", inject=inject)
def test_04_public_checkout(self):
- self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", inject=inject)
+ self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", inject=inject)
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index c43363b863e..fb5a83de1bd 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -240,7 +240,7 @@
-
+
diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py
index c41eae84ff8..3c84533e716 100644
--- a/openerp/addons/base/ir/ir_actions.py
+++ b/openerp/addons/base/ir/ir_actions.py
@@ -320,22 +320,24 @@ class ir_actions_act_window(osv.osv):
ids = [ids]
results = super(ir_actions_act_window, self).read(cr, uid, ids, fields=fields, context=context, load=load)
- if not fields or 'help' in fields:
- context = dict(context or {})
- eval_dict = {
- 'active_model': context.get('active_model'),
- 'active_id': context.get('active_id'),
- 'active_ids': context.get('active_ids'),
- 'uid': uid,
- }
- for res in results:
- model = res.get('res_model')
- if model and self.pool.get(model):
- try:
- with tools.mute_logger("openerp.tools.safe_eval"):
- eval_context = eval(res['context'] or "{}", eval_dict) or {}
- except Exception:
- continue
+ context = dict(context or {})
+ eval_dict = {
+ 'active_model': context.get('active_model'),
+ 'active_id': context.get('active_id'),
+ 'active_ids': context.get('active_ids'),
+ 'uid': uid,
+ 'context': context,
+ }
+ for res in results:
+ model = res.get('res_model')
+ if model and self.pool.get(model):
+ try:
+ with tools.mute_logger("openerp.tools.safe_eval"):
+ eval_context = eval(res['context'] or "{}", eval_dict) or {}
+ res['context'] = str(eval_context)
+ except Exception:
+ continue
+ if not fields or 'help' in fields:
custom_context = dict(context, **eval_context)
res['help'] = self.pool.get(model).get_empty_list_help(cr, uid, res.get('help', ""), context=custom_context)
if ids_int:
@@ -489,6 +491,8 @@ class ir_actions_server(osv.osv):
"based on the sequence. Low number means high priority."),
'model_id': fields.many2one('ir.model', 'Base Model', required=True, ondelete='cascade',
help="Base model on which the server action runs."),
+ 'model_name': fields.related('model_id', 'model', type='char',
+ string='Model Name', readonly=True),
'menu_ir_values_id': fields.many2one('ir.values', 'More Menu entry', readonly=True,
help='More menu entry.'),
# Client Action
@@ -648,6 +652,10 @@ class ir_actions_server(osv.osv):
'wkf_field_id': False,
'crud_model_id': model_id,
}
+
+ if model_id:
+ values['model_name'] = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
+
return {'value': values}
def on_change_wkf_wonfig(self, cr, uid, ids, use_relational_model, wkf_field_id, wkf_model_id, model_id, context=None):
@@ -751,6 +759,7 @@ class ir_actions_server(osv.osv):
crud_model_name = False
if crud_model_id:
crud_model_name = self.pool.get('ir.model').browse(cr, uid, crud_model_id, context).model
+
values = {'link_field_id': False, 'crud_model_name': crud_model_name}
return {'value': values}
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 6cb498e9ebf..2aa520d7c55 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -349,8 +349,9 @@
Check to attach the newly created record to the record on which the server action runs.
+
diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py
index ac3091e33b9..21c8b7ef941 100644
--- a/openerp/addons/base/ir/ir_http.py
+++ b/openerp/addons/base/ir/ir_http.py
@@ -76,17 +76,23 @@ class ir_http(osv.AbstractModel):
request.uid = request.session.uid
def _authenticate(self, auth_method='user'):
- if request.session.uid:
- try:
- request.session.check_security()
- # what if error in security.check()
- # -> res_users.check()
- # -> res_users.check_credentials()
- except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
- # All other exceptions mean undetermined status (e.g. connection pool full),
- # let them bubble up
- request.session.logout()
- getattr(self, "_auth_method_%s" % auth_method)()
+ try:
+ if request.session.uid:
+ try:
+ request.session.check_security()
+ # what if error in security.check()
+ # -> res_users.check()
+ # -> res_users.check_credentials()
+ except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
+ # All other exceptions mean undetermined status (e.g. connection pool full),
+ # let them bubble up
+ request.session.logout()
+ getattr(self, "_auth_method_%s" % auth_method)()
+ except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
+ raise
+ except Exception:
+ _logger.exception("Exception during request Authentication.")
+ raise openerp.exceptions.AccessDenied()
return auth_method
def _handle_exception(self, exception):
diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py
index eb154d4a05f..df673f95b32 100644
--- a/openerp/addons/base/module/module.py
+++ b/openerp/addons/base/module/module.py
@@ -496,6 +496,7 @@ class module(osv.osv):
'params': {'menu_id': menu_ids and menu_ids[0] or False}
}
+ #TODO remove me in master, not called anymore
def button_immediate_uninstall(self, cr, uid, ids, context=None):
"""
Uninstall the selected module(s) immediately and fully,
diff --git a/openerp/addons/base/module/module_view.xml b/openerp/addons/base/module/module_view.xml
index 3f92c79e2cf..7cc46539e8f 100644
--- a/openerp/addons/base/module/module_view.xml
+++ b/openerp/addons/base/module/module_view.xml
@@ -82,8 +82,7 @@
-
+
diff --git a/openerp/addons/base/module/wizard/base_module_upgrade.py b/openerp/addons/base/module/wizard/base_module_upgrade.py
index c75058ab835..365895be7ac 100644
--- a/openerp/addons/base/module/wizard/base_module_upgrade.py
+++ b/openerp/addons/base/module/wizard/base_module_upgrade.py
@@ -68,6 +68,20 @@ class base_module_upgrade(osv.osv_memory):
res = mod_obj.read(cr, uid, ids, ['name','state'], context)
return {'module_info': '\n'.join(map(lambda x: x['name']+' : '+x['state'], res))}
+ def upgrade_module_cancel(self, cr, uid, ids, context=None):
+ mod_obj = self.pool.get('ir.module.module')
+ to_installed_ids = mod_obj.search(cr, uid, [
+ ('state', 'in', ['to upgrade', 'to remove'])])
+ if to_installed_ids:
+ mod_obj.write(cr, uid, to_installed_ids, {'state': 'installed'}, context=context)
+
+ to_uninstalled_ids = mod_obj.search(cr, uid, [
+ ('state', '=', 'to install')])
+ if to_uninstalled_ids:
+ mod_obj.write(cr, uid, to_uninstalled_ids, {'state': 'uninstalled'}, context=context)
+
+ return {'type': 'ir.actions.act_window_close'}
+
def upgrade_module(self, cr, uid, ids, context=None):
ir_module = self.pool.get('ir.module.module')
diff --git a/openerp/addons/base/module/wizard/base_module_upgrade_view.xml b/openerp/addons/base/module/wizard/base_module_upgrade_view.xml
index 25e7ac0a3df..ae135b8fa90 100644
--- a/openerp/addons/base/module/wizard/base_module_upgrade_view.xml
+++ b/openerp/addons/base/module/wizard/base_module_upgrade_view.xml
@@ -7,14 +7,15 @@
base.module.upgrade
diff --git a/openerp/addons/base/res/res_bank.py b/openerp/addons/base/res/res_bank.py
index cc29c50928a..e8477ec18ff 100644
--- a/openerp/addons/base/res/res_bank.py
+++ b/openerp/addons/base/res/res_bank.py
@@ -128,8 +128,7 @@ class res_partner_bank(osv.osv):
change_default=True, domain="[('country_id','=',country_id)]"),
'company_id': fields.many2one('res.company', 'Company',
ondelete='cascade', help="Only if this bank account belong to your company"),
- 'partner_id': fields.many2one('res.partner', 'Account Owner', required=True,
- ondelete='cascade', select=True),
+ 'partner_id': fields.many2one('res.partner', 'Account Owner', ondelete='cascade', select=True),
'state': fields.selection(_bank_type_get, 'Bank Account Type', required=True,
change_default=True),
'sequence': fields.integer('Sequence'),
diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py
index 767d9fcd705..56065465441 100644
--- a/openerp/addons/base/res/res_config.py
+++ b/openerp/addons/base/res/res_config.py
@@ -28,6 +28,7 @@ from openerp.osv import osv, fields
from openerp.tools import ustr
from openerp.tools.translate import _
from openerp import exceptions
+from lxml import etree
_logger = logging.getLogger(__name__)
@@ -434,6 +435,43 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
def copy(self, cr, uid, id, values, context=None):
raise osv.except_osv(_("Cannot duplicate configuration!"), "")
+ def fields_view_get(self, cr, user, view_id=None, view_type='form',
+ context=None, toolbar=False, submenu=False):
+ ret_val = super(res_config_settings, self).fields_view_get(
+ cr, user, view_id=view_id, view_type=view_type, context=context,
+ toolbar=toolbar, submenu=submenu)
+
+ doc = etree.XML(ret_val['arch'])
+
+ for field in ret_val['fields']:
+ if not field.startswith("module_"):
+ continue
+ for node in doc.xpath("//field[@name='%s']" % field):
+ if 'on_change' not in node.attrib:
+ node.set("on_change",
+ "onchange_module(%s, '%s')" % (field, field))
+
+ ret_val['arch'] = etree.tostring(doc)
+ return ret_val
+
+ def onchange_module(self, cr, uid, ids, field_value, module_name, context={}):
+ module_pool = self.pool.get('ir.module.module')
+ module_ids = module_pool.search(
+ cr, uid, [('name', '=', module_name.replace("module_", '')),
+ ('state','in', ['to install', 'installed', 'to upgrade'])],
+ context=context)
+
+ if module_ids and not field_value:
+ dep_ids = module_pool.downstream_dependencies(cr, uid, module_ids, context=context)
+ dep_name = [x.shortdesc for x in module_pool.browse(
+ cr, uid, dep_ids + module_ids, context=context)]
+ message = '\n'.join(dep_name)
+ return {'warning': {'title': _('Warning!'),
+ 'message':
+ _('Disabling this option will also uninstall the following modules \n%s' % message)
+ }}
+ return {}
+
def _get_classified_fields(self, cr, uid, context=None):
""" return a dictionary with the fields classified by category::
diff --git a/openerp/http.py b/openerp/http.py
index 44df279bdb9..fcbd2581350 100644
--- a/openerp/http.py
+++ b/openerp/http.py
@@ -270,8 +270,6 @@ class WebRequest(object):
to abitrary responses. Anything returned (except None) will
be used as response."""
self._failed = exception # prevent tx commit
- if isinstance(exception, werkzeug.exceptions.HTTPException):
- return exception
raise
def _call_function(self, *args, **kwargs):
@@ -538,6 +536,15 @@ class HttpRequest(WebRequest):
params.pop('session_id', None)
self.params = params
+ def _handle_exception(self, exception):
+ """Called within an except block to allow converting exceptions
+ to abitrary responses. Anything returned (except None) will
+ be used as response."""
+ try:
+ return super(HttpRequest, self)._handle_exception(exception)
+ except werkzeug.exceptions.HTTPException, e:
+ return e
+
def dispatch(self):
if request.httprequest.method == 'OPTIONS' and request.endpoint and request.endpoint.routing.get('cors'):
headers = {
diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py
index 76a9f046dc6..b0f6141e78f 100644
--- a/openerp/modules/graph.py
+++ b/openerp/modules/graph.py
@@ -78,7 +78,7 @@ class Graph(dict):
)
## and we update the default values with values from the database
- additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
+ additional_data.update((x['name'], x) for x in cr.dictfetchall())
for package in self.values():
for k, v in additional_data[package.name].items():
diff --git a/openerp/modules/module.py b/openerp/modules/module.py
index 8f34a74f882..cd21e681a8b 100644
--- a/openerp/modules/module.py
+++ b/openerp/modules/module.py
@@ -384,11 +384,12 @@ class TestStream(object):
if self.r.match(s):
return
first = True
- for c in s.split('\n'):
+ level = logging.ERROR if s.startswith(('ERROR', 'FAIL', 'Traceback')) else logging.INFO
+ for c in s.splitlines():
if not first:
c = '` ' + c
first = False
- self.logger.info(c)
+ self.logger.log(level, c)
current_test = None
diff --git a/openerp/netsvc.py b/openerp/netsvc.py
index 391de55a32e..e6c05bf7298 100644
--- a/openerp/netsvc.py
+++ b/openerp/netsvc.py
@@ -212,9 +212,9 @@ PSEUDOCONFIG_MAPPER = {
'debug': ['openerp:DEBUG'],
'debug_sql': ['openerp.sql_db:DEBUG'],
'info': [],
- 'warn': ['openerp:WARNING'],
- 'error': ['openerp:ERROR'],
- 'critical': ['openerp:CRITICAL'],
+ 'warn': ['openerp:WARNING', 'werkzeug:WARNING'],
+ 'error': ['openerp:ERROR', 'werkzeug:ERROR'],
+ 'critical': ['openerp:CRITICAL', 'werkzeug:CRITICAL'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index 57961a1d32c..0eae4185aeb 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -561,7 +561,7 @@ class many2one(_column):
# we use uid=1 because the visibility of a many2one field value (just id and name)
# must be the access right of the parent form and not the linked object itself.
records = dict(obj.name_get(cr, SUPERUSER_ID,
- list(set([x for x in res.values() if isinstance(x, (int,long))])),
+ list(set([x for x in res.values() if x and isinstance(x, (int,long))])),
context=context))
for id in res:
if res[id] in records:
diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py
index 0a23dda61e3..4289d6660f8 100644
--- a/openerp/osv/orm.py
+++ b/openerp/osv/orm.py
@@ -3348,6 +3348,8 @@ class BaseModel(object):
return []
if fields_to_read is None:
fields_to_read = self._columns.keys()
+ else:
+ fields_to_read = list(set(fields_to_read))
# all inherited fields + all non inherited fields for which the attribute whose name is in load is True
fields_pre = [f for f in fields_to_read if
diff --git a/openerp/service/server.py b/openerp/service/server.py
index 72e130edf8b..c0dc15caab7 100644
--- a/openerp/service/server.py
+++ b/openerp/service/server.py
@@ -46,8 +46,15 @@ SLEEP_INTERVAL = 60 # 1 min
#----------------------------------------------------------
# Werkzeug WSGI servers patched
#----------------------------------------------------------
+class LoggingBaseWSGIServerMixIn(object):
+ def handle_error(self, request, client_address):
+ t, e, _ = sys.exc_info()
+ if t == socket.error and e.errno == errno.EPIPE:
+ # broken pipe, ignore error
+ return
+ _logger.exception('Exception happened during processing of request from %s', client_address)
-class BaseWSGIServerNoBind(werkzeug.serving.BaseWSGIServer):
+class BaseWSGIServerNoBind(LoggingBaseWSGIServerMixIn, werkzeug.serving.BaseWSGIServer):
""" werkzeug Base WSGI Server patched to skip socket binding. PreforkServer
use this class, sets the socket and calls the process_request() manually
"""
@@ -74,7 +81,7 @@ class RequestHandler(werkzeug.serving.WSGIRequestHandler):
# should also work with systemd socket activation. This is currently untested
# and not yet used.
-class ThreadedWSGIServerReloadable(werkzeug.serving.ThreadedWSGIServer):
+class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.ThreadedWSGIServer):
""" werkzeug Threaded WSGI Server patched to allow reusing a listen socket
given by the environement, this is used by autoreload to keep the listen
socket open when a reload happens.
diff --git a/openerp/sql_db.py b/openerp/sql_db.py
index 3e401347a18..add6cc9655a 100644
--- a/openerp/sql_db.py
+++ b/openerp/sql_db.py
@@ -30,12 +30,11 @@ the ORM does, in fact.
from contextlib import contextmanager
from functools import wraps
import logging
-import time
import uuid
+import psycopg2.extras
import psycopg2.extensions
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_REPEATABLE_READ
from psycopg2.pool import PoolError
-from psycopg2.psycopg1 import cursor as psycopg1cursor
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
@@ -76,7 +75,7 @@ sql_counter = 0
class Cursor(object):
"""Represents an open transaction to the PostgreSQL DB backend,
acting as a lightweight wrapper around psycopg2's
- ``psycopg1cursor`` objects.
+ ``cursor`` objects.
``Cursor`` is the object behind the ``cr`` variable used all
over the OpenERP code.
@@ -175,7 +174,7 @@ class Cursor(object):
self._serialized = serialized
self._cnx = pool.borrow(dsn(dbname))
- self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor)
+ self._obj = self._cnx.cursor()
if self.sql_log:
self.__caller = frame_codeinfo(currentframe(),2)
else:
@@ -188,6 +187,16 @@ class Cursor(object):
self.cache = {}
+ def __build_dict(self, row):
+ return { d.name: row[i] for i, d in enumerate(self._obj.description) }
+ def dictfetchone(self):
+ row = self._obj.fetchone()
+ return row and self.__build_dict(row)
+ def dictfetchmany(self, size):
+ return map(self.__build_dict, self._obj.fetchmany(size))
+ def dictfetchall(self):
+ return map(self.__build_dict, self._obj.fetchall())
+
def __del__(self):
if not self._closed and not self._cnx.closed:
# Oops. 'self' has not been closed explicitly.
diff --git a/openerp/tests/phantomtest.js b/openerp/tests/phantomtest.js
index 5a6a9696cc0..84f886c5dbb 100644
--- a/openerp/tests/phantomtest.js
+++ b/openerp/tests/phantomtest.js
@@ -1,14 +1,15 @@
-// Phantomjs openerp helper
+// Phantomjs odoo helper
+// jshint evil: true, loopfunc: true
-function waitFor (ready, callback, timeout, timeoutMessageCallback) {
+function waitFor (condition, callback, timeout, timeoutMessageCallback) {
timeout = timeout || 10000;
- var start = new Date;
+ var start = new Date();
(function waitLoop() {
- if(new Date - start > timeout) {
+ if(new Date() - start > timeout) {
console.log('error', timeoutMessageCallback ? timeoutMessageCallback() : "Timeout after "+timeout+" ms");
phantom.exit(1);
- } else if (ready()) {
+ } else if (condition()) {
callback();
} else {
setTimeout(waitLoop, 250);
@@ -44,7 +45,7 @@ function PhantomTest() {
}
return result.join('');
}));
- msg.push('(leaf frame on top)')
+ msg.push('(leaf frame on top)');
}
console.log('error', JSON.stringify(msg.join('\n')));
phantom.exit(1);
@@ -86,9 +87,9 @@ function PhantomTest() {
};
setTimeout(function () {
self.page.evaluate(function () {
- var message = ("Timeout\nhref: " + window.location.href
- + "\nreferrer: " + document.referrer
- + "\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
+ var message = ("Timeout\nhref: " + window.location.href +
+ "\nreferrer: " + document.referrer +
+ "\n\n" + (document.body && document.body.innerHTML)).replace(/[^a-z0-9\s~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "*");
console.log('error', message);
phantom.exit(1);
});
@@ -107,6 +108,8 @@ function PhantomTest() {
url_path = "/login?" + qp.join('&');
}
var url = self.origin + url_path;
+ code = code || "true";
+ ready = ready || "true";
self.page.open(url, function(status) {
if (status !== 'success') {
console.log('error', "failed to load " + url);
@@ -115,7 +118,7 @@ function PhantomTest() {
console.log('loaded', url, status);
// process ready
waitFor(function() {
- console.log("PhantomTest.run: wait for condition: " + ready);
+ console.log("PhantomTest.run: wait for condition:", ready);
return self.page.evaluate(function (ready) {
var r = false;
try {
diff --git a/setup.py b/setup.py
index f26abc26e40..041a8df178b 100644
--- a/setup.py
+++ b/setup.py
@@ -95,6 +95,7 @@ def py2exe_options():
"markupsafe", # dependence of jinja2 and mako
"mock",
"openerp",
+ "passlib",
"poplib",
"psutil",
"pychart",
@@ -163,6 +164,7 @@ setuptools.setup(
'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'mako',
'mock',
+ 'passlib',
'pillow', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'psutil', # windows binary code.google.com/p/psutil/downloads/list
'psycopg2 >= 2.2',
diff --git a/setup/debian/control b/setup/debian/control
index ced6043a650..0d19361ad8b 100644
--- a/setup/debian/control
+++ b/setup/debian/control
@@ -28,6 +28,7 @@ Depends:
python-mako,
python-mock,
python-openid,
+ python-passlib,
python-psutil,
python-psycopg2,
python-pybabel,
| |