mirror of
https://github.com/bringout/oca-ocb-pos.git
synced 2026-04-24 03:02:02 +02:00
19.0 vanilla
This commit is contained in:
parent
6e54c1af6c
commit
3ca647e428
1087 changed files with 132065 additions and 108499 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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).'))
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
Loading…
Add table
Add a link
Reference in a new issue