diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_restaurant/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/models')
| -rw-r--r-- | addons/pos_restaurant/models/__init__.py | 7 | ||||
| -rw-r--r-- | addons/pos_restaurant/models/pos_config.py | 64 | ||||
| -rw-r--r-- | addons/pos_restaurant/models/pos_order.py | 237 | ||||
| -rw-r--r-- | addons/pos_restaurant/models/pos_payment.py | 15 | ||||
| -rw-r--r-- | addons/pos_restaurant/models/pos_restaurant.py | 99 |
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') |
