[FIX] mail: no_auto_thread default True if the model has no mail.thread
[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',
303                                           ondelete="cascade", readonly=True, states={'new':[('readonly', False)]},
304                                           copy=True),
305         'total': fields.function(_price_get, string="Total", store={
306                  'lunch.order.line': (_fetch_orders_from_lines, ['product_id','order_id'], 20),
307             }),
308         'state': fields.selection([('new', 'New'), \
309                                     ('confirmed','Confirmed'), \
310                                     ('cancelled','Cancelled'), \
311                                     ('partially','Partially Confirmed')] \
312                                 ,'Status', readonly=True, select=True, copy=False),
313         'alerts': fields.function(_alerts_get, string="Alerts", type='text'),
314     }
315
316     _defaults = {
317         'user_id': lambda self, cr, uid, context: uid,
318         'date': fields.date.context_today,
319         'state': 'new',
320         'alerts': _default_alerts_get,
321     }
322
323
324 class lunch_order_line(osv.Model):
325     """ 
326     lunch order line: one lunch order can have many order lines
327     """
328     _name = 'lunch.order.line'
329     _description = 'lunch order line'
330
331     def onchange_price(self, cr, uid, ids, product_id, context=None):
332         if product_id:
333             price = self.pool.get('lunch.product').browse(cr, uid, product_id, context=context).price
334             return {'value': {'price': price}}
335         return {'value': {'price': 0.0}}
336
337     def order(self, cr, uid, ids, context=None):
338         """ 
339         The order_line is ordered to the supplier but isn't received yet
340         """
341         self.write(cr, uid, ids, {'state': 'ordered'}, context=context)
342         return self._update_order_lines(cr, uid, ids, context=context)
343
344     def confirm(self, cr, uid, ids, context=None):
345         """ 
346         confirm one or more order line, update order status and create new cashmove 
347         """
348         cashmove_ref = self.pool.get('lunch.cashmove')
349         for order_line in self.browse(cr, uid, ids, context=context):
350             if order_line.state != 'confirmed':
351                 values = {
352                     'user_id': order_line.user_id.id,
353                     'amount': -order_line.price,
354                     'description': order_line.product_id.name,
355                     'order_id': order_line.id,
356                     'state': 'order',
357                     'date': order_line.date,
358                 }
359                 cashmove_ref.create(cr, uid, values, context=context)
360                 order_line.write({'state': 'confirmed'})
361         return self._update_order_lines(cr, uid, ids, context=context)
362
363     def _update_order_lines(self, cr, uid, ids, context=None):
364         """
365         Update the state of lunch.order based on its orderlines
366         """
367         orders_ref = self.pool.get('lunch.order')
368         orders = []
369         for order_line in self.browse(cr, uid, ids, context=context):
370             orders.append(order_line.order_id)
371         for order in set(orders):
372             isconfirmed = True
373             for orderline in order.order_line_ids:
374                 if orderline.state == 'new':
375                     isconfirmed = False
376                 if orderline.state == 'cancelled':
377                     isconfirmed = False
378                     orders_ref.write(cr, uid, [order.id], {'state': 'partially'}, context=context)
379             if isconfirmed:
380                 orders_ref.write(cr, uid, [order.id], {'state': 'confirmed'}, context=context)
381         return {}
382
383     def cancel(self, cr, uid, ids, context=None):
384         """
385         cancel one or more order.line, update order status and unlink existing cashmoves
386         """
387         cashmove_ref = self.pool.get('lunch.cashmove')
388         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
389         for order_line in self.browse(cr, uid, ids, context=context):
390             cash_ids = [cash.id for cash in order_line.cashmove]
391             cashmove_ref.unlink(cr, uid, cash_ids, context=context)
392         return self._update_order_lines(cr, uid, ids, context=context)
393     
394     def _get_line_order_ids(self, cr, uid, ids, context=None):
395         """
396         return the list of lunch.order.lines ids to which belong the  lunch.order 'ids'
397         """
398         result = set()
399         for lunch_order in self.browse(cr, uid, ids, context=context):
400             for lines in lunch_order.order_line_ids:
401                 result.add(lines.id)
402         return list(result)
403
404     _columns = {
405         'name': fields.related('product_id', 'name', readonly=True),
406         'order_id': fields.many2one('lunch.order', 'Order', ondelete='cascade'),
407         'product_id': fields.many2one('lunch.product', 'Product', required=True),
408         'date': fields.related('order_id', 'date', type='date', string="Date", readonly=True, store={
409             'lunch.order': (_get_line_order_ids, ['date'], 10), 
410             'lunch.order.line': (lambda self, cr, uid, ids, ctx: ids, [], 10),
411             }),
412         'supplier': fields.related('product_id', 'supplier', type='many2one', relation='res.partner', string="Supplier", readonly=True, store=True),
413         'user_id': fields.related('order_id', 'user_id', type='many2one', relation='res.users', string='User', readonly=True, store=True),
414         'note': fields.text('Note'),
415         'price': fields.float("Price"),
416         'state': fields.selection([('new', 'New'), \
417                                     ('confirmed', 'Received'), \
418                                     ('ordered', 'Ordered'),  \
419                                     ('cancelled', 'Cancelled')], \
420                                 'Status', readonly=True, select=True),
421         'cashmove': fields.one2many('lunch.cashmove', 'order_id', 'Cash Move', ondelete='cascade'),
422
423     }
424     _defaults = {
425         'state': 'new',
426     }
427
428
429 class lunch_product(osv.Model):
430     """ 
431     lunch product 
432     """
433     _name = 'lunch.product'
434     _description = 'lunch product'
435     _columns = {
436         'name': fields.char('Product', required=True),
437         'category_id': fields.many2one('lunch.product.category', 'Category', required=True),
438         'description': fields.text('Description', size=256),
439         'price': fields.float('Price', digits=(16,2)), #TODO: use decimal precision of 'Account', move it from product to decimal_precision
440         'supplier': fields.many2one('res.partner', 'Supplier'),
441     }
442
443 class lunch_product_category(osv.Model):
444     """ 
445     lunch product category 
446     """
447     _name = 'lunch.product.category'
448     _description = 'lunch product category'
449     _columns = {
450         'name': fields.char('Category', required=True), #such as PIZZA, SANDWICH, PASTA, CHINESE, BURGER, ...
451     }
452
453 class lunch_cashmove(osv.Model):
454     """ 
455     lunch cashmove => order or payment 
456     """
457     _name = 'lunch.cashmove'
458     _description = 'lunch cashmove'
459     _columns = {
460         'user_id': fields.many2one('res.users', 'User Name', required=True),
461         'date': fields.date('Date', required=True),
462         'amount': fields.float('Amount', required=True), #depending on the kind of cashmove, the amount will be positive or negative
463         'description': fields.text('Description'), #the description can be an order or a payment
464         'order_id': fields.many2one('lunch.order.line', 'Order', ondelete='cascade'),
465         'state': fields.selection([('order','Order'), ('payment','Payment')], 'Is an order or a Payment'),
466     }
467     _defaults = {
468         'user_id': lambda self, cr, uid, context: uid,
469         'date': fields.date.context_today,
470         'state': 'payment',
471     }
472
473 class lunch_alert(osv.Model):
474     """ 
475     lunch alert 
476     """
477     _name = 'lunch.alert'
478     _description = 'Lunch Alert'
479     _columns = {
480         'message': fields.text('Message', size=256, required=True),
481         'alter_type': fields.selection([('specific', 'Specific Day'), \
482                                     ('week', 'Every Week'), \
483                                     ('days', 'Every Day')], \
484                                 string='Recurrency', required=True, select=True),
485         'specific_day': fields.date('Day'),
486         'monday': fields.boolean('Monday'),
487         'tuesday': fields.boolean('Tuesday'),
488         'wednesday': fields.boolean('Wednesday'),
489         'thursday': fields.boolean('Thursday'),
490         'friday': fields.boolean('Friday'),
491         'saturday': fields.boolean('Saturday'),
492         'sunday':  fields.boolean('Sunday'),
493         'active_from': fields.float('Between', required=True),
494         'active_to': fields.float('And', required=True),
495     }
496     _defaults = {
497         'alter_type': 'specific',
498         'specific_day': fields.date.context_today,
499         'active_from': 7,
500         'active_to': 23,
501     }