Merge lp:~openerp-dev/openobject-addons/7.0-opw-590221 into lp:openobject-addons/7.0

Proposed by Jigar A.
Status: Merged
Merged at revision: 8989
Proposed branch: lp:~openerp-dev/openobject-addons/7.0-opw-590221
Merge into: lp:openobject-addons/7.0
Diff against target: 195 lines (+71/-13)
6 files modified
project_mrp/test/project_task_procurement.yml (+1/-1)
purchase/company.py (+6/-2)
purchase/purchase.py (+27/-3)
sale_stock/company.py (+6/-3)
sale_stock/sale_stock.py (+27/-2)
sale_stock/test/picking_order_policy.yml (+4/-2)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/7.0-opw-590221
Reviewer Review Type Date Requested Status
Naresh(OpenERP) (community) Needs Fixing
Olivier Dony (Odoo) Pending
Review via email: mp+155543@code.launchpad.net

Description of the change

timezone issue: when converting the "order date" of Sales Orders or Purchase Orders into the "reference date" for scheduling delivery and procurement, the "order date" is taken as if it was in UTC like all datetime values. In reality it is a fields.date and therefore expressed in the user's timezone.

Test Case
Modules Installed Sale, Purchase Requisition, MRP and PST time zone of User.
Create SO with Order date = 2012-03-26 and When SO is confirmed, it calculates the Expected DO Date like [Sale Order Date (on the SO) + Customer Lead Time (on the SO line, coming from the product) - Security Days (on the company)] so expected date shown to according to PST tz will be 2012-03-25 17:00:00 instead of 2012-03-26 00:00:00. Same implies to PO. as mention on Bug report.

And
Also Improved the confusing and mis-leading Tool-tips for the Company Level Lead time fields Purchase Lead Time and Security Days.

Kindly Review this.
Thank You

To post a comment you must log in.
Revision history for this message
Naresh(OpenERP) (nch-openerp) wrote :

Hi Jigar,

Seems a cool fix ! Such a trap was not cached before where conversion being on a field type of different nature.

However few improvements I feel we should do !

It will be nice if we make this method in general so that any one can use it easily. Say we can define this in
server/openerp/tools/misc.py and then can be accessed where ever its needed. This will reduce code duplicity too.

*Usability* : we can improve a bit the help text added or can have community opinion on this !

*Supplier Order*:

This is the leads/security time for each purchase order. For company security purpose this many days will be removed from the date what suppliers has promised to you.

*Customer Order*:

For company security purpose this many days will be removed from the date, what you have promised to customers,
to cope up with any problems of procurement, final shipping, order negotiation etc.

Thanks,
Naresh Soni
OpenERP Enterprise Services

review: Needs Fixing
Revision history for this message
Jigar A. (ifixthat) wrote :

