import secrets import psycopg2 import json from odoo import fields, models, api, Command, SUPERUSER_ID, _ from odoo.exceptions import ValidationError, UserError from odoo.modules.registry import Registry from odoo.tools import SQL from odoo.addons.pos_urban_piper import const from .pos_urban_piper_request import UrbanPiperClient class PosConfig(models.Model): _inherit = 'pos.config' def _default_payment_methods(self): """ Override default payment methods to filter out delivery payment methods. """ payment_methods = super()._default_payment_methods() return payment_methods.filtered(lambda pm: not pm.is_delivery_payment) def _default_urbanpiper_store_identifier(self): """ Default unique Urban Piper POS ID. """ return secrets.token_hex() def _default_urbanpiper_pricelist(self): return self.env['product.pricelist'].search([ ('name', 'ilike', 'Urbanpiper'), ('currency_id', '=', self.env.company.currency_id.id), ('company_id', '=', self.env.company.id) ], limit=1) def _default_urbanpiper_fiscal_position(self): fiscal_position = self.env.ref('pos_urban_piper.pos_account_fiscal_position_urbanpiper', False) if fiscal_position and fiscal_position.sudo().company_id.id == self.env.company.id: return fiscal_position name = fields.Char( translate=True, ) urbanpiper_store_identifier = fields.Char( string='Urban Piper POS ID', help='Pos ID from Urban Piper (Atlas)', default=_default_urbanpiper_store_identifier, copy=False, ) urbanpiper_pricelist_id = fields.Many2one( 'product.pricelist', string='Urban Piper Pricelist', help='Pricelist for Urban Piper sync menu.', default=_default_urbanpiper_pricelist, ) urbanpiper_fiscal_position_id = fields.Many2one( 'account.fiscal.position', check_company=True, string='Urban Piper Fiscal Position', help='Fiscal position for Urban Piper sync menu.', default=_default_urbanpiper_fiscal_position, ) urbanpiper_payment_methods_ids = fields.Many2many( 'pos.payment.method', 'pos_config_urbanpiper_payment_method_ids_rel', 'config_id', string='Urban Piper Payment Methods', help='Payment methods for Urban Piper sync menu.' ) urbanpiper_last_sync_date = fields.Datetime(string='Last Sync on', help='Last sync date for menu sync.') urbanpiper_webhook_url = fields.Char( string='Register Urbanpiper Webhook URL', help='Store webhook url (base url) for security.' ) urbanpiper_delivery_provider_ids = fields.Many2many( 'pos.delivery.provider', string='Delivery Providers', help='The delivery providers used for online delivery through UrbanPiper.', ) _urbanpiper_store_identifier_uniq = models.Constraint( 'unique(urbanpiper_store_identifier)', "Store ID must be unique for every pos configuration.", ) def _init_column(self, column_name): if column_name != 'urbanpiper_store_identifier': return super()._init_column(column_name) # fetch void columns self.env.cr.execute(SQL("SELECT id FROM pos_config WHERE urbanpiper_store_identifier IS NULL")) pos_config_ids = self.env.cr.fetchall() if not pos_config_ids: return # update existing columns for pos_config_id in pos_config_ids: self.env.cr.execute(SQL( """ UPDATE pos_config SET urbanpiper_store_identifier = %s WHERE id = %s; """, self._default_urbanpiper_store_identifier(), pos_config_id[0] )) @api.model_create_multi def create(self, vals_list): pos_configs = super().create(vals_list) for config in pos_configs: if config.module_pos_urban_piper: config._setup_journals_and_payment_methods() config._configure_fiscal_position_and_pricelist() return pos_configs def write(self, vals): res = super().write(vals) for config in self: if (config.module_pos_urban_piper or vals.get('module_pos_urban_piper')) and vals.get('urbanpiper_store_identifier'): if not config.urbanpiper_payment_methods_ids or vals.get('urbanpiper_delivery_provider_ids'): config._setup_journals_and_payment_methods() config._configure_fiscal_position_and_pricelist() return res def _setup_journals_and_payment_methods(self): """ Fetch or create payment methods and enable option fiscal taxes. """ self.ensure_one() delivery_providers = self.urbanpiper_delivery_provider_ids for delivery_provider in delivery_providers: journal = self.env['account.journal'].sudo().search([ ('code', '=', f"{delivery_provider.journal_code}{self.id}"), ('company_id', '=', self.company_id.id), ], limit=1) if not journal: journal = self.env['account.journal'].sudo().create({ 'name': f"{delivery_provider.name} - {self.name}", 'code': f"{delivery_provider.journal_code}{self.id}", 'type': 'bank', 'company_id': self.company_id.id, }) payment_method = self.env['pos.payment.method'].sudo().search([ ('journal_id', '=', journal.id), ('is_delivery_payment', '=', True), ('delivery_provider_id', '=', delivery_provider.id), ], limit=1) if not payment_method: payment_method = self.env['pos.payment.method'].sudo().create({ 'name': f"{delivery_provider.name} - {self.name}", 'journal_id': journal.id, 'company_id': self.company_id.id, 'is_delivery_payment': True, 'delivery_provider_id': delivery_provider.id }) self.urbanpiper_payment_methods_ids |= payment_method def _configure_fiscal_position_and_pricelist(self): """ Set taxes in fiscal position for urban piper. """ self.ensure_one() fiscal_position = self.urbanpiper_fiscal_position_id or self.env.ref('pos_urban_piper.pos_account_fiscal_position_urbanpiper', False) if not fiscal_position or fiscal_position.sudo().company_id.id != self.company_id.id: fiscal_position = self.env['account.fiscal.position'].create({ 'name': 'UrbanPiper' }) if self.module_pos_urban_piper: if not self.urbanpiper_fiscal_position_id: self.urbanpiper_fiscal_position_id = fiscal_position if not self.urbanpiper_pricelist_id: pricelist = self.env['product.pricelist'].search([ ('name', 'ilike', 'UrbanPiper'), ('currency_id', '=', self.company_id.currency_id.id), ('company_id', '=', self.company_id.id) ]) if not pricelist: pricelist = self.env['product.pricelist'].create({ 'name': 'UrbanPiper', 'currency_id': self.company_id.currency_id.id, 'item_ids': [Command.create({ 'compute_price': 'percentage', 'percent_price': -30, })] }) self.urbanpiper_pricelist_id = pricelist.id self._add_line_to_fiscal_position(fiscal_position) def _add_line_to_fiscal_position(self, fiscal_position): # Outside of India, prices are tax-inclusive. So if users want tax-specific behavior, # they must manually add a tax line to the fiscal position. pass def prepare_taxes_data(self, pos_products): """ Prepare taxes data for urban piper for sync menu. """ return [] def update_urbanpiper_item_data(self, item, product): """ Override this method to update item data with product information. (as per requirements for different regions) """ self.ensure_one() return item def update_store_status(self, status): """ Activate and Deactivate store """ user_name = self.env['ir.config_parameter'].sudo().get_param('pos_urban_piper.urbanpiper_username') api_key = self.env['ir.config_parameter'].sudo().get_param('pos_urban_piper.urbanpiper_apikey') if not(user_name == 'demo' or api_key == 'demo'): up = UrbanPiperClient(self.with_context(provider_name=self.env.context.get('providerName'))) up.configure_webhook() up.urbanpiper_store_status_update(status=status) def order_status_update(self, order_id, new_status, code=None, urban_piper_test=False, extra_args={}): """ Update order status from urban piper webhook """ self.ensure_one() order = self.env['pos.order'].browse(order_id) if new_status == 'Food Ready' and order.state != 'paid': self._make_order_payment(order) up = UrbanPiperClient(self) is_success, message = False, '' order.prep_time = extra_args.get('orderPrepTime', 0) if urban_piper_test or (order.delivery_provider_id.technical_name in ['careem'] and new_status == 'Food Ready'): is_success = True else: is_success, message = up.request_status_update(order.delivery_identifier, new_status, order.prep_time, code) if is_success: order.write({ 'delivery_status': const.ORDER_STATUS_MAPPING[new_status][1], }) if not urban_piper_test and new_status == 'Acknowledged': up.urbanpiper_order_reference_update(order) if new_status == 'Cancelled': # Prevent loading cancelled orders in `getServerOrders` triggered by `_send_delivery_order_count` order.state = 'cancel' self._send_delivery_order_count(order_id) return {'is_success': is_success, 'message': message} def _make_order_payment(self, order): """ Make payment for order of urban piper orders. """ self.ensure_one() payment_method = self.urbanpiper_payment_methods_ids.filtered(lambda pm: pm.delivery_provider_id.id == order.delivery_provider_id.id) if not payment_method: payment_method = self.urbanpiper_payment_methods_ids[0] context_payment = { 'active_ids': [order.id], 'active_id': order.id } self.env['pos.make.payment'].with_context(context_payment).create({ 'amount': order.amount_total, 'payment_method_id': payment_method.id, }).check() order._compute_prices() def _send_delivery_order_count(self, order_id=None): """ Send delivery order count to pos ui """ self.ensure_one() if self.current_session_id: self._notify('DELIVERY_ORDER_COUNT', order_id) def _store_action_update(self, data): """ Send store status update to pos ui """ self.ensure_one() if self.current_session_id: self._notify('STORE_ACTION', data) def get_delivery_data(self): """ Fetch delivery order count and providers for pos ui """ self.ensure_one() delivery_order_count = self._get_urbanpiper_order_count() delivery_providers = self._get_active_delivery_providers() total_new_order = sum( provider_data.get('awaiting', 0) for provider_data in delivery_order_count['urbanpiper'].values() ) combined_data = { 'delivery_order_count': delivery_order_count, 'delivery_providers': delivery_providers, 'total_new_order': total_new_order, } return combined_data def _get_total_tax_tag(self): self.ensure_one() return 'total_included' def _get_urbanpiper_order_count(self): """ Updates order count whenever order status changes and when new order receives """ self.ensure_one() if not self.current_session_id: return {} session_id = self.current_session_id.id order_statuses = ['placed', 'acknowledged', 'food_ready', 'dispatched', 'completed'] order_count_data = {} for provider in self.urbanpiper_delivery_provider_ids: order_counts = { status: self.env['pos.order'].search_count([ ('delivery_status', '=', status), ('delivery_provider_id', '=', provider.id), ('session_id', '=', session_id), ('state', '!=', 'cancel') ]) for status in order_statuses } order_count_data[provider.technical_name] = { 'awaiting': order_counts['placed'], 'preparing': order_counts['acknowledged'], 'done': order_counts['food_ready'] + order_counts['dispatched'] + order_counts['completed'] } return {'urbanpiper': order_count_data} def _get_active_delivery_providers(self): """ Fetch delivery providers for pos ui """ urbanpiper_providers = [] for provider in self.urbanpiper_delivery_provider_ids or []: urbanpiper_providers.append({ 'code': provider.technical_name, 'name': provider.name, 'image': provider.image_128, 'id': provider.id }) return urbanpiper_providers def _urbanpiper_handle_response(self, response_json, raise_exception=False): """ Handle response from urban piper """ title, msg_type, message = _('Urban Piper'), 'danger', '' if response_json.get('errors'): title = list(response_json.get('errors').keys())[0] message = list(response_json.get('errors').values())[0] elif response_json.get('message') or response_json.get('error_message'): message = response_json.get('message') or response_json.get('error_message') if message: if response_json.get('status') == 'success': msg_type = 'success' message = message.split('.')[0] elif response_json.get('status') == 'error': if raise_exception: raise ValidationError(message) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': title, 'message': message, 'type': msg_type, 'sticky': False, 'next': {'type': 'ir.actions.act_window_close'}, # force a form reload } } def prepare_store_data(self, data): self.ensure_one() return data def _check_required_request_params(self, store_required=True): msg = '' if not self.env.company.sudo().pos_urbanpiper_username: msg += _('UrbanPiper Username is required.\n') if not self.env.company.sudo().pos_urbanpiper_apikey: msg += _('UrbanPiper API Key is required.\n') if not self.urbanpiper_store_identifier and store_required: msg += _('UrbanPiper Store ID is required.\n') if msg: raise UserError(msg) def log_xml(self, xml_string, func, name='Urbanpiper Logger'): self.env.flush_all() db_name = self.env.cr.dbname try: with Registry(db_name).cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) IrLogging = env['ir.logging'] IrLogging.sudo().create({ 'name': name, 'type': 'server', 'dbname': db_name, 'level': 'DEBUG', 'message': xml_string, 'path': 'Urbanpiper', 'func': func, 'line': 1 }) except psycopg2.Error: pass def _reset_urbanpiper_product_linkages(self): """ Reset product linkage to Urbanpiper and products become available for syncing again. """ if linked_statuses := self.env['product.urban.piper.status'].search([ ('config_id', 'in', self.ids), ('is_product_linked', '=', True) ]): linked_statuses.write({'is_product_linked': False}) def get_urban_piper_provider_states(self): raw = self.env['ir.config_parameter'].sudo().get_param('pos_urban_piper.toggle_state') or "{}" config_state = json.loads(raw) config_state = config_state.get(str(self.id), {}) return config_state def set_urban_piper_provider_states(self, value_json): config_state = json.loads(self.env['ir.config_parameter'].sudo().get_param('pos_urban_piper.toggle_state', "{}")) config_state[str(self.id)] = json.loads(value_json or "{}") self.env['ir.config_parameter'].sudo().set_param('pos_urban_piper.toggle_state', json.dumps(config_state)) self._notify('URBAN_PIPER_PROVIDER_STATES', config_state[str(self.id)]) return config_state[str(self.id)] def get_urbanpiper_special_products(self): return [ self.env.ref("pos_urban_piper.product_other_charges", raise_if_not_found=False), self.env.ref("pos_urban_piper.product_delivery_charges", raise_if_not_found=False), self.env.ref("pos_urban_piper.product_packaging_charges", raise_if_not_found=False), ]