[MERGE] forward port of branch 7.0 up to de07c64
[odoo/odoo.git] / addons / lunch / lunch.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from xml.sax.saxutils import escape
23 import time
24 from openerp.osv import fields, osv
25 from datetime import datetime
26 from lxml import etree
27 from openerp import tools
28 from openerp.tools.translate import _
29
30 class lunch_order(osv.Model):
31     """ 
32     lunch order (contains one or more lunch order line(s))
33     """
34     _name = 'lunch.order'
35     _description = 'Lunch Order'
36     _order = 'date desc'
37
38     def name_get(self, cr, uid, ids, context=None):
39         if not ids:
40             return []
41         res = []
42         for elmt in self.browse(cr, uid, ids, context=context):
43             name = _("Lunch Order")
44             name = name + ' ' + str(elmt.id)
45             res.append((elmt.id, name))
46         return res
47         
48     def _price_get(self, cr, uid, ids, name, arg, context=None):
49         """ 
50         get and sum the order lines' price
51         """
52         result = dict.fromkeys(ids, 0)
53         for order in self.browse(cr, uid, ids, context=context):
54             result[order.id] = sum(order_line.product_id.price
55                                    for order_line in order.order_line_ids)
56         return result
57
58     def _fetch_orders_from_lines(self, cr, uid, ids, name, context=None):
59         """ 
60         return the list of lunch orders to which belong the order lines `idsĀ“
61         """
62         result = set()
63         for order_line in self.browse(cr, uid, ids, context=context):
64             if order_line.order_id:
65                 result.add(order_line.order_id.id)
66         return list(result)
67
68     def add_preference(self, cr, uid, ids, pref_id, context=None):
69         """ 
70         create a new order line based on the preference selected (pref_id)
71         """
72         assert len(ids) == 1
73         orderline_ref = self.pool.get('lunch.order.line')
74         prod_ref = self.pool.get('lunch.product')
75         order = self.browse(cr, uid, ids[0], context=context)
76         pref = orderline_ref.browse(cr, uid, pref_id, context=context)
77         new_order_line = {
78             'date': order.date,
79             'user_id': uid,
80             'product_id': pref.product_id.id,
81             'note': pref.note,
82             'order_id': order.id,
83             'price': pref.product_id.price,
84             'supplier': pref.product_id.supplier.id
85         }
86         return orderline_ref.create(cr, uid, new_order_line, context=context)
87
88     def _alerts_get(self, cr, uid, ids, name, arg, context=None):
89         """ 
90         get the alerts to display on the order form 
91         """
92         result = {}
93         alert_msg = self._default_alerts_get(cr, uid, context=context)
94         for order in self.browse(cr, uid, ids, context=context):
95             if order.state == 'new':
96                 result[order.id] = alert_msg
97         return result
98
99     def check_day(self, alert):
100         """ 
101         This method is used by can_display_alert
102         to check if the alert day corresponds
103         to the current day 
104         """
105         today = datetime.now().isoweekday()
106         assert 1 <= today <= 7, "Should be between 1 and 7"
107         mapping = dict((idx, name) for idx, name in enumerate('days monday tuesday wednesday thursday friday saturday sunday'.split()))
108         return alert[mapping[today]]
109
110     def can_display_alert(self, alert):
111         """ 
112         This method check if the alert can be displayed today
113         """
114         if alert.alter_type == 'specific':
115             #the alert is only activated on a specific day
116             return alert.specific_day == time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
117         elif alert.alter_type == 'week':
118             #the alert is activated during some days of the week
119             return self.check_day(alert)
120         return True # alter_type == 'days' (every day)
121
122     def _default_alerts_get(self, cr, uid, context=None):
123         """ 
124         get the alerts to display on the order form
125         """
126         alert_ref = self.pool.get('lunch.alert')
127         alert_ids = alert_ref.search(cr, uid, [], context=context)
128         alert_msg = []
129         for alert in alert_ref.browse(cr, uid, alert_ids, context=context):
130             #check if the address must be displayed today
131             if self.can_display_alert(alert):
132                 #display the address only during its active time
133                 mynow = fields.datetime.context_timestamp(cr, uid, datetime.now(), context=context)
134                 hour_to = int(alert.active_to)
135                 min_to = int((alert.active_to - hour_to) * 60)
136                 to_alert = datetime.strptime(str(hour_to) + ":" + str(min_to), "%H:%M")
137                 hour_from = int(alert.active_from)
138                 min_from = int((alert.active_from - hour_from) * 60)
139                 from_alert = datetime.strptime(str(hour_from) + ":" + str(min_from), "%H:%M")
140                 if mynow.time() >= from_alert.time() and mynow.time() <= to_alert.time():
141                     alert_msg.append(alert.message)
142         return '\n'.join(alert_msg)
143
144     def onchange_price(self, cr, uid, ids, order_line_ids, context=None):
145         """
146         Onchange methode that refresh the total price of order
147         """
148         res = {'value': {'total': 0.0}}
149         order_line_ids = self.resolve_o2m_commands_to_record_dicts(cr, uid, "order_line_ids", order_line_ids, ["price"], context=context)
150         if order_line_ids:
151             tot = 0.0
152             product_ref = self.pool.get("lunch.product")
153             for prod in order_line_ids:
154                 if 'product_id' in prod:
155                     tot += product_ref.browse(cr, uid, prod['product_id'], context=context).price
156                 else:
157                     tot += prod['price']
158             res = {'value': {'total': tot}}
159         return res
160
161     def __getattr__(self, attr):
162         """ 
163         this method catch unexisting method call and if it starts with
164         add_preference_'n' we execute the add_preference method with 
165         'n' as parameter 
166         """
167         if attr.startswith('add_preference_'):
168             pref_id = int(attr[15:])
169             def specific_function(cr, uid, ids, context=None):
170                 return self.add_preference(cr, uid, ids, pref_id, context=context)
171             return specific_function
172         return super(lunch_order, self).__getattr__(attr)
173
174     def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
175         """ 
176         Add preferences in the form view of order.line 
177         """
178         res = super(lunch_order,self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu)
179         line_ref = self.pool.get("lunch.order.line")
180         if view_type == 'form':
181             doc = etree.XML(res['arch'])
182             pref_ids = line_ref.search(cr, uid, [('user_id', '=', uid)], order='id desc', context=context)
183             xml_start = etree.Element("div")
184             #If there are no preference (it's the first time for the user)
185             if len(pref_ids)==0:
186                 #create Elements
187                 xml_no_pref_1 = etree.Element("div")
188                 xml_no_pref_1.set('class','oe_inline oe_lunch_intro')
189                 xml_no_pref_2 = etree.Element("h3")
190                 xml_no_pref_2.text = _("This is the first time you order a meal")
191                 xml_no_pref_3 = etree.Element("p")
192                 xml_no_pref_3.set('class','oe_grey')
193                 xml_no_pref_3.text = _("Select a product and put your order comments on the note.")
194                 xml_no_pref_4 = etree.Element("p")
195                 xml_no_pref_4.set('class','oe_grey')
196                 xml_no_pref_4.text = _("Your favorite meals will be created based on your last orders.")
197                 xml_no_pref_5 = etree.Element("p")
198                 xml_no_pref_5.set('class','oe_grey')
199                 xml_no_pref_5.text = _("Don't forget the alerts displayed in the reddish area")
200                 #structure Elements
201                 xml_start.append(xml_no_pref_1)
202                 xml_no_pref_1.append(xml_no_pref_2)
203                 xml_no_pref_1.append(xml_no_pref_3)
204                 xml_no_pref_1.append(xml_no_pref_4)
205                 xml_no_pref_1.append(xml_no_pref_5)
206             #Else: the user already have preferences so we display them
207             else:
208                 preferences = line_ref.browse(cr, uid, pref_ids, context=context)
209                 categories = {} #store the different categories of products in preference
210                 count = 0
211                 for pref in preferences:
212                     #For each preference
213                     categories.setdefault(pref.product_id.category_id.name, {})
214                     #if this product has already been added to the categories dictionnary
215                     if pref.product_id.id in categories[pref.product_id.category_id.name]:
216                         #we check if for the same product the note has already been added
217                         if pref.note not in categories[pref.product_id.category_id.name][pref.product_id.id]:
218                             #if it's not the case then we add this to preferences
219                             categories[pref.product_id.category_id.name][pref.product_id.id][pref.note] = pref
220                     #if this product is not in the dictionnay, we add it
221                     else:
222                         categories[pref.product_id.category_id.name][pref.product_id.id] = {}
223                         categories[pref.product_id.category_id.name][pref.product_id.id][pref.note] = pref
224
225                 currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id
226
227                 #For each preferences that we get, we will create the XML structure
228                 for key, value in categories.items():
229                     xml_pref_1 = etree.Element("div")
230                     xml_pref_1.set('class', 'oe_lunch_30pc')
231                     xml_pref_2 = etree.Element("h2")
232                     xml_pref_2.text = key
233                     xml_pref_1.append(xml_pref_2)
234                     i = 0
235                     value = value.values()
236                     #TODO: sorted_values is used for a quick and dirty hack in order to display the 5 last orders of each categories.
237                     #It would be better to fetch only the 5 items to display instead of fetching everything then sorting them in order to keep only the 5 last.
238                     #NB: The note could also be ignored + we could fetch the preferences on the most ordered products instead of the last ones...
239                     sorted_values = {}
240                     for val in value:
241                         for elmt in val.values():
242                             sorted_values[elmt.id] = elmt
243                     for key, pref in sorted(sorted_values.iteritems(), key=lambda (k, v): (k, v), reverse=True):
244                         #We only show 5 preferences per category (or it will be too long)
245                         if i == 5:
246                             break
247                         i += 1
248                         xml_pref_3 = etree.Element("div")
249                         xml_pref_3.set('class','oe_lunch_vignette')
250                         xml_pref_1.append(xml_pref_3)
251
252                         xml_pref_4 = etree.Element("span")
253                         xml_pref_4.set('class','oe_lunch_button')
254                         xml_pref_3.append(xml_pref_4)
255
256                         xml_pref_5 = etree.Element("button")
257                         xml_pref_5.set('name',"add_preference_"+str(pref.id))
258                         xml_pref_5.set('class','oe_link oe_i oe_button_plus')
259                         xml_pref_5.set('type','object')
260                         xml_pref_5.set('string','+')
261                         xml_pref_4.append(xml_pref_5)
262
263                         xml_pref_6 = etree.Element("button")
264                         xml_pref_6.set('name',"add_preference_"+str(pref.id))
265                         xml_pref_6.set('class','oe_link oe_button_add')
266                         xml_pref_6.set('type','object')
267                         xml_pref_6.set('string',_("Add"))
268                         xml_pref_4.append(xml_pref_6)
269
270                         xml_pref_7 = etree.Element("div")
271                         xml_pref_7.set('class','oe_group_text_button')
272                         xml_pref_3.append(xml_pref_7)
273
274                         xml_pref_8 = etree.Element("div")
275                         xml_pref_8.set('class','oe_lunch_text')
276                         xml_pref_8.text = escape(pref.product_id.name)+str(" ")
277                         xml_pref_7.append(xml_pref_8)
278
279                         price = pref.product_id.price or 0.0
280                         cur = currency.name or ''
281                         xml_pref_9 = etree.Element("span")
282                         xml_pref_9.set('class','oe_tag')
283                         xml_pref_9.text = str(price)+str(" ")+cur
284                         xml_pref_8.append(xml_pref_9)
285
286                         xml_pref_10 = etree.Element("div")
287                         xml_pref_10.set('class','oe_grey')
288                         xml_pref_10.text = escape(pref.note or '')
289                         xml_pref_3.append(xml_pref_10)
290
291                         xml_start.append(xml_pref_1)
292
293             first_node = doc.xpath("//div[@name='preferences']")
294             if first_node and len(first_node)>0:
295                 first_node[0].append(xml_start)
296             res['arch'] = etree.tostring(doc)
297         return res
298
299     _columns = {
300         'user_id': fields.many2one('res.users', 'User Name', required=True, readonly=True, states={'new':[('readonly', False)]}),
301         'date': fields.date('Date', required=True, readonly=True, states={'new':[('readonly', False)]}),
302         'order_line_ids': fields.one2many('lunch.order.line', 'order_id', 'Products', ondelete="cascade", readonly=True, states={'new':[('readonly', False)]}),
303         'total': fields.function(_price_get, string="Total", store={
304                  'lunch.order.line': (_fetch_orders_from_lines, ['product_id','order_id'], 20),
305             }),
306         'state': fields.selection([('new', 'New'), \
307                                     ('confirmed','Confirmed'), \
308                                     ('cancelled','Cancelled'), \
309                                     ('partially','Partially Confirmed')] \
310                                 ,'Status', readonly=True, select=True),
311         'alerts': fields.function(_alerts_get, string="Alerts", type='text'),
312     }
313
314     _defaults = {
315         'user_id': lambda self, cr, uid, context: uid,
316         'date': fields.date.context_today,
317         'state': 'new',
318         'alerts': _default_alerts_get,
319     }
320
321
322 class lunch_order_line(osv.Model):
323     """ 
324     lunch order line: one lunch order can have many order lines
325     """
326     _name = 'lunch.order.line'
327     _description = 'lunch order line'
328
329     def onchange_price(self, cr, uid, ids, product_id, context=None):
330         if product_id:
331             price = self.pool.get('lunch.product').browse(cr, uid, product_id, context=context).price
332             return {'value': {'price': price}}
333         return {'value': {'price': 0.0}}
334
335     def order(self, cr, uid, ids, context=None):
336         """ 
337         The order_line is ordered to the supplier but isn't received yet
338         """
339         for order_line in self.browse(cr, uid, ids, context=context):
340             order_line.write({'state': 'ordered'}, context=context)
341         return self._update_order_lines(cr, uid, ids, context=context)
342
343     def confirm(self, cr, uid, ids, context=None):
344         """ 
345         confirm one or more order line, update order status and create new cashmove 
346         """
347         cashmove_ref = self.pool.get('lunch.cashmove')
348         for order_line in self.browse(cr, uid, ids, context=context):
349             if order_line.state != 'confirmed':
350                 values = {
351                     'user_id': order_line.user_id.id,
352                     'amount': -order_line.price,
353                     'description': order_line.product_id.name,
354                     'order_id': order_line.id,
355                     'state': 'order',
356                     'date': order_line.date,
357                 }
358                 cashmove_ref.create(cr, uid, values, context=context)
359                 order_line.write({'state': 'confirmed'}, context=context)
360         return self._update_order_lines(cr, uid, ids, context=context)
361
362     def _update_order_lines(self, cr, uid, ids, context=None):
363         """
364         Update the state of lunch.order based on its orderlines
365         """
366         orders_ref = self.pool.get('lunch.order')
367         orders = []
368         for order_line in self.browse(cr, uid, ids, context=context):
369             orders.append(order_line.order_id)
370         for order in set(orders):
371             isconfirmed = True
372             for orderline in order.order_line_ids:
373                 if orderline.state == 'new':
374                     isconfirmed = False
375                 if orderline.state == 'cancelled':
376                     isconfirmed = False
377                     orders_ref.write(cr, uid, [order.id], {'state': 'partially'}, context=context)
378             if isconfirmed:
379                 orders_ref.write(cr, uid, [order.id], {'state': 'confirmed'}, context=context)
380         return {}
381
382     def cancel(self, cr, uid, ids, context=None):
383         """
384         cancel one or more order.line, update order status and unlink existing cashmoves
385         """
386         cashmove_ref = self.pool.get('lunch.cashmove')
387         for order_line in self.browse(cr, uid, ids, context=context):
388             order_line.write({'state':'cancelled'}, context=context)
389             cash_ids = [cash.id for cash in order_line.cashmove]
390             cashmove_ref.unlink(cr, uid, cash_ids, context=context)
391         return self._update_order_lines(cr, uid, ids, context=context)
392     
393     def _get_line_order_ids(self, cr, uid, ids, context=None):
394         """
395         return the list of lunch.order.lines ids to which belong the  lunch.order 'ids'
396         """
397         result = set()
398         for lunch_order in self.browse(cr, uid, ids, context=context):
399             for lines in lunch_order.order_line_ids:
400                 result.add(lines.id)
401         return list(result)
402
403     _columns = {
404         'name': fields.related('product_id', 'name', readonly=True),
405         'order_id': fields.many2one('lunch.order', 'Order', ondelete='cascade'),
406         'product_id': fields.many2one('lunch.product', 'Product', required=True),
407         'date': fields.related('order_id', 'date', type='date', string="Date", readonly=True, store={
408             'lunch.order': (_get_line_order_ids, ['date'], 10), 
409             'lunch.order.line': (lambda self, cr, uid, ids, ctx: ids, [], 10),
410             }),
411         'supplier': fields.related('product_id', 'supplier', type='many2one', relation='res.partner', string="Supplier", readonly=True, store=True),
412         'user_id': fields.related('order_id', 'user_id', type='many2one', relation='res.users', string='User', readonly=True, store=True),
413         'note': fields.text('Note'),
414         'price': fields.float("Price"),
415         'state': fields.selection([('new', 'New'), \
416                                     ('confirmed', 'Received'), \
417                                     ('ordered', 'Ordered'),  \
418                                     ('cancelled', 'Cancelled')], \
419                                 'Status', readonly=True, select=True),
420         'cashmove': fields.one2many('lunch.cashmove', 'order_id', 'Cash Move', ondelete='cascade'),
421
422     }
423     _defaults = {
424         'state': 'new',
425     }
426
427
428 class lunch_product(osv.Model):
429     """ 
430     lunch product 
431     """
432     _name = 'lunch.product'
433     _description = 'lunch product'
434     _columns = {
435         'name': fields.char('Product', required=True, size=64),
436         'category_id': fields.many2one('lunch.product.category', 'Category', required=True),
437         'description': fields.text('Description', size=256),
438         'price': fields.float('Price', digits=(16,2)), #TODO: use decimal precision of 'Account', move it from product to decimal_precision
439         'supplier': fields.many2one('res.partner', 'Supplier'),
440     }
441
442 class lunch_product_category(osv.Model):
443     """ 
444     lunch product category 
445     """
446     _name = 'lunch.product.category'
447     _description = 'lunch product category'
448     _columns = {
449         'name': fields.char('Category', required=True), #such as PIZZA, SANDWICH, PASTA, CHINESE, BURGER, ...
450     }
451
452 class lunch_cashmove(osv.Model):
453     """ 
454     lunch cashmove => order or payment 
455     """
456     _name = 'lunch.cashmove'
457     _description = 'lunch cashmove'
458     _columns = {
459         'user_id': fields.many2one('res.users', 'User Name', required=True),
460         'date': fields.date('Date', required=True),
461         'amount': fields.float('Amount', required=True), #depending on the kind of cashmove, the amount will be positive or negative
462         'description': fields.text('Description'), #the description can be an order or a payment
463         'order_id': fields.many2one('lunch.order.line', 'Order', ondelete='cascade'),
464         'state': fields.selection([('order','Order'), ('payment','Payment')], 'Is an order or a Payment'),
465     }
466     _defaults = {
467         'user_id': lambda self, cr, uid, context: uid,
468         'date': fields.date.context_today,
469         'state': 'payment',
470     }
471
472 class lunch_alert(osv.Model):
473     """ 
474     lunch alert 
475     """
476     _name = 'lunch.alert'
477     _description = 'Lunch Alert'
478     _columns = {
479         'message': fields.text('Message', size=256, required=True),
480         'alter_type': fields.selection([('specific', 'Specific Day'), \
481                                     ('week', 'Every Week'), \
482                                     ('days', 'Every Day')], \
483                                 string='Recurrency', required=True, select=True),
484         'specific_day': fields.date('Day'),
485         'monday': fields.boolean('Monday'),
486         'tuesday': fields.boolean('Tuesday'),
487         'wednesday': fields.boolean('Wednesday'),
488         'thursday': fields.boolean('Thursday'),
489         'friday': fields.boolean('Friday'),
490         'saturday': fields.boolean('Saturday'),
491         'sunday':  fields.boolean('Sunday'),
492         'active_from': fields.float('Between', required=True),
493         'active_to': fields.float('And', required=True),
494     }
495     _defaults = {
496         'alter_type': 'specific',
497         'specific_day': fields.date.context_today,
498         'active_from': 7,
499         'active_to': 23,
500     }