summaryrefslogtreecommitdiff
path: root/addons/pos_restaurant/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_restaurant/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/models')
-rw-r--r--addons/pos_restaurant/models/__init__.py7
-rw-r--r--addons/pos_restaurant/models/pos_config.py64
-rw-r--r--addons/pos_restaurant/models/pos_order.py237
-rw-r--r--addons/pos_restaurant/models/pos_payment.py15
-rw-r--r--addons/pos_restaurant/models/pos_restaurant.py99
5 files changed, 422 insertions, 0 deletions
diff --git a/addons/pos_restaurant/models/__init__.py b/addons/pos_restaurant/models/__init__.py
new file mode 100644
index 00000000..09ed911c
--- /dev/null
+++ b/addons/pos_restaurant/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import pos_config
+from . import pos_order
+from . import pos_payment
+from . import pos_restaurant
diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py
new file mode 100644
index 00000000..a80dde70
--- /dev/null
+++ b/addons/pos_restaurant/models/pos_config.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class PosConfig(models.Model):
+ _inherit = 'pos.config'
+
+ iface_splitbill = fields.Boolean(string='Bill Splitting', help='Enables Bill Splitting in the Point of Sale.')
+ iface_printbill = fields.Boolean(string='Bill Printing', help='Allows to print the Bill before payment.')
+ iface_orderline_notes = fields.Boolean(string='Notes', help='Allow custom notes on Orderlines.')
+ floor_ids = fields.One2many('restaurant.floor', 'pos_config_id', string='Restaurant Floors', help='The restaurant floors served by this point of sale.')
+ printer_ids = fields.Many2many('restaurant.printer', 'pos_config_printer_rel', 'config_id', 'printer_id', string='Order Printers')
+ is_table_management = fields.Boolean('Floors & Tables')
+ is_order_printer = fields.Boolean('Order Printer')
+ set_tip_after_payment = fields.Boolean('Set Tip After Payment', help="Adjust the amount authorized by payment terminals to add a tip after the customers left or at the end of the day.")
+ module_pos_restaurant = fields.Boolean(default=True)
+
+ @api.onchange('module_pos_restaurant')
+ def _onchange_module_pos_restaurant(self):
+ if not self.module_pos_restaurant:
+ self.update({'iface_printbill': False,
+ 'iface_splitbill': False,
+ 'is_order_printer': False,
+ 'is_table_management': False,
+ 'iface_orderline_notes': False})
+
+ @api.onchange('iface_tipproduct')
+ def _onchange_iface_tipproduct(self):
+ if not self.iface_tipproduct:
+ self.set_tip_after_payment = False
+
+ def _force_http(self):
+ if self.printer_ids.filtered(lambda pt: pt.printer_type == 'epson_epos'):
+ return True
+ return super(PosConfig, self)._force_http()
+
+ def get_tables_order_count(self):
+ """ """
+ self.ensure_one()
+ tables = self.env['restaurant.table'].search([('floor_id.pos_config_id', 'in', self.ids)])
+ domain = [('state', '=', 'draft'), ('table_id', 'in', tables.ids)]
+
+ order_stats = self.env['pos.order'].read_group(domain, ['table_id'], 'table_id')
+ orders_map = dict((s['table_id'][0], s['table_id_count']) for s in order_stats)
+
+ result = []
+ for table in tables:
+ result.append({'id': table.id, 'orders': orders_map.get(table.id, 0)})
+ return result
+
+ def _get_forbidden_change_fields(self):
+ forbidden_keys = super(PosConfig, self)._get_forbidden_change_fields()
+ forbidden_keys.append('is_table_management')
+ forbidden_keys.append('floor_ids')
+ return forbidden_keys
+
+ def write(self, vals):
+ if ('is_table_management' in vals and vals['is_table_management'] == False):
+ vals['floor_ids'] = [(5, 0, 0)]
+ if ('is_order_printer' in vals and vals['is_order_printer'] == False):
+ vals['printer_ids'] = [(5, 0, 0)]
+ return super(PosConfig, self).write(vals)
diff --git a/addons/pos_restaurant/models/pos_order.py b/addons/pos_restaurant/models/pos_order.py
new file mode 100644
index 00000000..4dc6e4fa
--- /dev/null
+++ b/addons/pos_restaurant/models/pos_order.py
@@ -0,0 +1,237 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from itertools import groupby
+from re import search
+from functools import partial
+
+from odoo import api, fields, models
+
+
+class PosOrderLine(models.Model):
+ _inherit = 'pos.order.line'
+
+ note = fields.Char('Note added by the waiter.')
+ mp_skip = fields.Boolean('Skip line when sending ticket to kitchen printers.')
+ mp_dirty = fields.Boolean()
+
+
+class PosOrder(models.Model):
+ _inherit = 'pos.order'
+
+ table_id = fields.Many2one('restaurant.table', string='Table', help='The table where this order was served', index=True)
+ customer_count = fields.Integer(string='Guests', help='The amount of customers that have been served by this order.')
+ multiprint_resume = fields.Char()
+
+ def _get_pack_lot_lines(self, order_lines):
+ """Add pack_lot_lines to the order_lines.
+
+ The function doesn't return anything but adds the results directly to the order_lines.
+
+ :param order_lines: order_lines for which the pack_lot_lines are to be requested.
+ :type order_lines: pos.order.line.
+ """
+ pack_lots = self.env['pos.pack.operation.lot'].search_read(
+ domain = [('pos_order_line_id', 'in', [order_line['id'] for order_line in order_lines])],
+ fields = [
+ 'id',
+ 'lot_name',
+ 'pos_order_line_id'
+ ])
+ for pack_lot in pack_lots:
+ pack_lot['order_line'] = pack_lot['pos_order_line_id'][0]
+ pack_lot['server_id'] = pack_lot['id']
+
+ del pack_lot['pos_order_line_id']
+ del pack_lot['id']
+
+ for order_line_id, pack_lot_ids in groupby(pack_lots, key=lambda x:x['order_line']):
+ next(order_line for order_line in order_lines if order_line['id'] == order_line_id)['pack_lot_ids'] = list(pack_lots)
+
+ def _get_fields_for_order_line(self):
+ return [
+ 'id',
+ 'discount',
+ 'product_id',
+ 'price_unit',
+ 'order_id',
+ 'qty',
+ 'note',
+ 'mp_skip',
+ 'mp_dirty',
+ 'full_product_name',
+ ]
+
+ def _get_order_lines(self, orders):
+ """Add pos_order_lines to the orders.
+
+ The function doesn't return anything but adds the results directly to the orders.
+
+ :param orders: orders for which the order_lines are to be requested.
+ :type orders: pos.order.
+ """
+ order_lines = self.env['pos.order.line'].search_read(
+ domain = [('order_id', 'in', [to['id'] for to in orders])],
+ fields = self._get_fields_for_order_line())
+
+ if order_lines != []:
+ self._get_pack_lot_lines(order_lines)
+
+ extended_order_lines = []
+ for order_line in order_lines:
+ order_line['product_id'] = order_line['product_id'][0]
+ order_line['server_id'] = order_line['id']
+
+ del order_line['id']
+ if not 'pack_lot_ids' in order_line:
+ order_line['pack_lot_ids'] = []
+ extended_order_lines.append([0, 0, order_line])
+
+ for order_id, order_lines in groupby(extended_order_lines, key=lambda x:x[2]['order_id']):
+ next(order for order in orders if order['id'] == order_id[0])['lines'] = list(order_lines)
+
+ def _get_fields_for_payment_lines(self):
+ return [
+ 'id',
+ 'amount',
+ 'pos_order_id',
+ 'payment_method_id',
+ 'card_type',
+ 'cardholder_name',
+ 'transaction_id',
+ 'payment_status'
+ ]
+
+ def _get_payment_lines(self, orders):
+ """Add account_bank_statement_lines to the orders.
+
+ The function doesn't return anything but adds the results directly to the orders.
+
+ :param orders: orders for which the payment_lines are to be requested.
+ :type orders: pos.order.
+ """
+ payment_lines = self.env['pos.payment'].search_read(
+ domain = [('pos_order_id', 'in', [po['id'] for po in orders])],
+ fields = self._get_fields_for_payment_lines())
+
+ extended_payment_lines = []
+ for payment_line in payment_lines:
+ payment_line['server_id'] = payment_line['id']
+ payment_line['payment_method_id'] = payment_line['payment_method_id'][0]
+
+ del payment_line['id']
+ extended_payment_lines.append([0, 0, payment_line])
+ for order_id, payment_lines in groupby(extended_payment_lines, key=lambda x:x[2]['pos_order_id']):
+ next(order for order in orders if order['id'] == order_id[0])['statement_ids'] = list(payment_lines)
+
+ def _get_fields_for_draft_order(self):
+ return [
+ 'id',
+ 'pricelist_id',
+ 'partner_id',
+ 'sequence_number',
+ 'session_id',
+ 'pos_reference',
+ 'create_uid',
+ 'create_date',
+ 'customer_count',
+ 'fiscal_position_id',
+ 'table_id',
+ 'to_invoice',
+ 'multiprint_resume',
+ ]
+
+ @api.model
+ def get_table_draft_orders(self, table_id):
+ """Generate an object of all draft orders for the given table.
+
+ Generate and return an JSON object with all draft orders for the given table, to send to the
+ front end application.
+
+ :param table_id: Id of the selected table.
+ :type table_id: int.
+ :returns: list -- list of dict representing the table orders
+ """
+ table_orders = self.search_read(
+ domain = [('state', '=', 'draft'), ('table_id', '=', table_id)],
+ fields = self._get_fields_for_draft_order())
+
+ self._get_order_lines(table_orders)
+ self._get_payment_lines(table_orders)
+
+ for order in table_orders:
+ order['pos_session_id'] = order['session_id'][0]
+ order['uid'] = search(r"\d{5,}-\d{3,}-\d{4,}", order['pos_reference']).group(0)
+ order['name'] = order['pos_reference']
+ order['creation_date'] = order['create_date']
+ order['server_id'] = order['id']
+ if order['fiscal_position_id']:
+ order['fiscal_position_id'] = order['fiscal_position_id'][0]
+ if order['pricelist_id']:
+ order['pricelist_id'] = order['pricelist_id'][0]
+ if order['partner_id']:
+ order['partner_id'] = order['partner_id'][0]
+ if order['table_id']:
+ order['table_id'] = order['table_id'][0]
+
+ if not 'lines' in order:
+ order['lines'] = []
+ if not 'statement_ids' in order:
+ order['statement_ids'] = []
+
+ del order['id']
+ del order['session_id']
+ del order['pos_reference']
+ del order['create_date']
+
+ return table_orders
+
+ def set_tip(self, tip_line_vals):
+ """Set tip to `self` based on values in `tip_line_vals`."""
+
+ self.ensure_one()
+ PosOrderLine = self.env['pos.order.line']
+ process_line = partial(PosOrderLine._order_line_fields, session_id=self.session_id.id)
+
+ # 1. add/modify tip orderline
+ processed_tip_line_vals = process_line([0, 0, tip_line_vals])[2]
+ processed_tip_line_vals.update({ "order_id": self.id })
+ tip_line = self.lines.filtered(lambda line: line.product_id == self.session_id.config_id.tip_product_id)
+ if not tip_line:
+ tip_line = PosOrderLine.create(processed_tip_line_vals)
+ else:
+ tip_line.write(processed_tip_line_vals)
+
+ # 2. modify payment
+ payment_line = self.payment_ids.filtered(lambda line: not line.is_change)[0]
+ # TODO it would be better to throw error if there are multiple payment lines
+ # then ask the user to select which payment to update, no?
+ payment_line._update_payment_line_for_tip(tip_line.price_subtotal_incl)
+
+ # 3. flag order as tipped and update order fields
+ self.write({
+ "is_tipped": True,
+ "tip_amount": tip_line.price_subtotal_incl,
+ "amount_total": self.amount_total + tip_line.price_subtotal_incl,
+ "amount_paid": self.amount_paid + tip_line.price_subtotal_incl,
+ })
+
+ def set_no_tip(self):
+ """Override this method to introduce action when setting no tip."""
+ self.ensure_one()
+ self.write({
+ "is_tipped": True,
+ "tip_amount": 0,
+ })
+
+ @api.model
+ def _order_fields(self, ui_order):
+ order_fields = super(PosOrder, self)._order_fields(ui_order)
+ order_fields['table_id'] = ui_order.get('table_id', False)
+ order_fields['customer_count'] = ui_order.get('customer_count', 0)
+ order_fields['multiprint_resume'] = ui_order.get('multiprint_resume', False)
+ return order_fields
+
+ def _export_for_ui(self, order):
+ result = super(PosOrder, self)._export_for_ui(order)
+ result['table_id'] = order.table_id.id
+ return result
diff --git a/addons/pos_restaurant/models/pos_payment.py b/addons/pos_restaurant/models/pos_payment.py
new file mode 100644
index 00000000..d4bfae15
--- /dev/null
+++ b/addons/pos_restaurant/models/pos_payment.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class PosConfig(models.Model):
+ _inherit = 'pos.payment'
+
+ def _update_payment_line_for_tip(self, tip_amount):
+ """Inherit this method to perform reauthorization or capture on electronic payment."""
+ self.ensure_one()
+ self.write({
+ "amount": self.amount + tip_amount,
+ })
diff --git a/addons/pos_restaurant/models/pos_restaurant.py b/addons/pos_restaurant/models/pos_restaurant.py
new file mode 100644
index 00000000..50a60eb7
--- /dev/null
+++ b/addons/pos_restaurant/models/pos_restaurant.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+
+class RestaurantFloor(models.Model):
+
+ _name = 'restaurant.floor'
+ _description = 'Restaurant Floor'
+
+ name = fields.Char('Floor Name', required=True, help='An internal identification of the restaurant floor')
+ pos_config_id = fields.Many2one('pos.config', string='Point of Sale')
+ background_image = fields.Binary('Background Image', help='A background image used to display a floor layout in the point of sale interface')
+ background_color = fields.Char('Background Color', help='The background color of the floor layout, (must be specified in a html-compatible format)', default='rgb(210, 210, 210)')
+ table_ids = fields.One2many('restaurant.table', 'floor_id', string='Tables', help='The list of tables in this floor')
+ sequence = fields.Integer('Sequence', help='Used to sort Floors', default=1)
+ active = fields.Boolean(default=True)
+
+ def unlink(self):
+ confs = self.mapped('pos_config_id').filtered(lambda c: c.is_table_management == True)
+ opened_session = self.env['pos.session'].search([('config_id', 'in', confs.ids), ('state', '!=', 'closed')])
+ if opened_session:
+ error_msg = _("You cannot remove a floor that is used in a PoS session, close the session(s) first: \n")
+ for floor in self:
+ for session in opened_session:
+ if floor in session.config_id.floor_ids:
+ error_msg += _("Floor: %s - PoS Config: %s \n") % (floor.name, session.config_id.name)
+ if confs:
+ raise UserError(error_msg)
+ return super(RestaurantFloor, self).unlink()
+
+ def write(self, vals):
+ for floor in self:
+ if floor.pos_config_id.has_active_session and (vals.get('pos_config_id') or vals.get('active')) :
+ raise UserError(
+ 'Please close and validate the following open PoS Session before modifying this floor.\n'
+ 'Open session: %s' % (' '.join(floor.pos_config_id.mapped('name')),))
+ if vals.get('pos_config_id') and floor.pos_config_id.id and vals.get('pos_config_id') != floor.pos_config_id.id:
+ raise UserError('The %s is already used in another Pos Config.' % floor.name)
+ return super(RestaurantFloor, self).write(vals)
+
+
+class RestaurantTable(models.Model):
+
+ _name = 'restaurant.table'
+ _description = 'Restaurant Table'
+
+ name = fields.Char('Table Name', required=True, help='An internal identification of a table')
+ floor_id = fields.Many2one('restaurant.floor', string='Floor')
+ shape = fields.Selection([('square', 'Square'), ('round', 'Round')], string='Shape', required=True, default='square')
+ position_h = fields.Float('Horizontal Position', default=10,
+ help="The table's horizontal position from the left side to the table's center, in pixels")
+ position_v = fields.Float('Vertical Position', default=10,
+ help="The table's vertical position from the top to the table's center, in pixels")
+ width = fields.Float('Width', default=50, help="The table's width in pixels")
+ height = fields.Float('Height', default=50, help="The table's height in pixels")
+ seats = fields.Integer('Seats', default=1, help="The default number of customer served at this table.")
+ color = fields.Char('Color', help="The table's color, expressed as a valid 'background' CSS property value")
+ active = fields.Boolean('Active', default=True, help='If false, the table is deactivated and will not be available in the point of sale')
+
+ @api.model
+ def create_from_ui(self, table):
+ """ create or modify a table from the point of sale UI.
+ table contains the table's fields. If it contains an
+ id, it will modify the existing table. It then
+ returns the id of the table.
+ """
+ if table.get('floor_id'):
+ table['floor_id'] = table['floor_id'][0]
+
+ table_id = table.pop('id', False)
+ if table_id:
+ self.browse(table_id).write(table)
+ else:
+ table_id = self.create(table).id
+ return table_id
+
+ def unlink(self):
+ confs = self.mapped('floor_id').mapped('pos_config_id').filtered(lambda c: c.is_table_management == True)
+ opened_session = self.env['pos.session'].search([('config_id', 'in', confs.ids), ('state', '!=', 'closed')])
+ if opened_session:
+ error_msg = _("You cannot remove a table that is used in a PoS session, close the session(s) first.")
+ if confs:
+ raise UserError(error_msg)
+ return super(RestaurantTable, self).unlink()
+
+
+class RestaurantPrinter(models.Model):
+
+ _name = 'restaurant.printer'
+ _description = 'Restaurant Printer'
+
+ name = fields.Char('Printer Name', required=True, default='Printer', help='An internal identification of the printer')
+ printer_type = fields.Selection(string='Printer Type', default='iot',
+ selection=[('iot', ' Use a printer connected to the IoT Box')])
+ proxy_ip = fields.Char('Proxy IP Address', help="The IP Address or hostname of the Printer's hardware proxy")
+ product_categories_ids = fields.Many2many('pos.category', 'printer_category_rel', 'printer_id', 'category_id', string='Printed Product Categories')