lunch_order.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from odoo import api, fields, models, _
  4. from odoo.exceptions import ValidationError, UserError
  5. class LunchOrder(models.Model):
  6. _name = 'lunch.order'
  7. _description = 'Lunch Order'
  8. _order = 'id desc'
  9. _display_name = 'product_id'
  10. name = fields.Char(related='product_id.name', string="Product Name", store=True, readonly=True)
  11. topping_ids_1 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 1', domain=[('topping_category', '=', 1)])
  12. topping_ids_2 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 2', domain=[('topping_category', '=', 2)])
  13. topping_ids_3 = fields.Many2many('lunch.topping', 'lunch_order_topping', 'order_id', 'topping_id', string='Extras 3', domain=[('topping_category', '=', 3)])
  14. product_id = fields.Many2one('lunch.product', string="Product", required=True)
  15. category_id = fields.Many2one(
  16. string='Product Category', related='product_id.category_id', store=True)
  17. date = fields.Date('Order Date', required=True, readonly=True,
  18. states={'new': [('readonly', False)]},
  19. default=fields.Date.context_today)
  20. supplier_id = fields.Many2one(
  21. string='Vendor', related='product_id.supplier_id', store=True, index=True)
  22. available_today = fields.Boolean(related='supplier_id.available_today')
  23. order_deadline_passed = fields.Boolean(related='supplier_id.order_deadline_passed')
  24. user_id = fields.Many2one('res.users', 'User', readonly=True,
  25. states={'new': [('readonly', False)]},
  26. default=lambda self: self.env.uid)
  27. lunch_location_id = fields.Many2one('lunch.location', default=lambda self: self.env.user.last_lunch_location_id)
  28. note = fields.Text('Notes')
  29. price = fields.Monetary('Total Price', compute='_compute_total_price', readonly=True, store=True)
  30. active = fields.Boolean('Active', default=True)
  31. state = fields.Selection([('new', 'To Order'),
  32. ('ordered', 'Ordered'), # "Internally" ordered
  33. ('sent', 'Sent'), # Order sent to the supplier
  34. ('confirmed', 'Received'), # Order received
  35. ('cancelled', 'Cancelled')],
  36. 'Status', readonly=True, index=True, default='new')
  37. notified = fields.Boolean(default=False)
  38. company_id = fields.Many2one('res.company', default=lambda self: self.env.company.id)
  39. currency_id = fields.Many2one(related='company_id.currency_id', store=True)
  40. quantity = fields.Float('Quantity', required=True, default=1)
  41. display_toppings = fields.Text('Extras', compute='_compute_display_toppings', store=True)
  42. product_description = fields.Html('Description', related='product_id.description')
  43. topping_label_1 = fields.Char(related='product_id.supplier_id.topping_label_1')
  44. topping_label_2 = fields.Char(related='product_id.supplier_id.topping_label_2')
  45. topping_label_3 = fields.Char(related='product_id.supplier_id.topping_label_3')
  46. topping_quantity_1 = fields.Selection(related='product_id.supplier_id.topping_quantity_1')
  47. topping_quantity_2 = fields.Selection(related='product_id.supplier_id.topping_quantity_2')
  48. topping_quantity_3 = fields.Selection(related='product_id.supplier_id.topping_quantity_3')
  49. image_1920 = fields.Image(compute='_compute_product_images')
  50. image_128 = fields.Image(compute='_compute_product_images')
  51. available_toppings_1 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
  52. available_toppings_2 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
  53. available_toppings_3 = fields.Boolean(help='Are extras available for this product', compute='_compute_available_toppings')
  54. display_reorder_button = fields.Boolean(compute='_compute_display_reorder_button')
  55. @api.depends('product_id')
  56. def _compute_product_images(self):
  57. for line in self:
  58. line.image_1920 = line.product_id.image_1920 or line.category_id.image_1920
  59. line.image_128 = line.product_id.image_128 or line.category_id.image_128
  60. @api.depends('category_id')
  61. def _compute_available_toppings(self):
  62. for order in self:
  63. order.available_toppings_1 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 1)]))
  64. order.available_toppings_2 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 2)]))
  65. order.available_toppings_3 = bool(order.env['lunch.topping'].search_count([('supplier_id', '=', order.supplier_id.id), ('topping_category', '=', 3)]))
  66. @api.depends_context('show_reorder_button')
  67. @api.depends('state')
  68. def _compute_display_reorder_button(self):
  69. show_button = self.env.context.get('show_reorder_button')
  70. for order in self:
  71. order.display_reorder_button = show_button and order.state == 'confirmed' and order.supplier_id.available_today
  72. def init(self):
  73. self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)"""
  74. % self._table)
  75. def _extract_toppings(self, values):
  76. """
  77. If called in api.multi then it will pop topping_ids_1,2,3 from values
  78. """
  79. if self.ids:
  80. # TODO This is not taking into account all the toppings for each individual order, this is usually not a problem
  81. # since in the interface you usually don't update more than one order at a time but this is a bug nonetheless
  82. topping_1 = values.pop('topping_ids_1')[0][2] if 'topping_ids_1' in values else self[:1].topping_ids_1.ids
  83. topping_2 = values.pop('topping_ids_2')[0][2] if 'topping_ids_2' in values else self[:1].topping_ids_2.ids
  84. topping_3 = values.pop('topping_ids_3')[0][2] if 'topping_ids_3' in values else self[:1].topping_ids_3.ids
  85. else:
  86. topping_1 = values['topping_ids_1'][0][2] if 'topping_ids_1' in values else []
  87. topping_2 = values['topping_ids_2'][0][2] if 'topping_ids_2' in values else []
  88. topping_3 = values['topping_ids_3'][0][2] if 'topping_ids_3' in values else []
  89. return topping_1 + topping_2 + topping_3
  90. @api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
  91. def _check_topping_quantity(self):
  92. errors = {
  93. '1_more': _('You should order at least one %s'),
  94. '1': _('You have to order one and only one %s'),
  95. }
  96. for line in self:
  97. for index in range(1, 4):
  98. availability = line['available_toppings_%s' % index]
  99. quantity = line['topping_quantity_%s' % index]
  100. toppings = line['topping_ids_%s' % index].filtered(lambda x: x.topping_category == index)
  101. label = line['topping_label_%s' % index]
  102. if availability and quantity != '0_more':
  103. check = bool(len(toppings) == 1 if quantity == '1' else toppings)
  104. if not check:
  105. raise ValidationError(errors[quantity] % label)
  106. @api.model_create_multi
  107. def create(self, vals_list):
  108. orders = self.env['lunch.order']
  109. for vals in vals_list:
  110. lines = self._find_matching_lines({
  111. **vals,
  112. 'toppings': self._extract_toppings(vals),
  113. })
  114. if lines.filtered(lambda l: l.state not in ['sent', 'confirmed']):
  115. # YTI FIXME This will update multiple lines in the case there are multiple
  116. # matching lines which should not happen through the interface
  117. lines.update_quantity(1)
  118. orders |= lines[:1]
  119. else:
  120. orders |= super().create(vals)
  121. return orders
  122. def write(self, values):
  123. merge_needed = 'note' in values or 'topping_ids_1' in values or 'topping_ids_2' in values or 'topping_ids_3' in values
  124. default_location_id = self.env.user.last_lunch_location_id and self.env.user.last_lunch_location_id.id or False
  125. if merge_needed:
  126. lines_to_deactivate = self.env['lunch.order']
  127. for line in self:
  128. # Only write on topping_ids_1 because they all share the same table
  129. # and we don't want to remove all the records
  130. # _extract_toppings will pop topping_ids_1, topping_ids_2 and topping_ids_3 from values
  131. # This also forces us to invalidate the cache for topping_ids_2 and topping_ids_3 that
  132. # could have changed through topping_ids_1 without the cache knowing about it
  133. toppings = self._extract_toppings(values)
  134. self.invalidate_model(['topping_ids_2', 'topping_ids_3'])
  135. values['topping_ids_1'] = [(6, 0, toppings)]
  136. matching_lines = self._find_matching_lines({
  137. 'user_id': values.get('user_id', line.user_id.id),
  138. 'product_id': values.get('product_id', line.product_id.id),
  139. 'note': values.get('note', line.note or False),
  140. 'toppings': toppings,
  141. 'lunch_location_id': values.get('lunch_location_id', default_location_id),
  142. })
  143. if matching_lines:
  144. lines_to_deactivate |= line
  145. matching_lines.update_quantity(line.quantity)
  146. lines_to_deactivate.write({'active': False})
  147. return super(LunchOrder, self - lines_to_deactivate).write(values)
  148. return super().write(values)
  149. @api.model
  150. def _find_matching_lines(self, values):
  151. default_location_id = self.env.user.last_lunch_location_id and self.env.user.last_lunch_location_id.id or False
  152. domain = [
  153. ('user_id', '=', values.get('user_id', self.default_get(['user_id'])['user_id'])),
  154. ('product_id', '=', values.get('product_id', False)),
  155. ('date', '=', fields.Date.today()),
  156. ('note', '=', values.get('note', False)),
  157. ('lunch_location_id', '=', values.get('lunch_location_id', default_location_id)),
  158. ]
  159. toppings = values.get('toppings', [])
  160. return self.search(domain).filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).ids == toppings)
  161. @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity')
  162. def _compute_total_price(self):
  163. for line in self:
  164. line.price = line.quantity * (line.product_id.price + sum((line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3).mapped('price')))
  165. @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
  166. def _compute_display_toppings(self):
  167. for line in self:
  168. toppings = line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3
  169. line.display_toppings = ' + '.join(toppings.mapped('name'))
  170. def update_quantity(self, increment):
  171. for line in self.filtered(lambda line: line.state not in ['sent', 'confirmed']):
  172. if line.quantity <= -increment:
  173. # TODO: maybe unlink the order?
  174. line.active = False
  175. else:
  176. line.quantity += increment
  177. self._check_wallet()
  178. def add_to_cart(self):
  179. """
  180. This method currently does nothing, we currently need it in order to
  181. be able to reuse this model in place of a wizard
  182. """
  183. # YTI FIXME: Find a way to drop this.
  184. return True
  185. def _check_wallet(self):
  186. self.env.flush_all()
  187. for line in self:
  188. if self.env['lunch.cashmove'].get_wallet_balance(line.user_id) < 0:
  189. raise ValidationError(_('Your wallet does not contain enough money to order that. To add some money to your wallet, please contact your lunch manager.'))
  190. def action_order(self):
  191. for order in self:
  192. if not order.supplier_id.available_today:
  193. raise UserError(_('The vendor related to this order is not available today.'))
  194. if self.filtered(lambda line: not line.product_id.active):
  195. raise ValidationError(_('Product is no longer available.'))
  196. self.write({
  197. 'state': 'ordered',
  198. })
  199. for order in self:
  200. order.lunch_location_id = order.user_id.last_lunch_location_id
  201. self._check_wallet()
  202. def action_reorder(self):
  203. self.ensure_one()
  204. if not self.supplier_id.available_today:
  205. raise UserError(_('The vendor related to this order is not available today.'))
  206. self.copy({
  207. 'date': fields.Date.context_today(self),
  208. 'state': 'ordered',
  209. })
  210. action = self.env['ir.actions.act_window']._for_xml_id('lunch.lunch_order_action')
  211. return action
  212. def action_confirm(self):
  213. self.write({'state': 'confirmed'})
  214. def action_cancel(self):
  215. self.write({'state': 'cancelled'})
  216. def action_reset(self):
  217. self.write({'state': 'ordered'})
  218. def action_send(self):
  219. self.state = 'sent'
  220. def action_notify(self):
  221. self -= self.filtered('notified')
  222. if not self:
  223. return
  224. notified_users = set()
  225. # (company, lang): (subject, body)
  226. translate_cache = dict()
  227. for order in self:
  228. user = order.user_id
  229. if user in notified_users:
  230. continue
  231. _key = (order.company_id, user.lang)
  232. if _key not in translate_cache:
  233. context = {'lang': user.lang}
  234. translate_cache[_key] = (_('Lunch notification'), order.company_id.with_context(lang=user.lang).lunch_notify_message)
  235. del context
  236. subject, body = translate_cache[_key]
  237. user.partner_id.message_notify(
  238. subject=subject,
  239. body=body,
  240. partner_ids=user.partner_id.ids,
  241. email_layout_xmlid='mail.mail_notification_light',
  242. )
  243. notified_users.add(user)
  244. self.write({'notified': True})