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