19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:29:53 +01:00
parent 6e54c1af6c
commit 3ca647e428
1087 changed files with 132065 additions and 108499 deletions

View file

@ -3,7 +3,10 @@
from . import pos_config
from . import pos_order
from . import pos_order_line
from . import pos_payment
from . import pos_restaurant
from . import pos_session
from . import res_config_settings
from . import pos_preset
from . import restaurant_order_course

View file

@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import convert
class PosConfig(models.Model):
@ -9,70 +10,170 @@ class PosConfig(models.Model):
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='Internal Notes', help='Allow custom internal 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')
floor_ids = fields.Many2many('restaurant.floor', string='Restaurant Floors', help='The restaurant floors served by this point of sale.', copy=False)
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)
def _force_http(self):
enforce_https = self.env['ir.config_parameter'].sudo().get_param('point_of_sale.enforce_https')
if not enforce_https and 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()
floors = self.env['restaurant.floor'].search([('pos_config_id', 'in', self.ids)])
tables = self.env['restaurant.table'].search([('floor_id', 'in', floors.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
default_screen = fields.Selection([('tables', 'Tables'), ('register', 'Register')], string='Default Screen', default='tables')
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
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
is_restaurant = 'module_pos_restaurant' in vals and vals['module_pos_restaurant']
if is_restaurant:
if 'iface_printbill' not in vals:
vals['iface_printbill'] = True
if 'show_product_images' not in vals:
vals['show_product_images'] = False
if 'show_category_images' not in vals:
vals['show_category_images'] = False
if not is_restaurant or not vals.get('iface_tipproduct', False):
vals['set_tip_after_payment'] = False
pos_configs = super().create(vals_list)
for config in pos_configs:
if config.module_pos_restaurant:
self._setup_default_floor(config)
return pos_configs
def write(self, vals):
if ('is_table_management' in vals and vals['is_table_management'] == False):
if ('module_pos_restaurant' in vals and vals['module_pos_restaurant'] is 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)
if ('module_pos_restaurant' in vals and not vals['module_pos_restaurant']) or ('iface_tipproduct' in vals and not vals['iface_tipproduct']):
vals['set_tip_after_payment'] = False
if ('module_pos_restaurant' in vals and vals['module_pos_restaurant']):
self._setup_default_floor(self)
return super().write(vals)
def _setup_default_floor(self, pos_config):
if not pos_config.floor_ids:
main_floor = self.env['restaurant.floor'].create({
'name': pos_config.company_id.name,
'pos_config_ids': [(4, pos_config.id)],
})
self.env['restaurant.table'].create({
'table_number': 1,
'floor_id': main_floor.id,
'seats': 1,
'position_h': 100,
'position_v': 100,
'width': 130,
'height': 130,
})
@api.model
def add_cash_payment_method(self):
companies = self.env['res.company'].search([])
for company in companies.filtered('chart_template_id'):
pos_configs = self.search([('company_id', '=', company.id), ('module_pos_restaurant', '=', True)])
journal_counter = 2
for pos_config in pos_configs:
if pos_config.payment_method_ids.filtered('is_cash_count'):
continue
cash_journal = self.env['account.journal'].search([('company_id', '=', company.id), ('type', '=', 'cash'), ('pos_payment_method_ids', '=', False)], limit=1)
if not cash_journal:
cash_journal = self.env['account.journal'].create({
'name': _('Cash %s', journal_counter),
'code': 'RCSH%s' % journal_counter,
'type': 'cash',
'company_id': company.id
})
journal_counter += 1
payment_methods = pos_config.payment_method_ids
payment_methods |= self.env['pos.payment.method'].create({
'name': _('Cash Bar'),
'journal_id': cash_journal.id,
'company_id': company.id,
})
pos_config.write({'payment_method_ids': [(6, 0, payment_methods.ids)]})
def load_onboarding_bar_scenario(self, with_demo_data=True):
journal, payment_methods_ids = self._create_journal_and_payment_methods(cash_journal_vals={'name': 'Cash Bar', 'show_on_dashboard': False})
config = self.env['pos.config'].create({
'name': 'Bar',
'company_id': self.env.company.id,
'journal_id': journal.id,
'payment_method_ids': payment_methods_ids,
'iface_splitbill': True,
'module_pos_restaurant': True,
'default_screen': 'register'
})
self.env['ir.model.data']._update_xmlids([{
'xml_id': self._get_suffixed_ref_name('pos_restaurant.pos_config_main_bar'),
'record': config,
'noupdate': True,
}])
if not self.env.ref('pos_restaurant.floor_main', raise_if_not_found=False):
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/restaurant_floor.xml', idref=None, mode='init', noupdate=True)
config_floors = [(5, 0)]
if (floor_main := self.env.ref('pos_restaurant.floor_main', raise_if_not_found=False)):
config_floors += [(4, floor_main.id)]
if (floor_patio := self.env.ref('pos_restaurant.floor_patio', raise_if_not_found=False)):
config_floors += [(4, floor_patio.id)]
config.update({'floor_ids': config_floors})
config._load_bar_demo_data(with_demo_data)
return {'config_id': config.id}
def _load_bar_demo_data(self, with_demo_data=True):
self.ensure_one()
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/bar_category_data.xml', idref=None, mode='init', noupdate=True)
if with_demo_data:
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/bar_demo_data.xml', idref=None, mode='init', noupdate=True)
bar_categories = self.get_record_by_ref([
'pos_restaurant.pos_category_cocktails',
'pos_restaurant.pos_category_soft_drinks',
])
if bar_categories:
self.limit_categories = True
self.iface_available_categ_ids = bar_categories
@api.model
def load_onboarding_restaurant_scenario(self, with_demo_data=True):
journal, payment_methods_ids = self._create_journal_and_payment_methods(cash_journal_vals={'name': _('Cash Restaurant'), 'show_on_dashboard': False})
presets = self.get_record_by_ref([
'pos_restaurant.pos_takein_preset',
'pos_restaurant.pos_takeout_preset',
'pos_restaurant.pos_delivery_preset',
]) + self.env['pos.preset'].search([]).ids
config = self.env['pos.config'].create({
'name': _('Restaurant'),
'company_id': self.env.company.id,
'journal_id': journal.id,
'payment_method_ids': payment_methods_ids,
'iface_splitbill': True,
'module_pos_restaurant': True,
'use_presets': bool(presets),
'default_preset_id': presets[0] if presets else False,
'available_preset_ids': [(6, 0, presets)],
})
self.env['ir.model.data']._update_xmlids([{
'xml_id': self._get_suffixed_ref_name('pos_restaurant.pos_config_main_restaurant'),
'record': config,
'noupdate': True,
}])
if bool(presets):
# Ensure the "Presets" menu is visible when installing the restaurant scenario
self.env.ref("point_of_sale.group_pos_preset").implied_by_ids |= self.env.ref("base.group_user")
if not self.env.ref('pos_restaurant.floor_main', raise_if_not_found=False):
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/restaurant_floor.xml', idref=None, mode='init', noupdate=True)
config_floors = [(5, 0)]
if (floor_main := self.env.ref('pos_restaurant.floor_main', raise_if_not_found=False)):
config_floors += [(4, floor_main.id)]
if (floor_patio := self.env.ref('pos_restaurant.floor_patio', raise_if_not_found=False)):
config_floors += [(4, floor_patio.id)]
config.update({'floor_ids': config_floors})
config._load_restaurant_demo_data(with_demo_data)
existing_session = self.env.ref('pos_restaurant.pos_closed_session_3', raise_if_not_found=False)
if with_demo_data and self.env.company.id == self.env.ref('base.main_company').id and not existing_session:
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/restaurant_demo_session.xml', idref=None, mode='init', noupdate=True)
return {'config_id': config.id}
@api.depends('set_tip_after_payment')
def _compute_local_data_integrity(self):
super()._compute_local_data_integrity()
def _load_restaurant_demo_data(self, with_demo_data=True):
self.ensure_one()
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/restaurant_category_data.xml', idref=None, mode='init', noupdate=True)
if with_demo_data:
convert.convert_file(self._env_with_clean_context(), 'pos_restaurant', 'data/scenarios/restaurant_demo_data.xml', idref=None, mode='init', noupdate=True)
restaurant_categories = self.get_record_by_ref([
'pos_restaurant.food',
'pos_restaurant.drinks',
])
if restaurant_categories:
self.limit_categories = True
self.iface_available_categ_ids = restaurant_categories
def _get_demo_data_loader_methods(self):
mapping = super()._get_demo_data_loader_methods()
mapping.update({
'pos_restaurant.pos_config_main_restaurant': self._load_restaurant_demo_data,
'pos_restaurant.pos_config_main_bar': self._load_bar_demo_data,
})
return mapping
def _get_default_demo_data_xml_id(self):
if self.module_pos_restaurant:
return 'pos_restaurant.pos_config_main_restaurant'
return super()._get_default_demo_data_xml_id()

View file

@ -1,287 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tools import groupby
from re import search
from functools import partial
import pytz
from odoo import api, fields, models
class PosOrderLine(models.Model):
_inherit = 'pos.order.line'
note = fields.Char('Internal Note added by the waiter.')
uuid = fields.Char(string='Uuid', readonly=True, copy=False)
mp_skip = fields.Boolean('Skip line when sending ticket to kitchen printers.')
class PosOrder(models.Model):
_inherit = 'pos.order'
table_id = fields.Many2one('restaurant.table', string='Table', help='The table where this order was served', index='btree_not_null')
customer_count = fields.Integer(string='Guests', help='The amount of customers that have been served by this order.')
multiprint_resume = fields.Char(string='Multiprint Resume', help="Last printed state of the order")
table_id = fields.Many2one('restaurant.table', string='Table', help='The table where this order was served', index='btree_not_null', readonly=True)
customer_count = fields.Integer(string='Guests', help='The amount of customers that have been served by this order.', readonly=True)
course_ids = fields.One2many('restaurant.order.course', 'order_id', string="Courses")
def _get_pack_lot_lines(self, order_lines):
"""Add pack_lot_lines to the order_lines.
def _get_open_order(self, order):
config_id = self.env['pos.session'].browse(order.get('session_id')).config_id
if not config_id.module_pos_restaurant:
return super()._get_open_order(order)
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_lot_ids)
def _get_fields_for_order_line(self):
fields = super(PosOrder, self)._get_fields_for_order_line()
fields.extend([
'id',
'discount',
'product_id',
'price_unit',
'order_id',
'qty',
'note',
'uuid',
'mp_skip',
'full_product_name',
'customer_note',
'price_extra',
])
return fields
def _prepare_order_line(self, order_line):
"""Method that will allow the cleaning of values to send the correct information.
:param order_line: order_line that will be cleaned.
:type order_line: pos.order.line.
:returns: dict -- dict representing the order line's values.
"""
order_line = super()._prepare_order_line(order_line)
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"] = []
domain = []
if order.get('table_id', False) and order.get('state') == 'draft':
domain += ['|', ('uuid', '=', order.get('uuid')), '&', ('table_id', '=', order.get('table_id')), ('state', '=', 'draft')]
else:
order_line["pack_lot_ids"] = [[0, 0, lot] for lot in order_line["pack_lot_ids"]]
return order_line
domain += [('uuid', '=', order.get('uuid'))]
return self.env["pos.order"].search(domain, limit=1, order='id desc')
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:
extended_order_lines.append([0, 0, self._prepare_order_line(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_payments_lines_list(self, orders):
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])
return extended_payment_lines
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.
"""
extended_payment_lines = self._get_payments_lines_list(orders)
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',
'access_token',
]
def _get_domain_for_draft_orders(self, table_ids):
""" Get the domain to search for draft orders on a table.
:param table_ids: Ids of the selected tables.
:type table_ids: list of int.
"returns: list -- list of tuples that represents a domain.
"""
return [('state', '=', 'draft'), ('table_id', 'in', table_ids)]
def _add_activated_coupon_to_draft_orders(self, table_orders):
table_orders = super()._add_activated_coupon_to_draft_orders(table_orders)
return table_orders
@api.model
def get_table_draft_orders(self, table_ids):
"""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_ids: Ids of the selected tables.
:type table_ids: list of int.
:returns: list -- list of dict representing the table orders
"""
table_orders = self.search_read(
domain=self._get_domain_for_draft_orders(table_ids),
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 self._add_activated_coupon_to_draft_orders(table_orders)
@api.model
def remove_from_ui(self, server_ids):
""" Remove orders from the frontend PoS application
Remove orders from the server by id.
:param server_ids: list of the id's of orders to remove from the server.
:type server_ids: list.
:returns: list -- list of db-ids for the removed orders.
"""
orders = self.search([('id', 'in', server_ids), ('state', '=', 'draft')])
orders.write({'state': 'cancel'})
# TODO Looks like delete cascade is a better solution.
orders.mapped('payment_ids').sudo().unlink()
orders.sudo().unlink()
return orders.ids
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
def read_pos_data(self, data, config):
result = super().read_pos_data(data, config)
result['restaurant.order.course'] = self.env['restaurant.order.course']._load_pos_data_read(self.course_ids, config)
return result

View file

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class PosOrderLine(models.Model):
_inherit = 'pos.order.line'
course_id = fields.Many2one('restaurant.order.course', string="Course Ref", ondelete="set null", index='btree_not_null')
@api.model
def _load_pos_data_fields(self, config):
result = super()._load_pos_data_fields(config)
return result + ["course_id"]

View file

@ -4,7 +4,7 @@
from odoo import api, fields, models
class PosConfig(models.Model):
class PosPayment(models.Model):
_inherit = 'pos.payment'
def _update_payment_line_for_tip(self, tip_amount):

View file

@ -0,0 +1,22 @@
from odoo import api, models, fields, _
from odoo.exceptions import UserError
class PosPreset(models.Model):
_inherit = 'pos.preset'
use_guest = fields.Boolean(string='Guest', default=False, help="Force guest selection when clicking on order button in PoS restaurant")
@api.model
def _load_pos_data_fields(self, config):
return super()._load_pos_data_fields(config) + ['use_guest']
@api.ondelete(at_uninstall=False)
def _unlink_except_master_presets(self):
master_presets = self.env["pos.config"].get_record_by_ref([
'pos_restaurant.pos_takein_preset',
'pos_restaurant.pos_takeout_preset',
'pos_restaurant.pos_delivery_preset',
])
if any(preset.id in master_presets for preset in self):
raise UserError(_('You cannot delete the master preset(s).'))

View file

@ -1,55 +1,100 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
class RestaurantFloor(models.Model):
_name = 'restaurant.floor'
_description = 'Restaurant Floor'
_order = "sequence, name"
_inherit = ['pos.load.mixin']
name = fields.Char('Floor Name', required=True)
pos_config_id = fields.Many2one('pos.config', string='Point of Sale')
pos_config_ids = fields.Many2many('pos.config', string='Point of Sales', domain="[('module_pos_restaurant', '=', True)]")
background_image = fields.Binary('Background Image')
background_color = fields.Char('Background Color', help='The background color of the floor in a html-compatible format', default='rgb(210, 210, 210)')
background_color = fields.Char('Background Color', help='The background color of the floor in a html-compatible format')
table_ids = fields.One2many('restaurant.table', 'floor_id', string='Tables')
sequence = fields.Integer('Sequence', default=1)
active = fields.Boolean(default=True)
floor_background_image = fields.Image(string='Floor Background Image')
@api.model
def _load_pos_data_domain(self, data, config):
return [('pos_config_ids', '=', config.id)]
@api.model
def _load_pos_data_fields(self, config):
return ['name', 'background_color', 'table_ids', 'sequence', 'pos_config_ids', 'floor_background_image', 'active']
@api.ondelete(at_uninstall=False)
def _unlink_except_active_pos_session(self):
confs = self.mapped('pos_config_id').filtered(lambda c: c.is_table_management == True)
confs = self.mapped('pos_config_ids').filtered(lambda c: c.module_pos_restaurant)
opened_session = self.env['pos.session'].search([('config_id', 'in', confs.ids), ('state', '!=', 'closed')])
if opened_session:
if opened_session and confs:
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)
error_msg += _("Floor: %(floor)s - PoS Config: %(config)s \n", floor=floor.name, config=session.config_id.name)
raise UserError(error_msg)
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)
for config in floor.pos_config_ids:
if config.has_active_session and (vals.get('pos_config_ids') or vals.get('active')):
raise UserError(
self.env._(
"Please close and validate the following open PoS Session before modifying this floor.\n"
"Open session: %(session_names)s",
session_names=" ".join(config.mapped("name")),
)
)
return super().write(vals)
def rename_floor(self, new_name):
for floor in self:
floor.name = new_name
@api.model
def sync_from_ui(self, name, background_color, config_id):
floor_fields = {
"name": name,
"background_color": background_color,
}
pos_floor = self.create(floor_fields)
pos_floor.pos_config_ids = [Command.link(config_id)]
return {
'id': pos_floor.id,
'name': pos_floor.name,
'background_color': pos_floor.background_color,
'table_ids': [],
'sequence': pos_floor.sequence,
'tables': [],
}
def deactivate_floor(self, session_id):
draft_orders = self.env['pos.order'].search([('session_id', '=', session_id), ('state', '=', 'draft'), ('table_id.floor_id', '=', self.id)])
if draft_orders:
raise UserError(_("You cannot delete a floor when orders are still in draft for this floor."))
for table in self.table_ids:
table.active = False
self.active = False
return True
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')
_description = 'Restaurant Table'
_inherit = ['pos.load.mixin']
floor_id = fields.Many2one('restaurant.floor', string='Floor', index='btree_not_null')
table_number = fields.Integer('Table Number', required=True, help='The number of the table as displayed on the floor plan', default=0)
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")
@ -59,43 +104,35 @@ class RestaurantTable(models.Model):
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")
parent_id = fields.Many2one('restaurant.table', string='Parent Table', help="The parent table if this table is part of a group of tables")
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]
@api.depends('table_number', 'floor_id')
def _compute_display_name(self):
for table in self:
table.display_name = f"{table.floor_id.name}, {table.table_number}"
sanitized_table = dict([(key, val) for key, val in table.items() if key in self._fields and val is not None])
table_id = sanitized_table.pop('id', False)
if table_id:
self.browse(table_id).write(sanitized_table)
else:
table_id = self.create(sanitized_table).id
return table_id
@api.model
def _load_pos_data_domain(self, data, config):
return [('active', '=', True), ('floor_id', 'in', config.floor_ids.ids)]
@api.model
def _load_pos_data_fields(self, config):
return ['table_number', 'width', 'height', 'position_h', 'position_v', 'parent_id', 'shape', 'floor_id', 'color', 'seats', 'active']
def are_orders_still_in_draft(self):
draft_orders_count = self.env['pos.order'].search_count([('table_id', 'in', self.ids), ('state', '=', 'draft')])
if draft_orders_count > 0:
raise UserError(_("You cannot delete a table when orders are still in draft for this table."))
return True
@api.ondelete(at_uninstall=False)
def _unlink_except_active_pos_session(self):
confs = self.mapped('floor_id').mapped('pos_config_id').filtered(lambda c: c.is_table_management == True)
confs = self.mapped('floor_id.pos_config_ids').filtered(lambda c: c.module_pos_restaurant)
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)
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')

