merge with trunk and removed *.po
[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='create_date 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                     for val in value:
227                         for pref in val.values():
228                             #We only show 5 preferences per category (or it will be too long)
229                             if i==5: break
230                             i+=1
231                             xml_pref_3 = etree.Element("div")
232                             xml_pref_3.set('class','oe_lunch_vignette')
233                             xml_pref_1.append(xml_pref_3)
234
235                             xml_pref_4 = etree.Element("span")
236                             xml_pref_4.set('class','oe_lunch_button')
237                             xml_pref_3.append(xml_pref_4)
238
239                             xml_pref_5 = etree.Element("button")
240                             xml_pref_5.set('name',"add_preference_"+str(pref.id))
241                             xml_pref_5.set('class','oe_link oe_i oe_button_plus')
242                             xml_pref_5.set('type','object')
243                             xml_pref_5.set('string','+')
244                             xml_pref_4.append(xml_pref_5)
245
246                             xml_pref_6 = etree.Element("button")
247                             xml_pref_6.set('name',"add_preference_"+str(pref.id))
248                             xml_pref_6.set('class','oe_link oe_button_add')
249                             xml_pref_6.set('type','object')
250                             xml_pref_6.set('string',_("Add"))
251                             xml_pref_4.append(xml_pref_6)
252
253                             xml_pref_7 = etree.Element("div")
254                             xml_pref_7.set('class','oe_group_text_button')
255                             xml_pref_3.append(xml_pref_7)
256
257                             xml_pref_8 = etree.Element("div")
258                             xml_pref_8.set('class','oe_lunch_text')
259                             xml_pref_8.text = escape(pref.product_id.name)+str(" ")
260                             xml_pref_7.append(xml_pref_8)
261
262                             price = pref.product_id.price or 0.0
263                             cur = currency.name or ''
264                             xml_pref_9 = etree.Element("span")
265                             xml_pref_9.set('class','oe_tag')
266                             xml_pref_9.text = str(price)+str(" ")+cur
267                             xml_pref_8.append(xml_pref_9)
268
269                             xml_pref_10 = etree.Element("div")
270                             xml_pref_10.set('class','oe_grey')
271                             xml_pref_10.text = escape(pref.note or '')
272                             xml_pref_3.append(xml_pref_10)
273
274                             xml_start.append(xml_pref_1)
275
276             first_node = doc.xpath("//div[@name='preferences']")
277             if first_node and len(first_node)>0:
278                 first_node[0].append(xml_start)
279             res['arch'] = etree.tostring(doc)
280         return res
281
282     _columns = {
283         'user_id': fields.many2one('res.users', 'User Name', required=True, readonly=True, states={'new':[('readonly', False)]}),
284         'date': fields.date('Date', required=True, readonly=True, states={'new':[('readonly', False)]}),
285         'order_line_ids': fields.one2many('lunch.order.line', 'order_id', 'Products', ondelete="cascade", readonly=True, states={'new':[('readonly', False)]}),
286         'total': fields.function(_price_get, string="Total", store={
287                  'lunch.order.line': (_fetch_orders_from_lines, ['product_id','order_id'], 20),
288             }),
289         'state': fields.selection([('new', 'New'), \
290                                     ('confirmed','Confirmed'), \
291                                     ('cancelled','Cancelled'), \
292                                     ('partially','Partially Confirmed')] \
293                                 ,'Status', readonly=True, select=True),
294         'alerts': fields.function(_alerts_get, string="Alerts", type='text'),
295     }
296
297     _defaults = {
298         'user_id': lambda self, cr, uid, context: uid,
299         'date': fields.date.context_today,
300         'state': 'new',
301         'alerts': _default_alerts_get,
302     }
303
304
305 class lunch_order_line(osv.Model):
306     """ 
307     lunch order line: one lunch order can have many order lines
308     """
309     _name = 'lunch.order.line'
310     _description = 'lunch order line'
311
312     def onchange_price(self, cr, uid, ids, product_id, context=None):
313         if product_id:
314             price = self.pool.get('lunch.product').browse(cr, uid, product_id, context=context).price
315             return {'value': {'price': price}}
316         return {'value': {'price': 0.0}}
317
318     def order(self, cr, uid, ids, context=None):
319         """ 
320         The order_line is ordered to the supplier but isn't received yet
321         """
322         for order_line in self.browse(cr, uid, ids, context=context):
323             order_line.write({'state': 'ordered'}, context=context)
324         return self._update_order_lines(cr, uid, ids, context=context)
325
326     def confirm(self, cr, uid, ids, context=None):
327         """ 
328         confirm one or more order line, update order status and create new cashmove 
329         """
330         cashmove_ref = self.pool.get('lunch.cashmove')
331         for order_line in self.browse(cr, uid, ids, context=context):
332             if order_line.state != 'confirmed':
333                 values = {
334                     'user_id': order_line.user_id.id,
335                     'amount': -order_line.price,
336                     'description': order_line.product_id.name,
337                     'order_id': order_line.id,
338                     'state': 'order',
339                     'date': order_line.date,
340                 }
341                 cashmove_ref.create(cr, uid, values, context=context)
342                 order_line.write({'state': 'confirmed'}, context=context)
343         return self._update_order_lines(cr, uid, ids, context=context)
344
345     def _update_order_lines(self, cr, uid, ids, context=None):
346         """
347         Update the state of lunch.order based on its orderlines
348         """
349         orders_ref = self.pool.get('lunch.order')
350         orders = []
351         for order_line in self.browse(cr, uid, ids, context=context):
352             orders.append(order_line.order_id)
353         for order in set(orders):
354             isconfirmed = True
355             for orderline in order.order_line_ids:
356                 if orderline.state == 'new':
357                     isconfirmed = False
358                 if orderline.state == 'cancelled':
359                     isconfirmed = False
360                     orders_ref.write(cr, uid, [order.id], {'state': 'partially'}, context=context)
361             if isconfirmed:
362                 orders_ref.write(cr, uid, [order.id], {'state': 'confirmed'}, context=context)
363         return {}
364
365     def cancel(self, cr, uid, ids, context=None):
366         """
367         cancel one or more order.line, update order status and unlink existing cashmoves
368         """
369         cashmove_ref = self.pool.get('lunch.cashmove')
370         for order_line in self.browse(cr, uid, ids, context=context):
371             order_line.write({'state':'cancelled'}, context=context)
372             cash_ids = [cash.id for cash in order_line.cashmove]
373             cashmove_ref.unlink(cr, uid, cash_ids, context=context)
374         return self._update_order_lines(cr, uid, ids, context=context)
375     
376     def _get_line_order_ids(self, cr, uid, ids, context=None):
377         """
378         return the list of lunch.order.lines ids to which belong the  lunch.order 'ids'
379         """
380         result = set()
381         for lunch_order in self.browse(cr, uid, ids, context=context):
382             for lines in lunch_order.order_line_ids:
383                 result.add(lines.id)
384         return list(result)
385
386     _columns = {
387         'name': fields.related('product_id', 'name', readonly=True),
388         'order_id': fields.many2one('lunch.order', 'Order', ondelete='cascade'),
389         'product_id': fields.many2one('lunch.product', 'Product', required=True),
390         'date': fields.related('order_id', 'date', type='date', string="Date", readonly=True, store={
391             'lunch.order': (_get_line_order_ids, ['date'], 10), 
392             'lunch.order.line': (lambda self, cr, uid, ids, ctx: ids, [], 10),
393             }),
394         'supplier': fields.related('product_id', 'supplier', type='many2one', relation='res.partner', string="Supplier", readonly=True, store=True),
395         'user_id': fields.related('order_id', 'user_id', type='many2one', relation='res.users', string='User', readonly=True, store=True),
396         'note': fields.text('Note'),
397         'price': fields.float("Price"),
398         'state': fields.selection([('new', 'New'), \
399                                     ('confirmed', 'Received'), \
400                                     ('ordered', 'Ordered'),  \
401                                     ('cancelled', 'Cancelled')], \
402                                 'Status', readonly=True, select=True),
403         'cashmove': fields.one2many('lunch.cashmove', 'order_id', 'Cash Move', ondelete='cascade'),
404
405     }
406     _defaults = {
407         'state': 'new',
408     }
409
410
411 class lunch_product(osv.Model):
412     """ 
413     lunch product 
414     """
415     _name = 'lunch.product'
416     _description = 'lunch product'
417     _columns = {
418         'name': fields.char('Product', required=True, size=64),
419         'category_id': fields.many2one('lunch.product.category', 'Category', required=True),
420         'description': fields.text('Description', size=256),
421         'price': fields.float('Price', digits=(16,2)), #TODO: use decimal precision of 'Account', move it from product to decimal_precision
422         'supplier': fields.many2one('res.partner', 'Supplier'),
423     }
424
425 class lunch_product_category(osv.Model):
426     """ 
427     lunch product category 
428     """
429     _name = 'lunch.product.category'
430     _description = 'lunch product category'
431     _columns = {
432         'name': fields.char('Category', required=True), #such as PIZZA, SANDWICH, PASTA, CHINESE, BURGER, ...
433     }
434
435 class lunch_cashmove(osv.Model):
436     """ 
437     lunch cashmove => order or payment 
438     """
439     _name = 'lunch.cashmove'
440     _description = 'lunch cashmove'
441     _columns = {
442         'user_id': fields.many2one('res.users', 'User Name', required=True),
443         'date': fields.date('Date', required=True),
444         'amount': fields.float('Amount', required=True), #depending on the kind of cashmove, the amount will be positive or negative
445         'description': fields.text('Description'), #the description can be an order or a payment
446         'order_id': fields.many2one('lunch.order.line', 'Order', ondelete='cascade'),
447         'state': fields.selection([('order','Order'), ('payment','Payment')], 'Is an order or a Payment'),
448     }
449     _defaults = {
450         'user_id': lambda self, cr, uid, context: uid,
451         'date': fields.date.context_today,
452         'state': 'payment',
453     }
454
455 class lunch_alert(osv.Model):
456     """ 
457     lunch alert 
458     """
459     _name = 'lunch.alert'
460     _description = 'Lunch Alert'
461     _columns = {
462         'message': fields.text('Message', size=256, required=True),
463         'alter_type': fields.selection([('specific', 'Specific Day'), \
464                                     ('week', 'Every Week'), \
465                                     ('days', 'Every Day')], \
466                                 string='Recurrency', required=True, select=True),
467         'specific_day': fields.date('Day'),
468         'monday': fields.boolean('Monday'),
469         'tuesday': fields.boolean('Tuesday'),
470         'wednesday': fields.boolean('Wednesday'),
471         'thursday': fields.boolean('Thursday'),
472         'friday': fields.boolean('Friday'),
473         'saturday': fields.boolean('Saturday'),
474         'sunday':  fields.boolean('Sunday'),
475         'active_from': fields.float('Between', required=True),
476         'active_to': fields.float('And', required=True),
477     }
478     _defaults = {
479         'alter_type': 'specific',
480         'specific_day': fields.date.context_today,
481         'active_from': 7,
482         'active_to': 23,
483     }