From 9fc4ad99f0736b31ac58e25d7eb7a70c7d3ec7f9 Mon Sep 17 00:00:00 2001 From: Anthony Muschang Date: Wed, 11 Jun 2014 11:36:11 +0200 Subject: [PATCH 01/38] [FIX]mass mailing: performance issue on stats #469 --- addons/mass_mailing/mass_mailing.py | 91 +++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py index e7583d8e739..68edcde9390 100644 --- a/addons/mass_mailing/mass_mailing.py +++ b/addons/mass_mailing/mass_mailing.py @@ -37,15 +37,36 @@ class MassMailingCampaign(osv.Model): def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ - results = dict.fromkeys(ids, False) - for campaign in self.browse(cr, uid, ids, context=context): - results[campaign.id] = { - 'sent': len(campaign.statistics_ids), + results = dict.fromkeys(ids, { + 'sent': 0, + 'delivered': 0, + 'opened': 0, + 'replied': 0, + 'bounced': 0, + }) + cr.execute(""" + SELECT + mass_mailing_id, + COUNT(id) AS sent, + COUNT(CASE WHEN bounced is null THEN 1 ELSE null END) AS delivered, + COUNT(CASE WHEN opened is not null THEN 1 ELSE null END) AS opened, + COUNT(CASE WHEN replied is not null THEN 1 ELSE null END) AS replied , + COUNT(CASE WHEN bounced is not null THEN 1 ELSE null END) AS bounced + FROM + mail_mail_statistics + WHERE + mass_mailing_id IN %s + GROUP BY + mass_mailing_id + """, (tuple(ids), )) + for (campaign_id, sent, delivered, opened, replied, bounced) in cr.fetchall(): + results[campaign_id] = { + 'sent': sent, # delivered: shouldn't be: all mails - (failed + bounced) ? - 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and - 'opened': len([stat for stat in campaign.statistics_ids if stat.opened]), - 'replied': len([stat for stat in campaign.statistics_ids if stat.replied]), - 'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]), + 'delivered': delivered, + 'opened': opened, + 'replied': replied, + 'bounced': bounced, } return results @@ -53,14 +74,14 @@ class MassMailingCampaign(osv.Model): """ Gather data about mass mailings to display them in kanban view as nested kanban views is not possible currently. """ results = dict.fromkeys(ids, '') - for campaign in self.browse(cr, uid, ids, context=context): + for campaign_id in ids: mass_mailing_results = [] - for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]: - mass_mailing_object = {} - for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']: - mass_mailing_object[attr] = getattr(mass_mailing, attr) - mass_mailing_results.append(mass_mailing_object) - results[campaign.id] = mass_mailing_results + mass_mailing_results = self.pool['mail.mass_mailing'].search_read(cr, uid, + domain=[('mass_mailing_campaign_id', '=', campaign_id)], + fields=['name', 'sent', 'delivered', 'opened', 'replied', 'bounced'], + limit=self._kanban_mailing_nbr, + context=context) + results[campaign_id] = mass_mailing_results return results _columns = { @@ -195,15 +216,37 @@ class MassMailing(osv.Model): return res def _get_statistics(self, cr, uid, ids, name, arg, context=None): - """ Compute statistics of the mass mailing campaign """ - results = dict.fromkeys(ids, False) - for mass_mailing in self.browse(cr, uid, ids, context=context): - results[mass_mailing.id] = { - 'sent': len(mass_mailing.statistics_ids), - 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and - 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]), - 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]), - 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]), + """ Compute statistics of the mass mailing """ + results = dict.fromkeys(ids, { + 'sent': 0, + 'delivered': 0, + 'opened': 0, + 'replied': 0, + 'bounced': 0, + }) + cr.execute(""" + SELECT + mass_mailing_id, + COUNT(id) AS sent, + COUNT(CASE WHEN bounced is null THEN 1 ELSE null END) AS delivered, + COUNT(CASE WHEN opened is not null THEN 1 ELSE null END) AS opened, + COUNT(CASE WHEN replied is not null THEN 1 ELSE null END) AS replied , + COUNT(CASE WHEN bounced is not null THEN 1 ELSE null END) AS bounced + FROM + mail_mail_statistics + WHERE + mass_mailing_id IN %s + GROUP BY + mass_mailing_id + """, (tuple(ids), )) + for (campaign_id, sent, delivered, opened, replied, bounced) in cr.fetchall(): + results[campaign_id] = { + 'sent': sent, + # delivered: shouldn't be: all mails - (failed + bounced) ? + 'delivered': delivered, + 'opened': opened, + 'replied': replied, + 'bounced': bounced, } return results From d8a0e3d29a6d64822f703027e34238cdd8e0ea2e Mon Sep 17 00:00:00 2001 From: Nicolas Bessi Date: Fri, 27 Jun 2014 14:19:34 +0200 Subject: [PATCH 02/38] Fix wrong relative import of hr_payroll --- addons/l10n_in_hr_payroll/report/report_payslip_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/l10n_in_hr_payroll/report/report_payslip_details.py b/addons/l10n_in_hr_payroll/report/report_payslip_details.py index 462bb374287..37cb7c2a0b7 100644 --- a/addons/l10n_in_hr_payroll/report/report_payslip_details.py +++ b/addons/l10n_in_hr_payroll/report/report_payslip_details.py @@ -20,7 +20,7 @@ ############################################################################## from openerp.report import report_sxw -from hr_payroll import report +from openerp.addons.hr_payroll import report class payslip_details_report_in(report.report_payslip_details.payslip_details_report): @@ -32,4 +32,4 @@ class payslip_details_report_in(report.report_payslip_details.payslip_details_re report_sxw.report_sxw('report.paylip.details.in', 'hr.payslip', 'l10n_in_hr_payroll/report/report_payslip_details.rml', parser=payslip_details_report_in) -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From df40926d2a57c101a3e2d221ecfd08fbb4fea30e Mon Sep 17 00:00:00 2001 From: Jacques-Etienne Baudoux Date: Fri, 27 Jun 2014 16:47:20 +0200 Subject: [PATCH 03/38] [IMP] res_partner: more fault tolerant name_create In case of invalid format such as 'name email@server' (missing chevrons), the parsing would be failing due to a strict behaviour of getaddresses (returns nameemail@server). With the patch this format is accepted. opw 607312 --- openerp/addons/base/res/res_partner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index d43028d68ef..30d79bd7da1 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -559,7 +559,7 @@ class res_partner(osv.osv, format_address): """ Supported syntax: - 'Raoul ': will find name and email address - otherwise: default, everything is set as the name """ - emails = tools.email_split(text) + emails = tools.email_split(text.replace(' ',',')) if emails: email = emails[0] name = text[:text.index(email)].replace('"', '').replace('<', '').strip() From f5641739a69907b33a52cd0fba67403a873c386f Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Fri, 27 Jun 2014 17:26:03 +0200 Subject: [PATCH 04/38] [FIX] website_sale_delivery: Should be installable Has been set as not install-able for testing purposes, in commit ff37d0ebcaed01b628be188c4b0a29810765d18c --- addons/website_sale_delivery/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale_delivery/__openerp__.py b/addons/website_sale_delivery/__openerp__.py index 99899ea59a4..2864727e42e 100644 --- a/addons/website_sale_delivery/__openerp__.py +++ b/addons/website_sale_delivery/__openerp__.py @@ -16,5 +16,5 @@ Delivery Costs ], 'demo': [], 'qweb': [], - 'installable': False, + 'installable': True, } From e328ae8a2e6e576d54d408a91302184abd4c3cd2 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Sun, 29 Jun 2014 02:13:34 +0200 Subject: [PATCH 05/38] [FIX] website_sale_delivery: missing refactoring related to a5ca54fd5b068 --- addons/website_sale_delivery/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index fef661e5c40..93c6aa2bfaa 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -10,7 +10,7 @@ class website_sale(openerp.addons.website_sale.controllers.main.website_sale): @http.route(['/shop/payment'], type='http', auth="public", website=True) def payment(self, **post): cr, uid, context = request.cr, request.uid, request.context - order = self.get_order() + order = request.website.sale_get_order(context=context) carrier_id = post.get('carrier_id') if carrier_id: carrier_id = int(carrier_id) From 58ad25d0e3f002a630698bd7627657d27490b40e Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 10:46:30 +0200 Subject: [PATCH 06/38] [FIX] website_event - Check that template exists before to render it --- addons/website_event/controllers/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index 70e7b0fbe91..8b62335b481 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -34,6 +34,7 @@ import time from dateutil.relativedelta import relativedelta from openerp import tools import werkzeug.urls +from werkzeug.exceptions import NotFound try: import GeoIP @@ -175,6 +176,16 @@ class website_event(http.Controller): 'event': event, 'main_object': event } + + if '.' not in page: + page = 'website_event.%s' % page + + try: + request.website.get_template(page) + except ValueError, e: + # page not found + raise NotFound + return request.website.render(page, values) @http.route(['/event/'], type='http', auth="public", website=True, multilang=True) From 2c45b7162ce8d067123c54fa7d191c54fd024739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 30 Jun 2014 11:04:24 +0200 Subject: [PATCH 07/38] [IMP] [BACKPORT] Mailing lists usability improvements: headers related to mailing lists, use of email queue for notification emails (> 50 followers), archive website template imp and fix. Backport of commit 839815f of saas-5 (PR 502). --- addons/mail/mail_followers.py | 4 ++-- addons/mail/mail_group.py | 13 +++++++++++++ addons/mail/mail_mail.py | 10 ++++++++-- addons/mail/mail_thread.py | 10 ++++++++++ addons/website_mail_group/__init__.py | 1 + addons/website_mail_group/controllers/main.py | 16 ++++++++++++++-- addons/website_mail_group/models/__init__.py | 1 + addons/website_mail_group/models/mail_group.py | 18 ++++++++++++++++++ .../views/website_mail_group.xml | 2 +- 9 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 addons/website_mail_group/models/__init__.py create mode 100644 addons/website_mail_group/models/mail_group.py diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index 0985b16f5ac..1e2c6c8d9e6 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -183,7 +183,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: @@ -195,7 +195,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 65970265ce3..633b270b754 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -201,12 +201,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 @@ -254,6 +257,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'), @@ -268,7 +274,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 c9662f04567..3691d352252 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -690,6 +690,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 #------------------------------------------------------ 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/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml index 1c0ee08edd4..21f046fc70b 100644 --- a/addons/website_mail_group/views/website_mail_group.xml +++ b/addons/website_mail_group/views/website_mail_group.xml @@ -40,7 +40,7 @@
participants
- messages + messages / month
From 4d74d9ef53a6c7b8affc1143227cf5ba37a7dd8b Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 30 Jun 2014 11:58:11 +0200 Subject: [PATCH 08/38] [FIX] auth_signup: discard signup values that may overwriting existing info --- addons/auth_signup/res_users.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/addons/auth_signup/res_users.py b/addons/auth_signup/res_users.py index 5a744c21c81..576a778be45 100644 --- a/addons/auth_signup/res_users.py +++ b/addons/auth_signup/res_users.py @@ -201,6 +201,14 @@ class res_users(osv.Model): partner.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False}) partner_user = partner.user_ids and partner.user_ids[0] or False + + # avoid overwriting existing (presumably correct) values with geolocation data + if partner.country_id or partner.zip or partner.city: + values.pop('city', None) + values.pop('country_id', None) + if partner.lang: + values.pop('lang', None) + if partner_user: # user exists, modify it according to values values.pop('login', None) From 7ba41e75e2c5b024a17f2fa14b986bdb0719a6d0 Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 12:10:49 +0200 Subject: [PATCH 09/38] [FIX] Website pager - Be sure that page in get param is a digit else use page 1. Need to find which controller add ?page= instead of &page= because google find some url with '?page=1?page=2' --> page = '1?page=2'. According to google, the referrer is the old website. --- addons/website/models/website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 434712933f0..0bfd139c17f 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -250,7 +250,7 @@ class website(osv.osv): # Compute Pager page_count = int(math.ceil(float(total) / step)) - page = max(1, min(int(page), page_count)) + page = max(1, min(int(page if page.isdigit() else 1), page_count)) scope -= 1 pmin = max(page - int(math.floor(scope/2)), 1) From f4733aa8954b3d940ae214204a01913b94772c6f Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 12:27:49 +0200 Subject: [PATCH 10/38] [FIX] website - force to test isdigit on a string else a tb "'int' object has no attribute 'isdigit'" can exist if the page var was right and so already and int --- addons/website/models/website.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 0bfd139c17f..d6e47f53c43 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -250,7 +250,7 @@ class website(osv.osv): # Compute Pager page_count = int(math.ceil(float(total) / step)) - page = max(1, min(int(page if page.isdigit() else 1), page_count)) + page = max(1, min(int(page if str(page).isdigit() else 1), page_count)) scope -= 1 pmin = max(page - int(math.floor(scope/2)), 1) From e77594c3c3f15ba27c12dee691a8ec778ab9a8be Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Mon, 30 Jun 2014 18:29:20 +0200 Subject: [PATCH 11/38] [IMP] website: backport 5112421a (unslug) Update the regexp to be not case sensitive --- addons/website/models/ir_http.py | 6 ++--- addons/website/models/website.py | 14 +++++++++++- addons/website/tests/test_converter.py | 22 +++++++++++++++++++ .../controllers/main.py | 8 +++---- addons/website_customer/controllers/main.py | 8 +++---- addons/website_membership/controllers/main.py | 8 +++---- 6 files changed, 47 insertions(+), 19 deletions(-) diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 3550d2c6373..fc6de7eefc9 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -10,7 +10,7 @@ import werkzeug.routing import openerp from openerp.addons.base import ir from openerp.addons.base.ir import ir_qweb -from openerp.addons.website.models.website import slug +from openerp.addons.website.models.website import slug, _UNSLUG_RE from openerp.http import request from openerp.osv import orm @@ -194,7 +194,7 @@ class ModelConverter(ir.ir_http.ModelConverter): def __init__(self, url_map, model=False, domain='[]'): super(ModelConverter, self).__init__(url_map, model) self.domain = domain - self.regex = r'(?:[A-Za-z0-9-_]+?-)?(\d+)(?=$|/)' + self.regex = _UNSLUG_RE.pattern def to_url(self, value): return slug(value) @@ -203,7 +203,7 @@ class ModelConverter(ir.ir_http.ModelConverter): m = re.match(self.regex, value) _uid = RequestUID(value=value, match=m, converter=self) return request.registry[self.model].browse( - request.cr, _uid, int(m.group(1)), context=request.context) + request.cr, _uid, int(m.group(2)), context=request.context) def generate(self, cr, uid, query=None, args=None, context=None): obj = request.registry[self.model] diff --git a/addons/website/models/website.py b/addons/website/models/website.py index d6e47f53c43..261bbf3bc41 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -98,11 +98,23 @@ def slug(value): else: # assume name_search result tuple id, name = value - slugname = slugify(name or '') + slugname = slugify(name or '').strip().strip('-') if not slugname: return str(id) return "%s-%d" % (slugname, id) + +_UNSLUG_RE = re.compile(r'(?:(\w{1,2}|\w[a-zA-Z0-9-_]+?\w)-)?(-?\d+)(?=$|/)') + +def unslug(s): + """Extract slug and id from a string. + Always return un 2-tuple (str|None, int|None) + """ + m = _UNSLUG_RE.match(s) + if not m: + return None, None + return m.group(1), int(m.group(2)) + def urlplus(url, params): return werkzeug.Href(url)(params or None) diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py index ca72a361ac2..6a6b046ca93 100644 --- a/addons/website/tests/test_converter.py +++ b/addons/website/tests/test_converter.py @@ -9,10 +9,32 @@ from lxml.builder import E from openerp.tests import common from openerp.addons.base.ir import ir_qweb from openerp.addons.website.models.ir_qweb import html_to_text +from openerp.addons.website.models.website import unslug impl = getDOMImplementation() document = impl.createDocument(None, None, None) +class TestUnslug(unittest2.TestCase): + def test_unslug(self): + tests = { + '': (None, None), + 'foo': (None, None), + 'foo-': (None, None), + '-': (None, None), + 'foo-1': ('foo', 1), + 'foo-bar-1': ('foo-bar', 1), + 'foo--1': ('foo', -1), + '1': (None, 1), + '1-1': ('1', 1), + '--1': (None, None), + 'foo---1': (None, None), + 'foo1': (None, None), + } + + for slug, expected in tests.iteritems(): + self.assertEqual(unslug(slug), expected) + + class TestHTMLToText(unittest2.TestCase): def test_rawstring(self): self.assertEqual( diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index faae5593a9b..a880a83ad54 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import logging -import re import werkzeug _logger = logging.getLogger(__name__) @@ -13,7 +12,7 @@ except ImportError: from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request -from openerp.addons.website.models.website import slug +from openerp.addons.website.models.website import slug, unslug from openerp.tools.translate import _ @@ -146,7 +145,7 @@ class WebsiteCrmPartnerAssign(http.Controller): # Do not use semantic controller due to SUPERUSER_ID @http.route(['/partners/'], type='http', auth="public", website=True, multilang=True) def partners_detail(self, partner_id, partner_name='', **post): - mo = re.search('-([-0-9]+)$', str(partner_id)) + _, partner_id = unslug(partner_id) current_grade, current_country = None, None grade_id = post.get('grade_id') country_id = post.get('country_id') @@ -158,8 +157,7 @@ class WebsiteCrmPartnerAssign(http.Controller): country_ids = request.registry['res.country'].exists(request.cr, request.uid, int(country_id), context=request.context) if country_ids: current_country = request.registry['res.country'].browse(request.cr, request.uid, country_ids[0], context=request.context) - if mo: - partner_id = int(mo.group(1)) + if partner_id: partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) if partner.exists() and partner.website_published: values = { diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index 09db80c8908..810faf8bdf6 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import re - import openerp from openerp import SUPERUSER_ID from openerp.addons.web import http +from openerp.addons.website.models.website import unslug from openerp.tools.translate import _ from openerp.addons.web.http import request import werkzeug.urls @@ -84,9 +83,8 @@ class WebsiteCustomer(http.Controller): # Do not use semantic controller due to SUPERUSER_ID @http.route(['/customers/'], type='http', auth="public", website=True, multilang=True) def partners_detail(self, partner_id, **post): - mo = re.search('-([-0-9]+)$', str(partner_id)) - if mo: - partner_id = int(mo.group(1)) + _, partner_id = unslug(partner_id) + if partner_id: partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) if partner.exists() and partner.website_published: values = {} diff --git a/addons/website_membership/controllers/main.py b/addons/website_membership/controllers/main.py index 24e0ed2dd5b..1ab0e5e36af 100644 --- a/addons/website_membership/controllers/main.py +++ b/addons/website_membership/controllers/main.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -import re - from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request +from openerp.addons.website.models.website import unslug from openerp.tools.translate import _ import werkzeug.urls @@ -105,9 +104,8 @@ class WebsiteMembership(http.Controller): # Do not use semantic controller due to SUPERUSER_ID @http.route(['/members/'], type='http', auth="public", website=True, multilang=True) def partners_detail(self, partner_id, **post): - mo = re.search('-([-0-9]+)$', str(partner_id)) - if mo: - partner_id = int(mo.group(1)) + _, partner_id = unslug(partner_id) + if partner_id: partner = request.registry['res.partner'].browse(request.cr, SUPERUSER_ID, partner_id, context=request.context) if partner.exists() and partner.website_published: values = {} From f825b6043bd138049ee2a107e44596ceba8ef954 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 1 Jul 2014 11:29:22 +0200 Subject: [PATCH 12/38] [FIX] rml: avoid reports ending by zero (opw 608073) Commit b6a7402 (reverted at f8671cb) was almost correct, the PageReset should be added at the end of each stories but only if we have one more stories. The PageReset will force the page count to be reseted at zero which means that last page of report would have been at zero. --- openerp/report/render/rml2pdf/trml2pdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index 70965d2c0cd..9ed1a14a21b 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -999,10 +999,10 @@ class _rml_template(object): story_cnt = 0 for node_story in node_stories: if story_cnt > 0: + # Reset Page Number with new story tag + fis.append(PageReset()) fis.append(platypus.PageBreak()) fis += r.render(node_story) - # Reset Page Number with new story tag - fis.append(PageReset()) story_cnt += 1 try: if self.localcontext and self.localcontext.get('internal_header',False): From 3d3134108d51462474a9323903fe5b549aac0119 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 1 Jul 2014 12:22:28 +0200 Subject: [PATCH 13/38] [FIX] web_linkedin: update master img CDN domain, recently changed --- addons/web_linkedin/web_linkedin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web_linkedin/web_linkedin.py b/addons/web_linkedin/web_linkedin.py index 62883840f2d..5a27993bfbd 100644 --- a/addons/web_linkedin/web_linkedin.py +++ b/addons/web_linkedin/web_linkedin.py @@ -36,7 +36,7 @@ class Binary(openerp.addons.web.http.Controller): _scheme, _netloc, path, params, query, fragment = urlparse(url) # media.linkedin.com is the master domain for LinkedIn media (replicated to CDNs), # so forcing it should always work and prevents abusing this method to load arbitrary URLs - url = urlunparse(('http', 'media.linkedin.com', path, params, query, fragment)) + url = urlunparse(('http', 'media.licdn.com', path, params, query, fragment)) bfile = urllib2.urlopen(url) return base64.b64encode(bfile.read()) From 7721ddc7ad30f73f66cafe4981c8ba56a19cfdfe Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Tue, 1 Jul 2014 14:51:00 +0200 Subject: [PATCH 14/38] [FIX] survey: Nasty brackets... --- addons/survey/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 590d7ae22c8..9ccba440c86 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -360,7 +360,7 @@ class survey_survey(osv.Model): for cell in product(rows.keys(), answers.keys()): res[cell] = 0 for input_line in question.user_input_line_ids: - if input_line.answer_type == 'suggestion' and not(current_filters) or input_line.user_input_id.id in current_filters: + if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters): res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1 result_summary = {'answers': answers, 'rows': rows, 'result': res} From f31ea32229880e26d69e98aa6069b6251e408022 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 1 Jul 2014 15:23:11 +0200 Subject: [PATCH 15/38] [FIX] product: add missing dependency: report --- addons/product/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/product/__openerp__.py b/addons/product/__openerp__.py index 83ecea95886..469941620e1 100644 --- a/addons/product/__openerp__.py +++ b/addons/product/__openerp__.py @@ -25,7 +25,7 @@ 'version': '1.1', 'author': 'OpenERP SA', 'category': 'Sales Management', - 'depends': ['base', 'decimal_precision', 'mail'], + 'depends': ['base', 'decimal_precision', 'mail', 'report'], 'demo': [ 'product_demo.xml', 'product_image_demo.xml', From 14fae548639f14647796233ea42b4dddcd8ec85a Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Tue, 1 Jul 2014 15:37:27 +0200 Subject: [PATCH 16/38] [FIX] survey: bug while recording comments --- addons/survey/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 9ccba440c86..3a9145eed0b 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -1140,7 +1140,7 @@ class survey_user_input_line(osv.Model): comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() if comment_answer: - vals.update({'answer_type': 'text', 'value_text': comment_answer}) + vals.update({'answer_type': 'text', 'value_text': comment_answer, 'skipped': False}) self.create(cr, uid, vals, context=context) return True From 451b6b9f3a5987778227c3eafd1e0d64833e56b4 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 1 Jul 2014 15:48:53 +0200 Subject: [PATCH 17/38] [FIX] project_issue_sheet: make invoiceable field autofield for worklogs When setting a worklog on a project_issue, the field to_invoice is prefilled with on_change_account_id based on the contract settings. As the field was not present on the list view, the information was lost and every worklog was not written as invoiceable, even if enabled on the contract. opw 609082. --- addons/project_issue_sheet/project_issue_sheet_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/project_issue_sheet/project_issue_sheet_view.xml b/addons/project_issue_sheet/project_issue_sheet_view.xml index fb245a0510a..80304deaab1 100644 --- a/addons/project_issue_sheet/project_issue_sheet_view.xml +++ b/addons/project_issue_sheet/project_issue_sheet_view.xml @@ -31,6 +31,7 @@ + From 0b0c1fbdd6f3a1d07e3f16482e416d9053ec98b1 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Tue, 1 Jul 2014 15:56:50 +0200 Subject: [PATCH 18/38] [FIX] survey: catch exception while quizz mode --- addons/survey/survey.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 3a9145eed0b..1389e63e9e2 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -993,6 +993,8 @@ class survey_user_input_line(osv.Model): def __get_mark(self, cr, uid, value_suggested, context=None): try: mark = self.pool.get('survey.label').browse(cr, uid, int(value_suggested), context=context).quizz_mark + except AttributeError: + mark = 0.0 except KeyError: mark = 0.0 except ValueError: From 7a823e0e1530ecae8002b53cb9d2b85e88320a8d Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 1 Jul 2014 17:11:46 +0200 Subject: [PATCH 19/38] [FIX] stock: backport fixes from master --- addons/stock/security/ir.model.access.csv | 2 +- addons/stock/security/stock_security.xml | 2 +- addons/stock/stock.py | 259 ++++++++++++---------- 3 files changed, 143 insertions(+), 120 deletions(-) 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..acb228b6232 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -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, } @@ -3108,6 +3114,109 @@ class stock_warehouse(osv.osv): pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context) return True + def create_sequences_and_picking_types(self, cr, uid, warehouse, context=None): + seq_obj = self.pool.get('ir.sequence') + picking_type_obj = self.pool.get('stock.picking.type') + #create new sequences + in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence in'), 'prefix': warehouse.code + '/IN/', 'padding': 5}, context=context) + out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence out'), 'prefix': warehouse.code + '/OUT/', 'padding': 5}, context=context) + pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence packing'), 'prefix': warehouse.code + '/PACK/', 'padding': 5}, context=context) + pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence picking'), 'prefix': warehouse.code + '/PICK/', 'padding': 5}, context=context) + int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence internal'), 'prefix': warehouse.code + '/INT/', 'padding': 5}, context=context) + + wh_stock_loc = warehouse.lot_stock_id + wh_input_stock_loc = warehouse.wh_input_stock_loc_id + wh_output_stock_loc = warehouse.wh_output_stock_loc_id + wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id + + #fetch customer and supplier locations, for references + customer_loc, supplier_loc = self._get_partner_locations(cr, uid, warehouse.id, context=context) + + #create in, out, internal picking types for warehouse + input_loc = wh_input_stock_loc + if warehouse.reception_steps == 'one_step': + input_loc = wh_stock_loc + output_loc = wh_output_stock_loc + if warehouse.delivery_steps == 'ship_only': + output_loc = wh_stock_loc + + #choose the next available color for the picking types of this warehouse + color = 0 + available_colors = [c%9 for c in range(3, 12)] # put flashy colors first + all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color') + #don't use sets to preserve the list order + for x in all_used_colors: + if x['color'] in available_colors: + available_colors.remove(x['color']) + if available_colors: + color = available_colors[0] + + #order the picking types with a sequence allowing to have the following suit for each warehouse: reception, internal, pick, pack, ship. + max_sequence = self.pool.get('stock.picking.type').search_read(cr, uid, [], ['sequence'], order='sequence desc') + max_sequence = max_sequence and max_sequence[0]['sequence'] or 0 + + in_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Receptions'), + 'warehouse_id': warehouse.id, + 'code': 'incoming', + 'sequence_id': in_seq_id, + 'default_location_src_id': supplier_loc.id, + 'default_location_dest_id': input_loc.id, + 'sequence': max_sequence + 1, + 'color': color}, context=context) + out_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Delivery Orders'), + 'warehouse_id': warehouse.id, + 'code': 'outgoing', + 'sequence_id': out_seq_id, + 'return_picking_type_id': in_type_id, + 'default_location_src_id': output_loc.id, + 'default_location_dest_id': customer_loc.id, + 'sequence': max_sequence + 4, + 'color': color}, context=context) + picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context) + int_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Internal Transfers'), + 'warehouse_id': warehouse.id, + 'code': 'internal', + 'sequence_id': int_seq_id, + 'default_location_src_id': wh_stock_loc.id, + 'default_location_dest_id': wh_stock_loc.id, + 'active': True, + 'sequence': max_sequence + 2, + 'color': color}, context=context) + pack_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Pack'), + 'warehouse_id': warehouse.id, + 'code': 'internal', + 'sequence_id': pack_seq_id, + 'default_location_src_id': wh_pack_stock_loc.id, + 'default_location_dest_id': output_loc.id, + 'active': warehouse.delivery_steps == 'pick_pack_ship', + 'sequence': max_sequence + 3, + 'color': color}, context=context) + pick_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Pick'), + 'warehouse_id': warehouse.id, + 'code': 'internal', + 'sequence_id': pick_seq_id, + 'default_location_src_id': wh_stock_loc.id, + 'default_location_dest_id': wh_pack_stock_loc.id, + 'active': warehouse.delivery_steps != 'ship_only', + 'sequence': max_sequence + 2, + 'color': color}, context=context) + + #write picking types on WH + vals = { + 'in_type_id': in_type_id, + 'out_type_id': out_type_id, + 'pack_type_id': pack_type_id, + 'pick_type_id': pick_type_id, + 'int_type_id': int_type_id, + } + super(stock_warehouse, self).write(cr, uid, warehouse.id, vals=vals, context=context) + + def create(self, cr, uid, vals, context=None): if context is None: context = {} @@ -3147,108 +3256,10 @@ class stock_warehouse(osv.osv): }, context=context_with_inactive) vals[values['field']] = location_id - #create new sequences - in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence in'), 'prefix': vals.get('code', '') + '/IN/', 'padding': 5}, context=context) - out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence out'), 'prefix': vals.get('code', '') + '/OUT/', 'padding': 5}, context=context) - pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence packing'), 'prefix': vals.get('code', '') + '/PACK/', 'padding': 5}, context=context) - pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence picking'), 'prefix': vals.get('code', '') + '/PICK/', 'padding': 5}, context=context) - int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence internal'), 'prefix': vals.get('code', '') + '/INT/', 'padding': 5}, context=context) - #create WH new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context) - warehouse = self.browse(cr, uid, new_id, context=context) - wh_stock_loc = warehouse.lot_stock_id - wh_input_stock_loc = warehouse.wh_input_stock_loc_id - wh_output_stock_loc = warehouse.wh_output_stock_loc_id - wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id - - #fetch customer and supplier locations, for references - customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context) - - #create in, out, internal picking types for warehouse - input_loc = wh_input_stock_loc - if warehouse.reception_steps == 'one_step': - input_loc = wh_stock_loc - output_loc = wh_output_stock_loc - if warehouse.delivery_steps == 'ship_only': - output_loc = wh_stock_loc - - #choose the next available color for the picking types of this warehouse - color = 0 - available_colors = [c%9 for c in range(3, 12)] # put flashy colors first - all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color') - #don't use sets to preserve the list order - for x in all_used_colors: - if x['color'] in available_colors: - available_colors.remove(x['color']) - if available_colors: - color = available_colors[0] - - #order the picking types with a sequence allowing to have the following suit for each warehouse: reception, internal, pick, pack, ship. - max_sequence = self.pool.get('stock.picking.type').search_read(cr, uid, [], ['sequence'], order='sequence desc') - max_sequence = max_sequence and max_sequence[0]['sequence'] or 0 - - in_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Receptions'), - 'warehouse_id': new_id, - 'code': 'incoming', - 'sequence_id': in_seq_id, - 'default_location_src_id': supplier_loc.id, - 'default_location_dest_id': input_loc.id, - 'sequence': max_sequence + 1, - 'color': color}, context=context) - out_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Delivery Orders'), - 'warehouse_id': new_id, - 'code': 'outgoing', - 'sequence_id': out_seq_id, - 'return_picking_type_id': in_type_id, - 'default_location_src_id': output_loc.id, - 'default_location_dest_id': customer_loc.id, - 'sequence': max_sequence + 4, - 'color': color}, context=context) - picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context) - int_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Internal Transfers'), - 'warehouse_id': new_id, - 'code': 'internal', - 'sequence_id': int_seq_id, - 'default_location_src_id': wh_stock_loc.id, - 'default_location_dest_id': wh_stock_loc.id, - 'active': True, - 'sequence': max_sequence + 2, - 'color': color}, context=context) - pack_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Pack'), - 'warehouse_id': new_id, - 'code': 'internal', - 'sequence_id': pack_seq_id, - 'default_location_src_id': wh_pack_stock_loc.id, - 'default_location_dest_id': output_loc.id, - 'active': delivery_steps == 'pick_pack_ship', - 'sequence': max_sequence + 3, - 'color': color}, context=context) - pick_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Pick'), - 'warehouse_id': new_id, - 'code': 'internal', - 'sequence_id': pick_seq_id, - 'default_location_src_id': wh_stock_loc.id, - 'default_location_dest_id': wh_pack_stock_loc.id, - 'active': delivery_steps != 'ship_only', - 'sequence': max_sequence + 2, - 'color': color}, context=context) - - #write picking types on WH - vals = { - 'in_type_id': in_type_id, - 'out_type_id': out_type_id, - 'pack_type_id': pack_type_id, - 'pick_type_id': pick_type_id, - 'int_type_id': int_type_id, - } - super(stock_warehouse, self).write(cr, uid, new_id, vals=vals, context=context) + self.create_sequences_and_picking_types(cr, uid, warehouse, context=context) warehouse.refresh() #create routes and push/pull rules @@ -3292,7 +3303,8 @@ class stock_warehouse(osv.osv): for push in route.push_ids: push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context) #change the mto pull rule name - pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context) + if warehouse.mto_pull_id.id: + pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context) def _check_delivery_resupply(self, cr, uid, warehouse, new_location, change_to_multiple, context=None): """ Will check if the resupply routes from this warehouse follow the changes of number of delivery steps """ @@ -3374,11 +3386,12 @@ class stock_warehouse(osv.osv): if vals.get('name'): name = vals.get('name', warehouse.name) self._handle_renaming(cr, uid, warehouse, name, vals.get('code', warehouse.code), context=context_with_inactive) - seq_obj.write(cr, uid, warehouse.in_type_id.sequence_id.id, {'name': name + _(' Sequence in'), 'prefix': vals.get('code', warehouse.code) + '\IN\\'}, context=context) - seq_obj.write(cr, uid, warehouse.out_type_id.sequence_id.id, {'name': name + _(' Sequence out'), 'prefix': vals.get('code', warehouse.code) + '\OUT\\'}, context=context) - seq_obj.write(cr, uid, warehouse.pack_type_id.sequence_id.id, {'name': name + _(' Sequence packing'), 'prefix': vals.get('code', warehouse.code) + '\PACK\\'}, context=context) - seq_obj.write(cr, uid, warehouse.pick_type_id.sequence_id.id, {'name': name + _(' Sequence picking'), 'prefix': vals.get('code', warehouse.code) + '\PICK\\'}, context=context) - seq_obj.write(cr, uid, warehouse.int_type_id.sequence_id.id, {'name': name + _(' Sequence internal'), 'prefix': vals.get('code', warehouse.code) + '\INT\\'}, context=context) + if warehouse.in_type_id: + seq_obj.write(cr, uid, warehouse.in_type_id.sequence_id.id, {'name': name + _(' Sequence in'), 'prefix': vals.get('code', warehouse.code) + '\IN\\'}, context=context) + seq_obj.write(cr, uid, warehouse.out_type_id.sequence_id.id, {'name': name + _(' Sequence out'), 'prefix': vals.get('code', warehouse.code) + '\OUT\\'}, context=context) + seq_obj.write(cr, uid, warehouse.pack_type_id.sequence_id.id, {'name': name + _(' Sequence packing'), 'prefix': vals.get('code', warehouse.code) + '\PACK\\'}, context=context) + seq_obj.write(cr, uid, warehouse.pick_type_id.sequence_id.id, {'name': name + _(' Sequence picking'), 'prefix': vals.get('code', warehouse.code) + '\PICK\\'}, context=context) + seq_obj.write(cr, uid, warehouse.int_type_id.sequence_id.id, {'name': name + _(' Sequence internal'), 'prefix': vals.get('code', warehouse.code) + '\INT\\'}, context=context) if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): for cmd in vals.get('resupply_wh_ids'): if cmd[0] == 6: @@ -3818,8 +3831,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 +3841,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 +3888,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 @@ -4072,11 +4095,11 @@ class stock_picking_type(osv.osv): tristates = [] for picking in picking_obj.browse(cr, uid, picking_ids, context=context): if picking.date_done > picking.date: - tristates.insert(0, {'tooltip': picking.name + _(': Late'), 'value': -1}) + tristates.insert(0, {'tooltip': picking.name or '' + _(': Late'), 'value': -1}) elif picking.backorder_id: - tristates.insert(0, {'tooltip': picking.name + _(': Backorder exists'), 'value': 0}) + tristates.insert(0, {'tooltip': picking.name or '' + _(': Backorder exists'), 'value': 0}) else: - tristates.insert(0, {'tooltip': picking.name + _(': OK'), 'value': 1}) + tristates.insert(0, {'tooltip': picking.name or '' + _(': OK'), 'value': 1}) res[picking_type_id] = tristates return res From 581341ce3f91f997a7543fa9385a1764c0c53134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 2 Jul 2014 10:25:53 +0200 Subject: [PATCH 20/38] [FIX] mail: fixed display issue with the like button --- addons/mail/static/src/css/mail.css | 8 +------- addons/mail/static/src/xml/mail.xml | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/addons/mail/static/src/css/mail.css b/addons/mail/static/src/css/mail.css index 50e190c846d..e55948634c5 100644 --- a/addons/mail/static/src/css/mail.css +++ b/addons/mail/static/src/css/mail.css @@ -164,15 +164,9 @@ border-radius: 3px; margin: 0px; padding-left: 3px; - padding-right: 15px; + padding-right: 5px; margin-right: 5px; } -.openerp .oe_mail .oe_mail_vote_count .oe_e{ - position: absolute; - bottom: 1px; - right: 2px; - font-size: 26px; -} /* c) Message action icons */ diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index 2a4ac8d1bf9..5d3175b40d7 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -355,7 +355,7 @@ - 8 + like From 376cdf36b43381c06066cb0b6e5d0ef4b0c45c3b Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 2 Jul 2014 13:56:11 +0200 Subject: [PATCH 21/38] [FIX] auth_oauth: prevent crash on login screen Empty URLs for OAuth providers do not crash anymore the login screen --- addons/auth_oauth/controllers/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 214fdefe1fb..fe3c8bef905 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -48,7 +48,8 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home): def list_providers(self): try: provider_obj = request.registry.get('auth.oauth.provider') - providers = provider_obj.search_read(request.cr, SUPERUSER_ID, [('enabled', '=', True)]) + providers = provider_obj.search_read(request.cr, SUPERUSER_ID, [('enabled', '=', True), ('auth_endpoint', '!=', False), ('validation_endpoint', '!=', False)]) + # TODO in forwardport: remove conditions on 'auth_endpoint' and 'validation_endpoint' when these fields will be 'required' in model except Exception: providers = [] for provider in providers: From 1c5058c931a6ef55f013ec0c0ca2578ddf75f4a4 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 2 Jul 2014 14:07:52 +0200 Subject: [PATCH 22/38] [FIX] auth_oauth: hide invalid providers from login screen --- addons/auth_oauth/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 62eb2e812cc..8fb7ce92607 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -46,7 +46,7 @@ class OAuthController(oeweb.Controller): registry = RegistryManager.get(dbname) with registry.cursor() as cr: providers = registry.get('auth.oauth.provider') - l = providers.read(cr, SUPERUSER_ID, providers.search(cr, SUPERUSER_ID, [('enabled', '=', True)])) + l = providers.read(cr, SUPERUSER_ID, providers.search(cr, SUPERUSER_ID, [('enabled', '=', True), ('auth_endpoint', '!=', False), ('validation_endpoint', '!=', False)])) except Exception: l = [] return l From e80014eae3a622c1491d3f2905b3b6b9e12c6be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lionel=20Sausin=20=28Num=C3=A9rigraphe=29?= Date: Sat, 31 May 2014 19:00:34 +0200 Subject: [PATCH 23/38] [FIX] Fixes #273: avoid double-warning when changing the quantity of a stock move --- addons/stock/stock.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 981b6f749c0..3b1e70646f1 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1901,7 +1901,6 @@ class stock_move(osv.osv): result = { 'product_qty': 0.00 } - warning = {} if (not product_id) or (product_uos_qty <=0.0): result['product_uos_qty'] = 0.0 @@ -1909,22 +1908,15 @@ class stock_move(osv.osv): product_obj = self.pool.get('product.product') uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff']) - - # Warn if the quantity was decreased - for move in self.read(cr, uid, ids, ['product_uos_qty']): - if product_uos_qty < move['product_uos_qty']: - warning.update({ - 'title': _('Warning: No Back Order'), - 'message': _("By changing the quantity here, you accept the " - "new quantity as complete: OpenERP will not " - "automatically generate a Back Order.") }) - break + + # No warning if the quantity was decreased to avoid double warnings: + # The clients should call onchange_quantity too anyway if product_uos and product_uom and (product_uom != product_uos): result['product_qty'] = product_uos_qty / uos_coeff['uos_coeff'] else: result['product_qty'] = product_uos_qty - return {'value': result, 'warning': warning} + return {'value': result} def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, partner_id=False): From a6bf8cd07bc0712d7466b51f4f346ac91a61d7ea Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 2 Jul 2014 16:25:24 +0200 Subject: [PATCH 24/38] [FIX] website: avoid divisions by zero when no record to display --- addons/website/views/website_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index de4eb4fb94b..5932931688a 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -411,7 +411,7 @@ - +
From fbbc3a54e91e7768ecf33038154a2bdbd87505ba Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Wed, 2 Jul 2014 17:29:13 +0200 Subject: [PATCH 25/38] [FIX] RML reporting: understandable crash message When tag is misused in an RML report, logs a more explicit error --- openerp/report/render/rml2pdf/trml2pdf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index 9ed1a14a21b..ec5e9484805 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -373,7 +373,11 @@ class _rml_canvas(object): v = utils.attr_get(node, ['x','y']) text=self._textual(node, **v) text = utils.xml2str(text) - self.canvas.drawString(text=text, **v) + try: + self.canvas.drawString(text=text, **v) + except TypeError as e: + _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") + raise e def _drawCenteredString(self, node): v = utils.attr_get(node, ['x','y']) From 04eff4fe3d157774a1529c743f24b00cb2e28d93 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Thu, 3 Jul 2014 09:36:46 +0200 Subject: [PATCH 26/38] [TYPO] Stupid typo... --- openerp/report/render/rml2pdf/trml2pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index ec5e9484805..75bc30d4a19 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -376,7 +376,7 @@ class _rml_canvas(object): try: self.canvas.drawString(text=text, **v) except TypeError as e: - _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") + _logger.error("Bad RML: tag requires attributes 'x' and 'y'!") raise e def _drawCenteredString(self, node): From bf353998f20c27eed3494a172c7afa174c20865a Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 3 Jul 2014 12:30:48 +0200 Subject: [PATCH 27/38] [FIX] res_partner: backport of rev 37bf72a Correctly take the 'use parent address' into account in the onchange. Slightly updated the view that was weird with this parameter. opw 609344 --- openerp/addons/base/res/res_partner.py | 8 +++++--- openerp/addons/base/res/res_partner_view.xml | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 30d79bd7da1..4d86c1cbf3f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -344,6 +344,7 @@ class res_partner(osv.osv, format_address): value = {} value['title'] = False if is_company: + value['use_parent_address'] = False domain = {'title': [('domain', '=', 'partner')]} else: domain = {'title': [('domain', '=', 'contact')]} @@ -363,9 +364,10 @@ class res_partner(osv.osv, format_address): 'was never correctly set. If an existing contact starts working for a new ' 'company then a new contact should be created under that new ' 'company. You can use the "Discard" button to abandon this change.')} - parent = self.browse(cr, uid, parent_id, context=context) - address_fields = self._address_fields(cr, uid, context=context) - result['value'] = dict((key, value_or_id(parent[key])) for key in address_fields) + if use_parent_address: + parent = self.browse(cr, uid, parent_id, context=context) + address_fields = self._address_fields(cr, uid, context=context) + result['value'] = dict((key, value_or_id(parent[key])) for key in address_fields) else: result['value'] = {'use_parent_address': False} return result diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index fd394549f4b..724cce7bb5f 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -157,8 +157,8 @@
-