oca-ocb-sale/odoo-bringout-oca-ocb-point_of_sale/point_of_sale/models/pos_config.py
Ernad Husremovic 73afc09215 19.0 vanilla
2026-03-09 09:32:12 +01:00

1258 lines
64 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from uuid import uuid4
import pytz
from collections import defaultdict
from odoo import api, fields, models, _, Command, tools, SUPERUSER_ID
from odoo.http import request
from odoo.exceptions import AccessError, ValidationError, UserError
from odoo.tools import SQL, convert
from odoo.service.common import exp_version
from odoo.addons.point_of_sale.models.pos_printer import format_epson_certified_domain
DEFAULT_LIMIT_LOAD_PRODUCT = 5000
DEFAULT_LIMIT_LOAD_PARTNER = 100
class PosConfig(models.Model):
_name = 'pos.config'
_inherit = ['pos.bus.mixin', 'pos.load.mixin']
_description = 'Point of Sale Configuration'
_check_company_auto = True
def _default_warehouse_id(self):
return self.env['stock.warehouse'].search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1).id
def _default_picking_type_id(self):
return self.env['stock.warehouse'].with_context(active_test=False).search(self.env['stock.warehouse']._check_company_domain(self.env.company), limit=1).pos_type_id.id
def _default_sale_journal(self):
journal = self.env['account.journal']._ensure_company_account_journal()
return journal
def _default_invoice_journal(self):
return self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(self.env.company),
('type', '=', 'sale'),
], limit=1)
def _default_payment_methods(self):
""" Should only default to payment methods that are compatible to this config's company and currency.
"""
domain = [
*self.env['pos.payment.method']._check_company_domain(self.env.company),
('split_transactions', '=', False),
'|',
('journal_id', '=', False),
('journal_id.currency_id', 'in', (False, self.env.company.currency_id.id)),
]
non_cash_pm = self.env['pos.payment.method'].search(domain + [('is_cash_count', '=', False)])
available_cash_pm = self.env['pos.payment.method'].search(domain + [('is_cash_count', '=', True),
('config_ids', '=', False)], limit=1)
if not (non_cash_pm or available_cash_pm):
_dummy, payment_methods = self._create_journal_and_payment_methods()
return self.env['pos.payment.method'].browse(payment_methods)
return non_cash_pm | available_cash_pm
def _get_group_pos_manager(self):
return self.env.ref('point_of_sale.group_pos_manager')
def _get_group_pos_user(self):
return self.env.ref('point_of_sale.group_pos_user')
def _get_default_tip_product(self):
tip_product_id = self.env.ref("point_of_sale.product_product_tip", raise_if_not_found=False)
if not tip_product_id or (tip_product_id.sudo().company_id and tip_product_id.sudo().company_id != self.env.company):
tip_product_id = self.env['product.product'].search([('default_code', '=', 'TIPS')], limit=1)
return tip_product_id
name = fields.Char(string='Point of Sale', required=True, help="An internal identification of the point of sale.")
printer_ids = fields.Many2many('pos.printer', 'pos_config_printer_rel', 'config_id', 'printer_id', string='Order Printers')
is_order_printer = fields.Boolean('Order Printer')
is_installed_account_accountant = fields.Boolean(string="Is the Full Accounting Installed",
compute="_compute_is_installed_account_accountant")
picking_type_id = fields.Many2one(
'stock.picking.type',
string='Operation Type',
default=_default_picking_type_id,
required=True,
domain=lambda self: [('code', '=', 'outgoing'), ('warehouse_id.company_id', '=', self.env.company.id)],
ondelete='restrict')
journal_id = fields.Many2one(
'account.journal', string='Point of Sale Journal',
domain=[('type', 'in', ('general', 'sale'))],
check_company=True,
help="Accounting journal used to post POS session journal entries and POS invoice payments.",
default=_default_sale_journal,
ondelete='restrict')
invoice_journal_id = fields.Many2one(
'account.journal', string='Invoice Journal',
check_company=True,
domain=[('type', '=', 'sale')],
help="Accounting journal used to create invoices.",
default=_default_invoice_journal)
currency_id = fields.Many2one('res.currency', compute='_compute_currency', store=True, compute_sudo=True, string="Currency")
order_seq_id = fields.Many2one('ir.sequence', string='Order Sequence', readonly=True, copy=False)
order_backend_seq_id = fields.Many2one('ir.sequence', string='Order Backend Sequence', readonly=True, copy=False)
order_line_seq_id = fields.Many2one('ir.sequence', string='Order Line Sequence', readonly=True, copy=False)
device_seq_id = fields.Many2one('ir.sequence', string='Device Sequence', readonly=True, copy=False)
iface_cashdrawer = fields.Boolean(string='Cashdrawer', help="Automatically open the cashdrawer.")
iface_electronic_scale = fields.Boolean(string='Electronic Scale', help="Enables Electronic Scale integration.")
iface_print_via_proxy = fields.Boolean(string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.")
iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner and card swiping with a Vantiv card reader.")
iface_big_scrollbars = fields.Boolean('Large Scrollbars', help='For imprecise industrial touchscreens.')
iface_group_by_categ = fields.Boolean("Group products by categories", help='Display products grouped by categories.')
iface_print_auto = fields.Boolean(string='Automatic Receipt Printing', default=False,
help='The receipt will automatically be printed at the end of each order.')
iface_print_skip_screen = fields.Boolean(string='Skip Preview Screen', default=True,
help='The receipt screen will be skipped if the receipt can be printed automatically.')
iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Price'), ('total', 'Tax-Included Price')], string="Tax Display", default='total', required=True)
iface_available_categ_ids = fields.Many2many('pos.category', string='Available PoS Product Categories',
help='The point of sale will only display products which are within one of the selected category trees. If no category is specified, all available products will be shown')
customer_display_bg_img = fields.Image(string='Background Image', max_width=1920, max_height=1920)
customer_display_bg_img_name = fields.Char(string='Background Image Name')
restrict_price_control = fields.Boolean(string='Restrict Price Modifications to Managers',
help="Only users with Manager access rights for PoS app can modify the product prices on orders.")
is_margins_costs_accessible_to_every_user = fields.Boolean(string='Margins & Costs', default=False,
help='When disabled, only PoS manager can view the margin and cost of product among the Product info.')
cash_control = fields.Boolean(string='Advanced Cash Control', compute='_compute_cash_control', help="Check the amount of the cashbox at opening and closing.")
set_maximum_difference = fields.Boolean('Set Maximum Difference', help="Set a maximum difference allowed between the expected and counted money during the closing of the session.")
receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt.")
receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt.")
basic_receipt = fields.Boolean(string='Basic Receipt', help="Print basic ticket without prices. Can be used for gifts.")
proxy_ip = fields.Char(string='IP Address', size=45,
help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty.')
active = fields.Boolean(default=True)
uuid = fields.Char(readonly=True, default=lambda self: str(uuid4()), copy=False,
help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.')
session_ids = fields.One2many('pos.session', 'config_id', string='Sessions')
current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session")
current_session_state = fields.Char(compute='_compute_current_session')
number_of_rescue_session = fields.Integer(string="Number of Rescue Session", compute='_compute_current_session')
last_session_closing_cash = fields.Float(compute='_compute_last_session')
last_session_closing_date = fields.Date(compute='_compute_last_session')
pos_session_username = fields.Char(compute='_compute_current_session_user')
pos_session_state = fields.Char(compute='_compute_current_session_user')
pos_session_duration = fields.Char(compute='_compute_current_session_user')
pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist',
help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured if any.")
available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists',
help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.")
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company)
group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager,
help='This field is there to pass the id of the pos manager group to the point of sale client.')
group_pos_user_id = fields.Many2one('res.groups', string='Point of Sale User Group', default=_get_group_pos_user,
help='This field is there to pass the id of the pos user group to the point of sale client.')
iface_tipproduct = fields.Boolean(string="Product tips")
tip_product_id = fields.Many2one('product.product', string='Tip Product', default=_get_default_tip_product, help="This product is used as reference on customer receipts.")
fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.')
default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position')
default_bill_ids = fields.Many2many('pos.bill', string="Coins/Bills")
use_pricelist = fields.Boolean("Use a pricelist.")
use_presets = fields.Boolean("Use Presets")
default_preset_id = fields.Many2one('pos.preset', string='Default Preset')
available_preset_ids = fields.Many2many('pos.preset', string='Available Presets')
tax_regime_selection = fields.Boolean("Tax Regime Selection value")
limit_categories = fields.Boolean("Restrict Categories")
module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant")
module_pos_avatax = fields.Boolean("AvaTax PoS Integration", help="Use automatic taxes mapping with Avatax in PoS")
module_pos_discount = fields.Boolean("Global Discounts")
module_pos_appointment = fields.Boolean("Online Booking")
is_posbox = fields.Boolean("PosBox")
is_header_or_footer = fields.Boolean("Custom Header & Footer")
module_pos_hr = fields.Boolean(help="Show employee login screen")
amount_authorized_diff = fields.Float('Amount Authorized Difference',
help="This field depicts the maximum difference allowed between the ending balance and the theoretical cash when "
"closing a session, for non-POS managers. If this maximum is reached, the user will have an error message at "
"the closing of his session saying that he needs to contact his manager.")
payment_method_ids = fields.Many2many('pos.payment.method', string='Payment Methods', default=lambda self: self._default_payment_methods(), copy=False)
company_has_template = fields.Boolean(string="Company has chart of accounts", compute="_compute_company_has_template")
current_user_id = fields.Many2one('res.users', string='Current Session Responsible', compute='_compute_current_session_user')
other_devices = fields.Boolean(string="Other Devices", help="Connect devices to your PoS without an IoT Box.")
rounding_method = fields.Many2one('account.cash.rounding', string="Cash rounding")
cash_rounding = fields.Boolean(string="Cash Rounding")
only_round_cash_method = fields.Boolean(string="Only apply rounding on cash")
has_active_session = fields.Boolean(compute='_compute_current_session')
manual_discount = fields.Boolean(string="Line Discounts", default=True)
ship_later = fields.Boolean(string="Ship Later")
warehouse_id = fields.Many2one('stock.warehouse', default=_default_warehouse_id, ondelete='restrict')
route_id = fields.Many2one('stock.route', string="Spefic route for products delivered later.")
picking_policy = fields.Selection([
('direct', 'As soon as possible'),
('one', 'When all products are ready')],
string='Shipping Policy', required=True, default='direct',
help="If you deliver all products at once, the delivery order will be scheduled based on the greatest "
"product lead time. Otherwise, it will be based on the shortest.")
auto_validate_terminal_payment = fields.Boolean(default=True, help="Automatically validates orders paid with a payment terminal.")
trusted_config_ids = fields.Many2many("pos.config", relation="pos_config_trust_relation", column1="is_trusting",
column2="is_trusted", string="Trusted Point of Sale Configurations",
domain="[('company_id', '=', company_id)]")
access_token = fields.Char("Access Token", default=lambda self: uuid4().hex[:16])
show_product_images = fields.Boolean(string="Show Product Images", help="Show product images in the Point of Sale interface.", default=True)
show_category_images = fields.Boolean(string="Show Category Images", help="Show category images in the Point of Sale interface.", default=True)
note_ids = fields.Many2many('pos.note', string='Note Models', help='The predefined notes of this point of sale.')
module_pos_sms = fields.Boolean(string="SMS Enabled", help="Activate SMS feature for point_of_sale")
is_closing_entry_by_product = fields.Boolean(
string='Closing Entry by product',
help="Display the breakdown of sales lines by product in the automatically generated closing entry.")
order_edit_tracking = fields.Boolean(string="Track orders edits", help="Store edited orders in the backend", default=False)
last_data_change = fields.Datetime(string='Last Write Date', readonly=True, compute='_compute_local_data_integrity', store=True)
fallback_nomenclature_id = fields.Many2one('barcode.nomenclature', string="Fallback Nomenclature")
epson_printer_ip = fields.Char(
string='Epson Printer IP',
help=(
"Local IP address of an Epson receipt printer, or its serial number if the "
"'Automatic Certificate Update' option is enabled in the printer settings."
),
)
use_fast_payment = fields.Boolean('Fast Payment Validation', help="Enable fast payment methods to validate orders on the product screen.")
fast_payment_method_ids = fields.Many2many(
'pos.payment.method', string='Fast Payment Methods', compute="_compute_fast_payment_method_ids", relation='pos_payment_method_config_fast_validation_relation',
store=True, help="These payment methods will be available for fast payment", readonly=False)
statistics_for_current_session = fields.Json(string="Session Statistics", compute="_compute_statistics_for_session")
def _get_next_order_refs(self, device_identifier='0'):
next_number = self.order_backend_seq_id._next()
year_2_digits = str(datetime.now().year)[-2:]
tracking_number = f"{int(next_number) % 1000}"
return f"{year_2_digits}{device_identifier}-{self.id}-{next_number}", tracking_number
def notify_synchronisation(self, session_id, device_identifier, records={}):
self.ensure_one()
static_records = {}
for model, ids in records.items():
records = self.env[model].browse(ids).exists()
static_records[model] = self.env[model]._load_pos_data_read(records, self)
self._notify('SYNCHRONISATION', {
'static_records': static_records,
'session_id': session_id,
'device_identifier': device_identifier,
'records': records
})
for config in self.trusted_config_ids:
config._notify('SYNCHRONISATION', {
'static_records': static_records,
'session_id': config.current_session_id.id,
'login_number': 0,
'records': records
})
def read_config_open_orders(self, domain, record_ids=[]):
delete_record_ids = {}
dynamic_records = {}
for model, dom in domain.items():
ids = record_ids.get(model, [])
browsed = self.env[model].browse(ids)
dynamic_records[model] = self.env[model].search(dom)
delete_record_ids[model] = browsed.filtered(lambda r: not r.exists()).ids
# Cancelled orders must be forced deleted from the user interface.
if model == "pos.order":
delete_record_ids[model] += browsed.exists().filtered(lambda r: r.state == "cancel").ids
pos_order_data = dynamic_records.get('pos.order') or self.env['pos.order']
data = pos_order_data.read_pos_data([], self)
for key, records in dynamic_records.items():
fields = self.env[key]._load_pos_data_fields(self)
ids = list(set(records.ids + [record['id'] for record in data.get(key, [])]))
dynamic_records[key] = self.env[key].browse(ids).read(fields, load=False)
for key, value in data.items():
if key not in dynamic_records:
dynamic_records[key] = value
return {
'dynamic_records': dynamic_records,
'deleted_record_ids': delete_record_ids,
}
@api.model
def _load_pos_data_domain(self, data, config):
return [('id', '=', config.id)]
@api.model
def _load_pos_data_read(self, records, config):
read_records = super()._load_pos_data_read(records, config)
if not read_records:
return read_records
record = read_records[0]
record['_server_version'] = exp_version()
record['_base_url'] = config.get_base_url()
record['_data_server_date'] = self.env.context.get('pos_last_server_date') or self.env.cr.now()
record['_has_cash_move_perm'] = self.env.user.has_group('account.group_account_invoice')
record['_has_cash_delete_perm'] = self.env.user.has_group('account.group_account_basic')
record['_pos_special_products_ids'] = self.env['pos.config']._get_special_products().ids
# Add custom fields for 'formula' taxes.
# We can ignore data for _load_pos_data_domain since isn't needed in the domain computation of account.tax
taxes = self.env['account.tax'].search(self.env['account.tax']._load_pos_data_domain({}, config))
product_fields = taxes._eval_taxes_computation_prepare_product_fields()
record['_product_default_values'] = \
self.env['account.tax']._eval_taxes_computation_prepare_product_default_values(product_fields)
if not record['use_pricelist']:
record['pricelist_id'] = False
record['_IS_VAT'] = self.env.company.country_id.id in self.env.ref("base.europe").country_ids.ids
return read_records
@api.depends('payment_method_ids')
def _compute_fast_payment_method_ids(self):
for config in self:
config.fast_payment_method_ids = config.fast_payment_method_ids.filtered(lambda pm: pm.id in config.payment_method_ids.ids)
if not config.fast_payment_method_ids:
config.use_fast_payment = False
@api.depends('payment_method_ids')
def _compute_cash_control(self):
for config in self:
config.cash_control = bool(config.payment_method_ids.filtered('is_cash_count'))
@api.depends('company_id')
def _compute_company_has_template(self):
for config in self:
config.company_has_template = config.company_id.root_id.sudo()._existing_accounting() or config.company_id.chart_template
def _compute_is_installed_account_accountant(self):
account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')])
for pos_config in self:
pos_config.is_installed_account_accountant = account_accountant and account_accountant.id
@api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id', 'company_id', 'company_id.currency_id')
def _compute_currency(self):
for pos_config in self:
if pos_config.journal_id:
pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.sudo().currency_id.id
else:
pos_config.currency_id = pos_config.company_id.sudo().currency_id.id
@api.depends('session_ids', 'session_ids.state')
def _compute_current_session(self):
"""If there is an open session, store it to current_session_id / current_session_State.
"""
self.session_ids.fetch(["state"])
for pos_config in self:
opened_sessions = pos_config.session_ids.filtered(lambda s: s.state != 'closed')
rescue_sessions = opened_sessions.filtered('rescue')
session = pos_config.session_ids.filtered(lambda s: s.state != 'closed' and not s.rescue)
# sessions ordered by id desc
pos_config.has_active_session = opened_sessions and True or False
pos_config.current_session_id = session and session[0].id or False
pos_config.current_session_state = session and session[0].state or False
pos_config.number_of_rescue_session = len(rescue_sessions)
def _compute_statistics_for_session(self):
for config in self:
session = config.session_ids.filtered(lambda s: s.state != 'closed' and not s.rescue)
session_record = session[0] if session else None
if not session_record or not session_record.exists():
config.statistics_for_current_session = False
continue
config.statistics_for_current_session = config.get_statistics_for_session(session_record)
def get_statistics_for_session(self, session):
self.ensure_one()
currency = self.currency_id
timezone = pytz.timezone(self.env.context.get('tz') or self.env.user.tz or 'UTC')
statistics = {
'cash': {
'raw_opening_cash': session.cash_register_balance_start,
'opening_cash': currency.format(session.cash_register_balance_start)
},
'date': {
'is_started': bool(session.start_at),
'start_date': session.start_at.astimezone(timezone).strftime('%b %d') if session.start_at else False,
},
'orders': {
'paid': False,
'draft': False,
},
}
all_paid_orders = session.order_ids.filtered(lambda o: o.state in ['paid', 'done'])
refund_orders = all_paid_orders.filtered(lambda o: o.is_refund)
draft_orders = session.order_ids.filtered(lambda o: o.state == 'draft')
non_refund_orders = all_paid_orders - refund_orders
# calculate total refunded amount per original order for refund count check
refund_totals = defaultdict(float)
for refund in refund_orders:
if refund.refunded_order_id:
refund_totals[refund.refunded_order_id.id] += abs(refund.amount_total)
# count paid orders that are not completely refunded
paid_order_count = sum(
1 for order in non_refund_orders
if refund_totals.get(order.id, 0.0) != order.amount_total
)
if paid_order_count:
total_paid = sum(all_paid_orders.mapped('amount_total'))
statistics['orders']['paid'] = {
'amount': total_paid,
'count': paid_order_count,
'display': f"{currency.format(total_paid)} ({paid_order_count} {'order' if paid_order_count == 1 else 'orders'})"
}
if draft_orders:
total_draft = sum(draft_orders.mapped('amount_total'))
count_draft = len(draft_orders)
statistics['orders']['draft'] = {
'amount': total_draft,
'count': count_draft,
'display': f"{currency.format(total_draft)} ({count_draft} {'order' if count_draft == 1 else 'orders'})"
}
return statistics
@api.depends('session_ids')
def _compute_last_session(self):
PosSession = self.env['pos.session']
for pos_config in self:
session = PosSession.search_read(
[('config_id', '=', pos_config.id), ('state', '=', 'closed')],
['cash_register_balance_end_real', 'stop_at'],
order="stop_at desc", limit=1)
if session:
timezone = self.env.tz
pos_config.last_session_closing_date = session[0]['stop_at'].astimezone(timezone).date()
pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real']
else:
pos_config.last_session_closing_cash = 0
pos_config.last_session_closing_date = False
@api.depends('session_ids')
def _compute_current_session_user(self):
for pos_config in self:
session = pos_config.session_ids.filtered(lambda s: s.state in ['opening_control', 'opened', 'closing_control'] and not s.rescue)
if session:
pos_config.pos_session_username = session[0].user_id.sudo().name
pos_config.pos_session_state = session[0].state
pos_config.pos_session_duration = (
datetime.now() - session[0].start_at
).days if session[0].start_at else 0
pos_config.current_user_id = session[0].user_id
else:
pos_config.pos_session_username = False
pos_config.pos_session_state = False
pos_config.pos_session_duration = 0
pos_config.current_user_id = False
@api.constrains('rounding_method')
def _check_rounding_method_strategy(self):
for config in self:
if config.cash_rounding and config.rounding_method.strategy != 'add_invoice_line':
selection_value = "Add a rounding line"
for key, val in self.env["account.cash.rounding"]._fields["strategy"]._description_selection(config.env):
if key == "add_invoice_line":
selection_value = val
break
raise ValidationError(_(
"The cash rounding strategy of the point of sale %(pos)s must be: '%(value)s'",
pos=config.name,
value=selection_value,
))
def _check_profit_loss_cash_journal(self):
if self.cash_control and self.payment_method_ids:
for method in self.payment_method_ids:
if method.is_cash_count and (not method.journal_id.loss_account_id or not method.journal_id.profit_account_id):
raise ValidationError(_("You need a loss and profit account on your cash journal."))
@api.constrains('company_id', 'payment_method_ids')
def _check_company_payment(self):
for config in self:
if self.env['pos.payment.method'].search_count([('id', 'in', config.payment_method_ids.ids), ('company_id', '!=', config.company_id.id)]):
raise ValidationError(_("The payment methods for the point of sale %s must belong to its company.", self.name))
@api.constrains('pricelist_id', 'use_pricelist', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'payment_method_ids')
def _check_currencies(self):
for config in self:
if config.use_pricelist and config.pricelist_id and config.pricelist_id not in config.available_pricelist_ids:
raise ValidationError(_("The default pricelist must be included in the available pricelists."))
# Check if the config's payment methods are compatible with its currency
for pm in config.payment_method_ids:
if pm.journal_id and pm.journal_id.currency_id and pm.journal_id.currency_id != config.currency_id:
raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))
if config.use_pricelist and any(config.available_pricelist_ids.mapped(lambda pricelist: pricelist.currency_id != config.currency_id)):
raise ValidationError(_("All available pricelists must be in the same currency as the company or"
" as the Sales Journal set on this point of sale if you use"
" the Accounting application."))
if config.invoice_journal_id.currency_id and config.invoice_journal_id.currency_id != config.currency_id:
raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."))
def _check_payment_method_ids(self):
self.ensure_one()
if not self.payment_method_ids:
raise ValidationError(
_("You must have at least one payment method configured to launch a session.")
)
@api.constrains('pricelist_id', 'available_pricelist_ids')
def _check_pricelists(self):
self._check_companies()
self = self.sudo()
if self.pricelist_id.company_id and self.pricelist_id.company_id != self.company_id:
raise ValidationError(
_("The default pricelist must belong to no company or the company of the point of sale."))
@api.constrains('company_id', 'available_pricelist_ids')
def _check_companies(self):
for config in self:
if any(pricelist.company_id.id not in [False, config.company_id.id] for pricelist in config.available_pricelist_ids):
raise ValidationError(_("The selected pricelists must belong to no company or the company of the point of sale."))
def _check_company_has_template(self):
self.ensure_one()
if not self.company_has_template:
raise ValidationError(_("No chart of account configured, go to the \"configuration / settings\" menu, and "
"install one from the Invoicing tab."))
@api.constrains('payment_method_ids')
def _check_payment_method_ids_journal(self):
for config in self:
for cash_method in config.payment_method_ids.filtered(lambda m: m.journal_id.type == 'cash'):
if self.env['pos.config'].search_count([('id', '!=', config.id), ('payment_method_ids', 'in', cash_method.ids)], limit=1):
raise ValidationError(_("This cash payment method is already used in another Point of Sale.\n"
"A new cash payment method should be created for this Point of Sale."))
if len(cash_method.journal_id.pos_payment_method_ids) > 1:
raise ValidationError(_("You cannot use the same journal on multiples cash payment methods."))
@api.constrains('trusted_config_ids')
def _check_trusted_config_ids_currency(self):
for config in self:
for trusted_config in config.trusted_config_ids:
if trusted_config.currency_id != config.currency_id:
raise ValidationError(_("You cannot share open orders with configuration that does not use the same currency."))
def _check_header_footer(self, values):
if not self.env.is_admin() and {'is_header_or_footer', 'receipt_header', 'receipt_footer'} & values.keys():
raise AccessError(_('Only administrators can edit receipt headers and footers'))
def _check_company_has_fiscal_country(self):
self.ensure_one()
if not self.company_id.account_fiscal_country_id:
raise ValidationError(_("The company must have a fiscal country set."))
@api.model_create_multi
def create(self, vals_list):
if not self._default_warehouse_id():
self.env['stock.warehouse'].create({
'code': vals_list[0].get('name')[:3], # first 3 characters of pos.config name
'company_id': self.env.company.id,
})
for vals in vals_list:
self._check_header_footer(vals)
pos_configs = super().create(vals_list)
pos_configs._create_sequences()
pos_configs.sudo()._check_modules_to_install()
pos_configs.sudo()._check_groups_implied()
pos_configs._update_preparation_printers_menuitem_visibility()
# If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
return pos_configs
def _create_sequences(self):
for pos_config in self:
sequence_vals = {
'padding': 6,
'code': "pos.order",
'company_id': pos_config.company_id.id,
'implementation': 'no_gap',
}
# Create sequences for all orders
pos_config.order_seq_id = self.env['ir.sequence'].sudo().create({
**sequence_vals,
'name': _('POS order from config #%s', pos_config.id),
})
# Create sequences for order that are created from self ore backend
pos_config.order_backend_seq_id = self.env['ir.sequence'].sudo().create({
**sequence_vals,
'name': _('POS order backend from config #%s', pos_config.id),
})
# Create sequences for all order lines
pos_config.order_line_seq_id = self.env['ir.sequence'].sudo().create({
**sequence_vals,
'name': _('POS order line from config #%s', pos_config.id),
})
# Create sequences for devices
pos_config.device_seq_id = self.env['ir.sequence'].sudo().create({
**sequence_vals,
'name': _('POS device from config #%s', pos_config.id),
'padding': 0,
})
def register_new_device_identifier(self):
self.ensure_one()
identifier = self.device_seq_id._next()
return {
'device_identifier': identifier,
}
def _reset_default_on_vals(self, vals):
if 'tip_product_id' in vals and not vals['tip_product_id'] and 'iface_tipproduct' in vals and vals['iface_tipproduct']:
default_product = self.env.ref('point_of_sale.product_product_tip', False)
if default_product:
vals['tip_product_id'] = default_product.id
else:
raise UserError(_('The default tip product is missing. Please manually specify the tip product. (See Tips field.)'))
def _update_preparation_printers_menuitem_visibility(self):
prepa_printers_menuitem = self.sudo().env.ref('point_of_sale.menu_pos_preparation_printer', raise_if_not_found=False)
if prepa_printers_menuitem:
prepa_printers_menuitem.active = self.sudo().env['pos.config'].search_count([('is_order_printer', '=', True)], limit=1) > 0
@api.depends('use_pricelist', 'pricelist_id', 'available_pricelist_ids', 'payment_method_ids', 'limit_categories',
'iface_available_categ_ids', 'module_pos_hr', 'module_pos_discount', 'iface_tipproduct', 'default_preset_id', 'module_pos_appointment')
def _compute_local_data_integrity(self):
self.last_data_change = self.env.cr.now()
def write(self, vals):
self._check_header_footer(vals)
self._reset_default_on_vals(vals)
if ('is_order_printer' in vals and not vals['is_order_printer']):
vals['printer_ids'] = [fields.Command.clear()]
bypass_payment_method_ids_forbidden_change = self.env.context.get('bypass_payment_method_ids_forbidden_change', False)
self._preprocess_x2many_vals_from_settings_view(vals)
vals = self._keep_new_vals(vals)
opened_session = self.mapped('session_ids').filtered(lambda s: s.state != 'closed')
if opened_session:
forbidden_fields = []
for key in self._get_forbidden_change_fields():
if key in vals.keys():
if bypass_payment_method_ids_forbidden_change and key == 'payment_method_ids':
continue
field_name = self._fields[key].get_description(self.env)["string"]
forbidden_fields.append(field_name)
if len(forbidden_fields) > 0:
raise UserError(_(
"Unable to modify this PoS Configuration because you can't modify %s while a session is open.",
", ".join(forbidden_fields)
))
result = super(PosConfig, self).write(vals)
for config in self:
if config.use_presets and config.default_preset_id and config.default_preset_id.id not in config.available_preset_ids.ids:
config.available_preset_ids |= config.default_preset_id
self.sudo()._set_fiscal_position()
self.sudo()._check_modules_to_install()
self.sudo()._check_groups_implied()
if 'is_order_printer' in vals:
self._update_preparation_printers_menuitem_visibility()
return result
def _preprocess_x2many_vals_from_settings_view(self, vals):
""" From the res.config.settings view, changes in the x2many fields always result to an array of link commands or a single set command.
- As a result, the items that should be unlinked are not properly unlinked.
- So before doing the write, we inspect the commands to determine which records should be unlinked.
- We only care about the link command.
- We can consider set command as absolute as it will replace all.
"""
from_settings_view = self.env.context.get('from_settings_view')
if not from_settings_view:
# If vals is not from the settings view, we don't need to preprocess.
return
# Only ensure one when write is from settings view.
self.ensure_one()
fields_to_preprocess = []
for f in self.fields_get([]).values():
if f['type'] in ['many2many', 'one2many']:
fields_to_preprocess.append(f['name'])
for x2many_field in fields_to_preprocess:
if x2many_field in vals:
linked_ids = set(self[x2many_field].ids)
for command in vals[x2many_field]:
if command[0] == 4:
_id = command[1]
if _id in linked_ids:
linked_ids.remove(_id)
# Remaining items in linked_ids should be unlinked.
unlink_commands = [Command.unlink(_id) for _id in linked_ids]
vals[x2many_field] = unlink_commands + vals[x2many_field]
def _keep_new_vals(self, vals):
""" Keep values in vals that are different than
self's values.
"""
from_settings_view = self.env.context.get('from_settings_view')
if not from_settings_view:
return vals
new_vals = {}
for field, val in vals.items():
config_field = self._fields.get(field)
if config_field:
cache_value = config_field.convert_to_cache(val, self)
record_value = config_field.convert_to_record(cache_value, self)
if record_value != self[field]:
new_vals[field] = val
return new_vals
def _get_forbidden_change_fields(self):
return ['module_pos_restaurant', 'payment_method_ids', 'active']
def unlink(self):
# Delete the pos.config records first then delete the sequences linked to them
sequences_to_delete = self.order_line_seq_id | self.device_seq_id
res = super(PosConfig, self).unlink()
sequences_to_delete.unlink()
return res
# TODO-JCB: Maybe we can move this logic in `_reset_default_on_vals`
def _set_fiscal_position(self):
for config in self:
if config.tax_regime_selection and config.default_fiscal_position_id and (config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids):
config.fiscal_position_ids = [(4, config.default_fiscal_position_id.id)]
elif not config.tax_regime_selection and config.fiscal_position_ids.ids:
config.fiscal_position_ids = [(5, 0, 0)]
def _check_modules_to_install(self):
# determine modules to install
expected = [
fname[7:] # 'module_account' -> 'account'
for fname in self._fields
if fname.startswith('module_')
if any(pos_config[fname] for pos_config in self)
]
if expected:
STATES = ('installed', 'to install', 'to upgrade')
modules = self.env['ir.module.module'].sudo().search([('name', 'in', expected)])
modules = modules.filtered(lambda module: module.state not in STATES)
if modules:
modules.button_immediate_install()
# just in case we want to do something if we install a module. (like a refresh ...)
return True
return False
def _check_groups_implied(self):
for pos_config in self:
for field_name in [f for f in pos_config._fields if f.startswith('group_')]:
field = pos_config._fields[field_name]
if field.type in ('boolean', 'selection') and hasattr(field, 'implied_group'):
field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',')
field_groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in field_group_xmlids))
field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]})
def execute(self):
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
# Methods to open the POS
def _action_to_open_ui(self):
if not self.current_session_id:
self.env['pos.session'].create({'user_id': self.env.uid, 'config_id': self.id})
pos_url = '/pos/ui/%d?from_backend=True' % self.id
debug = request and request.session.debug
if debug:
pos_url += '&debug=%s' % debug
return {
'type': 'ir.actions.act_url',
'url': pos_url,
'target': 'self',
}
def _get_url_to_cache(self, debug):
url_to_cache = [
f"/pos/ui/{self.id}?from_backend=True",
f"/pos/ui/{self.id}",
]
return self.env["ir.qweb"]._get_asset_links("point_of_sale.assets_prod", debug=debug) + url_to_cache
def _check_before_creating_new_session(self):
self._check_company_has_template()
self._check_pricelists()
self._check_company_payment()
self._check_currencies()
self._check_profit_loss_cash_journal()
self._check_payment_method_ids()
def open_ui(self):
"""Open the pos interface with config_id as an extra argument.
In vanilla PoS each user can only have one active session, therefore it was not needed to pass the config_id
on opening a session. It is also possible to login to sessions created by other users.
:returns: dict
"""
self.ensure_one()
# In case of test environment, don't create the pdf
if self.env.uid == SUPERUSER_ID and not tools.config['test_enable']:
raise UserError(_("You do not have permission to open a POS session. Please try opening a session with a different user"))
if not self.current_session_id:
res = self._check_before_creating_new_session()
if res:
return res
self._validate_fields(self._fields)
self._check_company_has_fiscal_country()
return self._action_to_open_ui()
def close_ui(self):
return self.open_ui()
def open_existing_session_cb(self):
""" close session button
access session form to validate entries
"""
self.ensure_one()
return self._open_session(self.current_session_id.id)
def _open_session(self, session_id):
self._check_pricelists() # The pricelist company might have changed after the first opening of the session
return {
'name': _('Session'),
'view_mode': 'form,list',
'res_model': 'pos.session',
'res_id': session_id,
'view_id': False,
'type': 'ir.actions.act_window',
}
def open_opened_rescue_session_form(self):
rescue_session_ids = self.session_ids.filtered(lambda s: s.state != 'closed' and s.rescue)
if len(rescue_session_ids) == 1:
return {
'res_model': 'pos.session',
'view_mode': 'form',
'res_id': rescue_session_ids.id,
'type': 'ir.actions.act_window',
}
else:
return {
'name': _('Rescue Sessions'),
'res_model': 'pos.session',
'view_mode': 'list,form',
'domain': [('id', 'in', rescue_session_ids.ids)],
'type': 'ir.actions.act_window',
}
def _link_same_non_cash_payment_methods(self, source_config):
pms = source_config.payment_method_ids.filtered(lambda pm: not pm.is_cash_count)
if pms:
self.payment_method_ids = [Command.link(pm.id) for pm in pms]
def _is_journal_exist(self, journal_code, name, company_id):
account_journal = self.env['account.journal']
existing_journal = account_journal.search([
('name', '=', name),
('code', '=', journal_code),
('company_id', '=', company_id),
], limit=1)
return existing_journal.id or account_journal.create({
'name': name,
'code': journal_code,
'type': 'cash',
'company_id': company_id,
}).id
def _is_pos_pm_exist(self, name, journal_id, company_id):
pos_payment = self.env['pos.payment.method']
existing_pos_cash_pm = pos_payment.search([
('name', '=', name),
('journal_id', '=', journal_id),
('company_id', '=', company_id),
], limit=1)
return existing_pos_cash_pm.id or pos_payment.create({
'name': name,
'journal_id': journal_id,
'company_id': company_id,
}).id
def get_limited_product_count(self):
config_param = self.env['ir.config_parameter'].sudo().get_param('point_of_sale.limited_product_count', DEFAULT_LIMIT_LOAD_PRODUCT)
try:
return int(config_param)
except (TypeError, ValueError, OverflowError):
return DEFAULT_LIMIT_LOAD_PRODUCT
def _get_limited_partner_count(self):
config_param = self.env['ir.config_parameter'].sudo().get_param('point_of_sale.limited_customer_count', DEFAULT_LIMIT_LOAD_PARTNER)
try:
return int(config_param)
except (TypeError, ValueError, OverflowError):
return DEFAULT_LIMIT_LOAD_PARTNER
def get_limited_partners_loading(self, offset=0):
return self.env.execute_query(SQL("""
WITH pm AS
(
SELECT partner_id,
Count(partner_id) order_count
FROM pos_order
GROUP BY partner_id)
SELECT id
FROM res_partner AS partner
LEFT JOIN pm
ON (
partner.id = pm.partner_id)
WHERE (
partner.company_id=%s OR partner.company_id IS NULL
)
ORDER BY COALESCE(pm.order_count, 0) DESC,
NAME limit %s offset %s;
""", self.company_id.id, self._get_limited_partner_count(), offset))
def action_pos_config_modal_edit(self):
return {
'view_mode': 'form',
'res_model': 'pos.config',
'type': 'ir.actions.act_window',
'target': 'new',
'res_id': self.id,
'context': {'pos_config_open_modal': True},
}
def _add_trusted_config_id(self, config_id):
self.trusted_config_ids += config_id
def _remove_trusted_config_id(self, config_id):
self.trusted_config_ids -= config_id
def _get_payment_method(self, payment_type):
for pm in self.payment_method_ids:
if pm.type == payment_type:
return pm
return False
def _get_special_products(self):
return self.env.ref('point_of_sale.product_product_tip', raise_if_not_found=False) or self.env['product.product']
def update_customer_display(self, order, device_uuid):
self.ensure_one()
self._notify(f"UPDATE_CUSTOMER_DISPLAY-{device_uuid}", order)
def _get_display_device_ip(self):
self.ensure_one()
return self.proxy_ip
def _get_customer_display_data(self):
self.ensure_one()
return {
'config_id': self.id,
'access_token': self.access_token,
'has_bg_img': bool(self.customer_display_bg_img),
'company_id': self.company_id.id,
'proxy_ip': self._get_display_device_ip(),
}
@api.model
def _create_cash_payment_method(self, cash_journal_vals=None):
if cash_journal_vals is None:
cash_journal_vals = {}
journal_vals = {
'name': _('Cash'),
'type': 'cash',
'company_id': self.env.company.id,
**cash_journal_vals,
}
default_cash_account = self.env['account.account'].with_context(lang='en_US').search([
('account_type', '=', 'asset_cash'),
('name', '=', 'Cash'),
('company_ids', 'in', self.env.company.root_id.id)
], limit=1)
if default_cash_account:
journal_vals['default_account_id'] = default_cash_account.id
cash_journal = self.env['account.journal'].create(journal_vals)
return self.env['pos.payment.method'].create({
'name': _('Cash'),
'journal_id': cash_journal.id,
'company_id': self.env.company.id,
})
def _create_journal_and_payment_methods(self, cash_ref=None, cash_journal_vals=None):
"""This should only be called at creation of a new pos.config."""
journal = self.env['account.journal']._ensure_company_account_journal()
payment_methods = self.env['pos.payment.method']
# create cash payment method per config
cash_pm_from_ref = cash_ref and self.env.ref(cash_ref, raise_if_not_found=False)
if cash_pm_from_ref:
try:
cash_pm_from_ref.check_access('read')
cash_pm = cash_pm_from_ref
except AccessError:
cash_pm = self._create_cash_payment_method(cash_journal_vals)
else:
cash_pm = self._create_cash_payment_method(cash_journal_vals)
if cash_ref and cash_pm != cash_pm_from_ref:
self.env['ir.model.data']._update_xmlids([{
'xml_id': cash_ref,
'record': cash_pm,
'noupdate': True,
}])
payment_methods |= cash_pm
# only create bank and customer account payment methods per company
bank_pm = self.env['pos.payment.method'].search([('journal_id.type', '=', 'bank'), ('company_id', 'in', self.env.company.parent_ids.ids)])
if not bank_pm:
bank_journal = self.env['account.journal'].search([('type', '=', 'bank'), ('company_id', 'in', self.env.company.parent_ids.ids)], limit=1)
if not bank_journal:
raise UserError(_('Ensure that there is an existing bank journal. Check if chart of accounts is installed in your company.'))
chart_template = self.with_context(allowed_company_ids=self.env.company.root_id.ids).env['account.chart.template']
outstanding_account = chart_template.ref('account_journal_payment_debit_account_id', raise_if_not_found=False) or self.env.company.transfer_account_id
bank_pm = self.env['pos.payment.method'].create({
'name': _('Card'),
'journal_id': bank_journal.id,
'outstanding_account_id': outstanding_account.id if outstanding_account else False,
'company_id': self.env.company.id,
'sequence': 1,
})
payment_methods |= bank_pm
pay_later_pm = self.env['pos.payment.method'].search([('journal_id', '=', False), ('company_id', 'in', self.env.company.parent_ids.ids)])
if not pay_later_pm:
pay_later_pm = self.env['pos.payment.method'].create({
'name': _('Customer Account'),
'company_id': self.env.company.id,
'split_transactions': True,
'sequence': 2,
})
payment_methods |= pay_later_pm
return journal, payment_methods.ids
def get_record_by_ref(self, recordRefs):
# filters out unavailable external id
return [self.env.ref(record).id for record in recordRefs if self.env.ref(record, raise_if_not_found=False)]
def load_demo_data(self):
self = self.with_context(bypass_categories_forbidden_change=True)
xml_id = self.get_external_id().get(self.id) or self._get_default_demo_data_xml_id()
loaders = self._get_demo_data_loader_methods()
for prefix, loader in loaders.items():
if xml_id.startswith(prefix):
return loader(True)
return loaders.get(self._get_default_demo_data_xml_id(), self._load_onboarding_furniture_demo_data)(True)
def _get_demo_data_loader_methods(self):
return {
'point_of_sale.pos_config_clothes': self._load_onboarding_clothes_demo_data,
'point_of_sale.pos_config_bakery': self._load_onboarding_bakery_demo_data,
'point_of_sale.pos_config_main': self._load_onboarding_furniture_demo_data,
}
def _get_default_demo_data_xml_id(self):
return 'point_of_sale.pos_config_main'
@api.model
def load_onboarding_clothes_scenario(self, with_demo_data=True):
journal, payment_methods_ids = self._create_journal_and_payment_methods(
cash_journal_vals={'name': _('Cash Clothes Shop'), 'show_on_dashboard': False})
config = self.env['pos.config'].create([{
'name': _('Clothes Shop'),
'company_id': self.env.company.id,
'journal_id': journal.id,
'payment_method_ids': payment_methods_ids
}])
self.env['ir.model.data']._update_xmlids([{
'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_clothes'),
'record': config,
'noupdate': True,
}])
config._load_onboarding_clothes_demo_data(with_demo_data)
return {'config_id': config.id}
def _load_onboarding_clothes_demo_data(self, with_demo_data=True):
self.ensure_one()
convert.convert_file(self._env_with_clean_context(), 'point_of_sale', 'data/scenarios/clothes_category_data.xml', idref=None, mode='init', noupdate=True)
if with_demo_data:
product_module = self.env['ir.module.module'].search([('name', '=', 'product')])
if not product_module.demo:
convert.convert_file(self._env_with_clean_context(), 'product', 'data/product_attribute_demo.xml', idref=None, mode='init', noupdate=True)
convert.convert_file(self._env_with_clean_context(), 'point_of_sale', 'data/scenarios/clothes_data.xml', idref=None, mode='init', noupdate=True)
clothes_categories = self.get_record_by_ref([
'point_of_sale.pos_category_upper',
'point_of_sale.pos_category_lower',
'point_of_sale.pos_category_others'
])
if clothes_categories:
self.limit_categories = True
self.iface_available_categ_ids = clothes_categories
@api.model
def load_onboarding_bakery_scenario(self, with_demo_data=True):
journal, payment_methods_ids = self._create_journal_and_payment_methods(
cash_journal_vals={'name': _('Cash Bakery'), 'show_on_dashboard': False})
config = self.env['pos.config'].create({
'name': _('Bakery Shop'),
'company_id': self.env.company.id,
'journal_id': journal.id,
'payment_method_ids': payment_methods_ids
})
self.env['ir.model.data']._update_xmlids([{
'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_bakery'),
'record': config,
'noupdate': True,
}])
config._load_onboarding_bakery_demo_data(with_demo_data)
return {'config_id': config.id}
def _load_onboarding_bakery_demo_data(self, with_demo_data=True):
self.ensure_one()
convert.convert_file(self._env_with_clean_context(), 'point_of_sale', 'data/scenarios/bakery_category_data.xml', idref=None, mode='init', noupdate=True)
if with_demo_data:
convert.convert_file(self._env_with_clean_context(), 'point_of_sale', 'data/scenarios/bakery_data.xml', idref=None, mode='init', noupdate=True)
bakery_categories = self.get_record_by_ref([
'point_of_sale.pos_category_breads',
'point_of_sale.pos_category_pastries',
])
if bakery_categories:
self.limit_categories = True
self.iface_available_categ_ids = bakery_categories
@api.model
def load_onboarding_furniture_scenario(self, with_demo_data=True):
journal, payment_methods_ids = self._create_journal_and_payment_methods(
cash_ref='point_of_sale.cash_payment_method_furniture',
cash_journal_vals={'name': _("Cash Furn. Shop"), 'show_on_dashboard': False},
)
config = self.env['pos.config'].create([{
'name': _('Furniture Shop'),
'company_id': self.env.company.id,
'journal_id': journal.id,
'payment_method_ids': payment_methods_ids
}])
self.env['ir.model.data']._update_xmlids([{
'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_main'),
'record': config,
'noupdate': True,
}])
config._load_onboarding_furniture_demo_data(with_demo_data)
existing_session = self.env.ref('point_of_sale.pos_closed_session_2', 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(), 'point_of_sale', 'data/orders_demo.xml', idref=None, mode='init', noupdate=True)
return {'config_id': config.id}
def _load_onboarding_furniture_demo_data(self, with_demo_data=False):
self.ensure_one()
convert.convert_file(self._env_with_clean_context(), 'point_of_sale', 'data/scenarios/furniture_category_data.xml', idref=None, mode='init', noupdate=True)
if with_demo_data:
product_module = self.env['ir.module.module'].search([('name', '=', 'product')])
if not product_module.demo:
convert.convert_file(self._env_with_clean_context(), 'product', 'data/product_category_demo.xml', idref=None, mode='init', noupdate=True)
convert.convert_file(self._env_with_clean_context(), 'product', 'data/product_attribute_demo.xml', idref=None, mode='init', noupdate=True)
convert.convert_file(self._env_with_clean_context(), 'product', 'data/product_demo.xml', idref=None, mode='init', noupdate=True)
convert.convert_file(self._env_with_clean_context(), 'point_of_sale', 'data/scenarios/furniture_data.xml', idref=None, mode='init', noupdate=True)
furniture_categories = self.get_record_by_ref([
'point_of_sale.pos_category_miscellaneous',
'point_of_sale.pos_category_desks',
'point_of_sale.pos_category_chairs'
])
if furniture_categories:
self.limit_categories = True
self.iface_available_categ_ids = furniture_categories
@api.model
def load_onboarding_retail_scenario(self, with_demo_data=False):
journal, payment_methods_ids = self._create_journal_and_payment_methods(
cash_journal_vals={'name': _("Cash %s", self.env.company.name), 'show_on_dashboard': False},
)
config = self.env['pos.config'].create([{
'name': self.env.company.name,
'company_id': self.env.company.id,
'journal_id': journal.id,
'payment_method_ids': payment_methods_ids
}])
self.env['ir.model.data']._update_xmlids([{
'xml_id': self._get_suffixed_ref_name('point_of_sale.pos_config_retail'),
'record': config,
'noupdate': True,
}])
return {'config_id': config.id}
def _get_suffixed_ref_name(self, ref_name):
"""Suffix the given ref_name with the id of the current company if it's not the main company."""
main_company = self.env.ref('base.main_company', raise_if_not_found=False)
if main_company and self.env.company.id == main_company.id:
return ref_name
else:
return f"{ref_name}_{self.env.company.id}"
@api.model
def get_pos_kanban_view_state(self):
has_pos_config = bool(self.env['pos.config'].search_count(
self._check_company_domain(self.env.company)
))
has_chart_template = bool(self.env.company.chart_template)
main_company = self.env.ref('base.main_company', raise_if_not_found=False)
return {
"has_pos_config": has_pos_config,
"has_chart_template": has_chart_template,
"is_restaurant_installed": bool(self.env['ir.module.module'].search_count([('name', '=', 'pos_restaurant'), ('state', '=', 'installed')])),
"is_main_company": main_company and self.env.company.id == main_company.id or False
}
@api.model
def install_pos_restaurant(self):
pos_restaurant_module = self.env['ir.module.module'].search([('name', '=', 'pos_restaurant')])
pos_restaurant_module.button_immediate_install()
return {'installed_with_demo': pos_restaurant_module.demo}
def _get_available_pricelists(self):
self.ensure_one()
return self.available_pricelist_ids + self.pricelist_id if self.use_pricelist else self.pricelist_id
def _env_with_clean_context(self):
safe_context = {}
if 'allowed_company_ids' in self.env.context:
safe_context['allowed_company_ids'] = self.env.context['allowed_company_ids']
return self.env(context=safe_context)
@api.model
def _set_default_pos_load_limit(self):
param_model = self.env["ir.config_parameter"]
if not param_model.get_param("point_of_sale.limited_product_count"):
param_model.set_param("point_of_sale.limited_product_count", DEFAULT_LIMIT_LOAD_PRODUCT)
if not param_model.get_param("point_of_sale.limited_customer_count"):
param_model.set_param("point_of_sale.limited_customer_count", DEFAULT_LIMIT_LOAD_PARTNER)
def _is_quantities_set(self):
return self.is_closing_entry_by_product
@api.onchange("epson_printer_ip")
def _onchange_epson_printer_ip(self):
for rec in self:
if rec.epson_printer_ip:
rec.epson_printer_ip = format_epson_certified_domain(rec.epson_printer_ip)