Hello,
Have Committed the **Usability** Improvements as suggested.
I do agree with that this should be generic Implantation, but by keeping specific handling on addons side then, It is possible for people to update a single module without updating the whole code if we think of stable perspective.
Yes we should forward port this to trunk with adding this as Utility either Under Tools as you suggested, or Under Date field definitions which will target it as Date related function.
Kindly review the issue.
Thank You

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'project_mrp/test/project_task_procurement.yml'
--- project_mrp/test/project_task_procurement.yml 2012-12-18 16:37:16 +0000
+++ project_mrp/test/project_task_procurement.yml 2013-04-04 12:34:35 +0000
@@ -27,7 +27,7 @@
27 assert (not project and not account) or project.analytic_account_id == account, "Project does not correspond."27 assert (not project and not account) or project.analytic_account_id == account, "Project does not correspond."
28 planned_hours = self._convert_qty_company_hours(cr, uid, procurement, context=context)28 planned_hours = self._convert_qty_company_hours(cr, uid, procurement, context=context)
29 assert task.planned_hours == planned_hours, 'Planned Hours do not correspond.'29 assert task.planned_hours == planned_hours, 'Planned Hours do not correspond.'
30 assert datetime.strptime(task.date_deadline, '%Y-%m-%d') == datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S'), 'Deadline does not correspond.'30 assert datetime.strptime(task.date_deadline, '%Y-%m-%d') == datetime.strptime(procurement.date_planned[:10], '%Y-%m-%d'), 'Deadline does not correspond.'
31 if procurement.product_id.product_manager:31 if procurement.product_id.product_manager:
32 assert task.user_id.id == procurement.product_id.product_manager.id, 'Allocated Person does not correspond with Service Product Manager.'32 assert task.user_id.id == procurement.product_id.product_manager.id, 'Allocated Person does not correspond with Service Product Manager.'
33-33-
3434
=== modified file 'purchase/company.py'
--- purchase/company.py 2012-12-06 14:56:32 +0000
+++ purchase/company.py 2013-04-04 12:34:35 +0000
@@ -24,8 +24,12 @@
24class company(osv.osv):24class company(osv.osv):
25 _inherit = 'res.company'25 _inherit = 'res.company'
26 _columns = {26 _columns = {
27 'po_lead': fields.float('Purchase Lead Time', required=True,27 'po_lead': fields.float(
28 help="This is the leads/security time for each purchase order."),28 'Purchase Lead Time', required=True,
29 help="Margin of error for supplier lead times. When the system"\
30 "generates Purchase Orders for procuring products,"\
31 "they will be scheduled that many days earlier "\
32 "to cope with unexpected supplier delays."),
29 }33 }
30 _defaults = {34 _defaults = {
31 'po_lead': lambda *a: 1.0,35 'po_lead': lambda *a: 1.0,
3236
=== modified file 'purchase/purchase.py'
--- purchase/purchase.py 2012-12-21 16:48:08 +0000
+++ purchase/purchase.py 2013-04-04 12:34:35 +0000
@@ -20,6 +20,8 @@
20##############################################################################20##############################################################################
2121
22import time22import time
23import pytz
24from openerp import SUPERUSER_ID
23from datetime import datetime25from datetime import datetime
24from dateutil.relativedelta import relativedelta26from dateutil.relativedelta import relativedelta
2527
@@ -593,11 +595,33 @@
593 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)595 wf_service.trg_validate(uid, 'purchase.order', id, 'purchase_cancel', cr)
594 return True596 return True
595597
598 def date_to_datetime(self, cr, uid, userdate, context=None):
599 """ Convert date values expressed in user's timezone to
600 server-side UTC timestamp, assuming a default arbitrary
601 time of 12:00 AM - because a time is needed.
602
603 :param str userdate: date string in in user time zone
604 :return: UTC datetime string for server-side use
605 """
606 # TODO: move to fields.datetime in server after 7.0
607 user_datetime = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(hours=12.0)
608 if context and context.get('tz'):
609 tz_name = context['tz']
610 else:
611 tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
612 if tz_name:
613 utc = pytz.timezone('UTC')
614 context_tz = pytz.timezone(tz_name)
615 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
616 user_datetime = local_timestamp.astimezone(utc)
617 return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
618 return userdate
619
596 def _prepare_order_picking(self, cr, uid, order, context=None):620 def _prepare_order_picking(self, cr, uid, order, context=None):
597 return {621 return {
598 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),622 'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
599 'origin': order.name + ((order.origin and (':' + order.origin)) or ''),623 'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
600 'date': order.date_order,624 'date': self.date_to_datetime(cr, uid, order.date_order, context),
601 'partner_id': order.dest_address_id.id or order.partner_id.id,625 'partner_id': order.dest_address_id.id or order.partner_id.id,
602 'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',626 'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
603 'type': 'in',627 'type': 'in',
@@ -615,8 +639,8 @@
615 'product_uos_qty': order_line.product_qty,639 'product_uos_qty': order_line.product_qty,
616 'product_uom': order_line.product_uom.id,640 'product_uom': order_line.product_uom.id,
617 'product_uos': order_line.product_uom.id,641 'product_uos': order_line.product_uom.id,
618 'date': order_line.date_planned,642 'date': self.date_to_datetime(cr, uid, order.date_order, context),
619 'date_expected': order_line.date_planned,643 'date_expected': self.date_to_datetime(cr, uid, order.date_order, context),
620 'location_id': order.partner_id.property_stock_supplier.id,644 'location_id': order.partner_id.property_stock_supplier.id,
621 'location_dest_id': order.location_id.id,645 'location_dest_id': order.location_id.id,
622 'picking_id': picking_id,646 'picking_id': picking_id,
623647
=== modified file 'sale_stock/company.py'
--- sale_stock/company.py 2012-12-06 14:56:32 +0000
+++ sale_stock/company.py 2013-04-04 12:34:35 +0000
@@ -24,9 +24,12 @@
24class company(osv.osv):24class company(osv.osv):
25 _inherit = 'res.company'25 _inherit = 'res.company'
26 _columns = {26 _columns = {
27 'security_lead': fields.float('Security Days', required=True,27 'security_lead': fields.float(
28 help="This is the days added to what you promise to customers "\28 'Security Days', required=True,
29 "for security purpose"),29 help="Margin of error for dates promised to customers. "\
30 "Products will be scheduled for procurement and delivery "\
31 "that many days earlier than the actual promised date, to "\
32 "cope with unexpected delays in the supply chain."),
30 }33 }
31 _defaults = {34 _defaults = {
32 'security_lead': 0.0,35 'security_lead': 0.0,
3336
=== modified file 'sale_stock/sale_stock.py'
--- sale_stock/sale_stock.py 2012-12-21 16:48:08 +0000
+++ sale_stock/sale_stock.py 2013-04-04 12:34:35 +0000
@@ -25,6 +25,8 @@
25from openerp.osv import fields, osv25from openerp.osv import fields, osv
26from openerp import netsvc26from openerp import netsvc
27from openerp.tools.translate import _27from openerp.tools.translate import _
28import pytz
29from openerp import SUPERUSER_ID
2830
29class sale_shop(osv.osv):31class sale_shop(osv.osv):
30 _inherit = "sale.shop"32 _inherit = "sale.shop"
@@ -232,6 +234,28 @@
232 res.append(line.procurement_id.id)234 res.append(line.procurement_id.id)
233 return res235 return res
234236
237 def date_to_datetime(self, cr, uid, userdate, context=None):
238 """ Convert date values expressed in user's timezone to
239 server-side UTC timestamp, assuming a default arbitrary
240 time of 12:00 AM - because a time is needed.
241
242 :param str userdate: date string in in user time zone
243 :return: UTC datetime string for server-side use
244 """
245 # TODO: move to fields.datetime in server after 7.0
246 user_datetime = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(hours=12.0)
247 if context and context.get('tz'):
248 tz_name = context['tz']
249 else:
250 tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
251 if tz_name:
252 utc = pytz.timezone('UTC')
253 context_tz = pytz.timezone(tz_name)
254 local_timestamp = context_tz.localize(user_datetime, is_dst=False)
255 user_datetime = local_timestamp.astimezone(utc)
256 return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
257 return userdate
258
235 # if mode == 'finished':259 # if mode == 'finished':
236 # returns True if all lines are done, False otherwise260 # returns True if all lines are done, False otherwise
237 # if mode == 'canceled':261 # if mode == 'canceled':
@@ -314,7 +338,7 @@
314 return {338 return {
315 'name': pick_name,339 'name': pick_name,
316 'origin': order.name,340 'origin': order.name,
317 'date': order.date_order,341 'date': self.date_to_datetime(cr, uid, order.date_order, context),
318 'type': 'out',342 'type': 'out',
319 'state': 'auto',343 'state': 'auto',
320 'move_type': order.picking_policy,344 'move_type': order.picking_policy,
@@ -348,7 +372,8 @@
348 return True372 return True
349373
350 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):374 def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
351 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)375 start_date = self.date_to_datetime(cr, uid, start_date, context)
376 date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=line.delay or 0.0)
352 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)377 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
353 return date_planned378 return date_planned
354379
355380
=== modified file 'sale_stock/test/picking_order_policy.yml'
--- sale_stock/test/picking_order_policy.yml 2012-12-18 22:50:15 +0000
+++ sale_stock/test/picking_order_policy.yml 2013-04-04 12:34:35 +0000
@@ -26,7 +26,8 @@
26 order = self.browse(cr, uid, ref("sale.sale_order_6"))26 order = self.browse(cr, uid, ref("sale.sale_order_6"))
27 for order_line in order.order_line:27 for order_line in order.order_line:
28 procurement = order_line.procurement_id28 procurement = order_line.procurement_id
29 date_planned = datetime.strptime(order.date_order, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=order_line.delay or 0.0)29 sale_order_date = self.date_to_datetime(cr, uid, order.date_order, context)
30 date_planned = datetime.strptime(sale_order_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=order_line.delay or 0.0)
30 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)31 date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
31 assert procurement.date_planned == date_planned, "Scheduled date is not correspond."32 assert procurement.date_planned == date_planned, "Scheduled date is not correspond."
32 assert procurement.product_id.id == order_line.product_id.id, "Product is not correspond."33 assert procurement.product_id.id == order_line.product_id.id, "Product is not correspond."
@@ -60,7 +61,8 @@
60 output_id = sale_order.shop_id.warehouse_id.lot_output_id.id61 output_id = sale_order.shop_id.warehouse_id.lot_output_id.id
61 for move in picking.move_lines:62 for move in picking.move_lines:
62 order_line = move.sale_line_id63 order_line = move.sale_line_id
63 date_planned = datetime.strptime(sale_order.date_order, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=order_line.delay or 0.0)64 sale_order_date = self.date_to_datetime(cr, uid, sale_order.date_order, context)
65 date_planned = datetime.strptime(sale_order_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=order_line.delay or 0.0)
64 date_planned = (date_planned - timedelta(days=sale_order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)66 date_planned = (date_planned - timedelta(days=sale_order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
65 assert datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) == datetime.strptime(date_planned, DEFAULT_SERVER_DATETIME_FORMAT), "Excepted Date is not correspond with Planned Date."67 assert datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) == datetime.strptime(date_planned, DEFAULT_SERVER_DATETIME_FORMAT), "Excepted Date is not correspond with Planned Date."
66 assert move.product_id.id == order_line.product_id.id,"Product is not correspond."68 assert move.product_id.id == order_line.product_id.id,"Product is not correspond."