View file

@ -1,64 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from itertools import groupby
from odoo.osv.expression import AND
from odoo import models, api
import json
class PosSession(models.Model):
_inherit = 'pos.session'
def _pos_ui_models_to_load(self):
result = super()._pos_ui_models_to_load()
@api.model
def _load_pos_data_models(self, config):
data = super()._load_pos_data_models(config)
if self.config_id.module_pos_restaurant:
result.append('restaurant.printer')
if self.config_id.is_table_management:
result.append('restaurant.floor')
return result
data += ['restaurant.floor', 'restaurant.table', 'restaurant.order.course']
return data
def _loader_params_restaurant_floor(self):
return {
'search_params': {
'domain': [('pos_config_id', '=', self.config_id.id)],
'fields': ['name', 'background_color', 'table_ids', 'sequence'],
'order': 'sequence',
},
}
def _loader_params_restaurant_table(self):
return {
'search_params': {
'domain': [('active', '=', True)],
'fields': [
'name', 'width', 'height', 'position_h', 'position_v',
'shape', 'floor_id', 'color', 'seats', 'active'
],
},
}
def _get_pos_ui_restaurant_floor(self, params):
floors = self.env['restaurant.floor'].search_read(**params['search_params'])
floor_ids = [floor['id'] for floor in floors]
table_params = self._loader_params_restaurant_table()
table_params['search_params']['domain'] = AND([table_params['search_params']['domain'], [('floor_id', 'in', floor_ids)]])
tables = self.env['restaurant.table'].search(table_params['search_params']['domain'], order='floor_id')
tables_by_floor_id = {}
for floor_id, table_group in groupby(tables, key=lambda table: table.floor_id):
floor_tables = self.env['restaurant.table'].concat(*table_group)
tables_by_floor_id[floor_id.id] = floor_tables.read(table_params['search_params']['fields'])
for floor in floors:
floor['tables'] = tables_by_floor_id.get(floor['id'], [])
return floors
def _loader_params_restaurant_printer(self):
return {
'search_params': {
'domain': [('id', 'in', self.config_id.printer_ids.ids)],
'fields': ['name', 'proxy_ip', 'product_categories_ids', 'printer_type'],
},
}
def _get_pos_ui_restaurant_printer(self, params):
return self.env['restaurant.printer'].search_read(**params['search_params'])
@api.model
def _set_last_order_preparation_change(self, order_ids):
for order_id in order_ids:
order = self.env['pos.order'].browse(order_id)
last_order_preparation_change = {
'lines': {},
'generalCustomerNote': '',
}
for orderline in order['lines']:
last_order_preparation_change['lines'][orderline.uuid + " - "] = {
"uuid": orderline.uuid,
"name": orderline.full_product_name,
"note": "",
"product_id": orderline.product_id.id,
"quantity": orderline.qty,
"attribute_value_ids": orderline.attribute_value_ids.ids,
}
order.write({'last_order_preparation_change': json.dumps(last_order_preparation_change)})

View file

@ -6,36 +6,24 @@ from odoo import fields, models, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
def _get_floors_domain(self):
return ['|', ('pos_config_id', 'in', self.pos_config_id.ids), ('pos_config_id', '=', False)]
pos_floor_ids = fields.One2many(related='pos_config_id.floor_ids', readonly=False, domain=lambda self: self._get_floors_domain())
pos_iface_orderline_notes = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
pos_floor_ids = fields.Many2many(related='pos_config_id.floor_ids', readonly=False)
pos_iface_printbill = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
pos_iface_splitbill = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
pos_is_order_printer = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
pos_is_table_management = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
pos_printer_ids = fields.Many2many(related='pos_config_id.printer_ids', readonly=False)
pos_set_tip_after_payment = fields.Boolean(compute='_compute_pos_set_tip_after_payment', store=True, readonly=False)
pos_default_screen = fields.Selection(related="pos_config_id.default_screen", readonly=False)
@api.depends('pos_module_pos_restaurant', 'pos_config_id')
def _compute_pos_module_pos_restaurant(self):
for res_config in self:
if not res_config.pos_module_pos_restaurant:
res_config.update({
'pos_iface_orderline_notes': False,
'pos_iface_printbill': False,
'pos_iface_splitbill': False,
'pos_is_order_printer': False,
'pos_is_table_management': False,
})
else:
res_config.update({
'pos_iface_orderline_notes': res_config.pos_config_id.iface_orderline_notes,
'pos_iface_printbill': res_config.pos_config_id.iface_printbill,
'pos_iface_splitbill': res_config.pos_config_id.iface_splitbill,
'pos_is_order_printer': res_config.pos_config_id.is_order_printer,
'pos_is_table_management': res_config.pos_config_id.is_table_management,
})
@api.depends('pos_iface_tipproduct', 'pos_config_id')

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from uuid import uuid4
class RestaurantOrderCourse(models.Model):
_name = 'restaurant.order.course'
_description = 'POS Restaurant Order Course'
_inherit = ['pos.load.mixin']
fired = fields.Boolean(string="Fired", default=False)
fired_date = fields.Datetime(string="Fired Date")
uuid = fields.Char(string='Uuid', readonly=True, default=lambda self: str(uuid4()), copy=False)
index = fields.Integer(string="Course index", default=0)
order_id = fields.Many2one('pos.order', string='Order Ref', required=True, index=True, ondelete='cascade')
line_ids = fields.One2many('pos.order.line', 'course_id', string="Order Lines", readonly=True)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('fired') and not vals.get('fired_date'):
vals['fired_date'] = fields.Datetime.now()
return super().create(vals_list)
def write(self, vals):
if vals.get('fired') and not self.fired_date:
vals['fired_date'] = fields.Datetime.now()
return super().write(vals)
@api.model
def _load_pos_data_domain(self, data, config):
return [('order_id', 'in', [order['id'] for order in data['pos.order']])]
@api.model
def _load_pos_data_fields(self, config):
return ['uuid', 'fired', 'order_id', 'line_ids', 'index', 'write_date']