# -*- coding: utf-8 -*- import logging from odoo import http, _ from odoo.http import request from odoo.osv.expression import AND from odoo.tools import format_amount from odoo.addons.account.controllers.portal import PortalAccount from datetime import timedelta, datetime _logger = logging.getLogger(__name__) class PosController(PortalAccount): @http.route(['/pos/web', '/pos/ui'], type='http', auth='user') def pos_web(self, config_id=False, **k): """Open a pos session for the given config. The right pos session will be selected to open, if non is open yet a new session will be created. /pos/ui and /pos/web both can be used to access the POS. On the SaaS, /pos/ui uses HTTPS while /pos/web uses HTTP. :param debug: The debug mode to load the session in. :type debug: str. :param config_id: id of the config that has to be loaded. :type config_id: str. :returns: object -- The rendered pos session. """ is_internal_user = request.env.user.has_group('base.group_user') if not is_internal_user: return request.not_found() domain = [ ('state', 'in', ['opening_control', 'opened']), ('user_id', '=', request.session.uid), ('rescue', '=', False) ] if config_id: domain = AND([domain,[('config_id', '=', int(config_id))]]) pos_config = request.env['pos.config'].sudo().browse(int(config_id)) pos_session = request.env['pos.session'].sudo().search(domain, limit=1) # The same POS session can be opened by a different user => search without restricting to # current user. Note: the config must be explicitly given to avoid fallbacking on a random # session. if not pos_session and config_id: domain = [ ('state', 'in', ['opening_control', 'opened']), ('rescue', '=', False), ('config_id', '=', int(config_id)), ] pos_session = request.env['pos.session'].sudo().search(domain, limit=1) if not pos_session or config_id and not pos_config.active: return request.redirect('/web#action=point_of_sale.action_client_pos_menu') # The POS only works in one company, so we enforce the one of the session in the context company = pos_session.company_id session_info = request.env['ir.http'].session_info() session_info['user_context']['allowed_company_ids'] = company.ids session_info['user_companies'] = {'current_company': company.id, 'allowed_companies': {company.id: session_info['user_companies']['allowed_companies'][company.id]}} session_info['nomenclature_id'] = pos_session.company_id.nomenclature_id.id session_info['fallback_nomenclature_id'] = pos_session._get_pos_fallback_nomenclature_id() context = { 'session_info': session_info, 'login_number': pos_session.login(), 'pos_session_id': pos_session.id, } response = request.render('point_of_sale.index', context) response.headers['Cache-Control'] = 'no-store' return response @http.route('/pos/ui/tests', type='http', auth="user") def test_suite(self, mod=None, **kwargs): domain = [ ('state', '=', 'opened'), ('user_id', '=', request.session.uid), ('rescue', '=', False) ] pos_session = request.env['pos.session'].sudo().search(domain, limit=1) session_info = request.env['ir.http'].session_info() session_info['user_context']['allowed_company_ids'] = pos_session.company_id.ids context = { 'session_info': session_info, 'pos_session_id': pos_session.id, } return request.render('point_of_sale.qunit_suite', qcontext=context) @http.route('/pos/sale_details_report', type='http', auth='user') def print_sale_details(self, date_start=False, date_stop=False, **kw): r = request.env['report.point_of_sale.report_saledetails'] pdf, _ = request.env['ir.actions.report'].with_context(date_start=date_start, date_stop=date_stop)._render_qweb_pdf('point_of_sale.sale_details_report', r) pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] return request.make_response(pdf, headers=pdfhttpheaders) @http.route(['/pos/ticket'], type='http', auth="public", website=True, sitemap=False) def invoice_request_screen(self, **kwargs): errors = {} form_values = {} if request.httprequest.method == 'POST': for field in ['pos_reference', 'date_order', 'ticket_code']: if not kwargs.get(field): errors[field] = " " else: form_values[field] = kwargs.get(field) if errors: errors['generic'] = _("Please fill all the required fields.") elif len(form_values['pos_reference']) < 14: errors['pos_reference'] = _("The Ticket Number should be at least 14 characters long.") else: date_order = datetime(*[int(i) for i in form_values['date_order'].split('-')]) order = request.env['pos.order'].sudo().search([ ('pos_reference', '=like', '%' + form_values['pos_reference'].replace('%', r'\%').replace('_', r'\_')), ('date_order', '>=', date_order), ('date_order', '<', date_order + timedelta(days=1)), ('ticket_code', '=', form_values['ticket_code']), ], limit=1) if order: return request.redirect('/pos/ticket/validate?access_token=%s' % (order.access_token)) else: errors['generic'] = _("No sale order found.") return request.render("point_of_sale.ticket_request_with_code", { 'errors': errors, 'banner_error': " ".join(errors.values()), 'form_values': form_values, }) @http.route(['/pos/ticket/validate'], type='http', auth="public", website=True, sitemap=False) def show_ticket_validation_screen(self, access_token='', **kwargs): def _parse_additional_values(fields, prefix, kwargs): """ Parse the values in the kwargs by extracting the ones matching the given fields name. :return a dict with the parsed value and the field name as key, and another on with the prefix to re-render the form with previous values if needed. """ res, res_prefixed = {}, {} for field in fields: key = prefix + field.name if key in kwargs: val = kwargs.pop(key) res[field.name] = val res_prefixed[key] = val return res, res_prefixed # If the route is called directly, return a 404 if not access_token: return request.not_found() # Get the order using the access token. We can't use the id in the route because we may not have it yet when the QR code is generated. pos_order = request.env['pos.order'].sudo().search([('access_token', '=', access_token)]) if not pos_order: return request.not_found() # Set the proper context in case of unauthenticated user accessing # from the main company website pos_order = pos_order.with_company(pos_order.company_id) # If the order was already invoiced, return the invoice directly by forcing the access token so that the non-connected user can see it. if pos_order.account_move and pos_order.account_move.is_sale_document(): return request.redirect('/my/invoices/%s?access_token=%s' % (pos_order.account_move.id, pos_order.account_move._portal_ensure_token())) # Get the optional extra fields that could be required for a localisation. pos_order_country = pos_order.company_id.account_fiscal_country_id additional_partner_fields = request.env['res.partner'].get_partner_localisation_fields_required_to_invoice(pos_order_country) additional_invoice_fields = request.env['account.move'].get_invoice_localisation_fields_required_to_invoice(pos_order_country) user_is_connected = not request.env.user._is_public() # Validate the form by ensuring required fields are filled and the VAT is correct. form_values = {'error': {}, 'error_message': {}, 'extra_field_values': {}} if kwargs and request.httprequest.method == 'POST': form_values.update(kwargs) # Extract the additional fields values from the kwargs now as they can't be there when validating the 'regular' partner form. partner_values, prefixed_partner_values = _parse_additional_values(additional_partner_fields, 'partner_', kwargs) form_values['extra_field_values'].update(prefixed_partner_values) # Do the same for invoice values, separately as they are only needed for the invoice creation. invoice_values, prefixed_invoice_values = _parse_additional_values(additional_invoice_fields, 'invoice_', kwargs) form_values['extra_field_values'].update(prefixed_invoice_values) # Check the basic form fields if the user is not connected as we will need these information to create the new user. if not user_is_connected: error, error_message = self.details_form_validate(kwargs, partner_creation=True) else: # Check that the billing information of the user are filled. error, error_message = {}, [] partner = request.env.user.partner_id for field in self.MANDATORY_BILLING_FIELDS: if not partner[field]: error[field] = 'error' error_message.append(_('The %s must be filled in your details.', request.env['ir.model.fields']._get('res.partner', field).field_description)) # Check that the "optional" additional fields are filled. error, error_message = self.extra_details_form_validate(partner_values, additional_partner_fields, error, error_message) error, error_message = self.extra_details_form_validate(invoice_values, additional_invoice_fields, error, error_message) if not error: return self._get_invoice(partner_values, invoice_values, pos_order, additional_invoice_fields, kwargs) else: form_values.update({'error': error, 'error_message': error_message}) elif user_is_connected: return self._get_invoice({}, {}, pos_order, additional_invoice_fields, kwargs) # Most of the time, the country of the customer will be the same as the order. We can prefill it by default with the country of the company. if 'country_id' not in form_values: form_values['country_id'] = pos_order_country.id partner = request.env['res.partner'] # Prefill the customer extra values if there is any and an user is connected partner = (user_is_connected and request.env.user.partner_id) or pos_order.partner_id if partner: if additional_partner_fields: form_values['extra_field_values'] = {'partner_' + field.name: partner[field.name] for field in additional_partner_fields if field.name not in form_values['extra_field_values']} # This is just to ensure that the user went and filled its information at least once. # Another more thorough check is done upon posting the form. if not partner.country_id or not partner.street: form_values['partner_address'] = False else: form_values['partner_address'] = partner._display_address() return request.render("point_of_sale.ticket_validation_screen", { 'partner': partner, 'address_url': f'/my/account?redirect=/pos/ticket/validate?access_token={access_token}', 'user_is_connected': user_is_connected, 'format_amount': format_amount, 'env': request.env, 'countries': request.env['res.country'].sudo().search([]), 'states': request.env['res.country.state'].sudo().search([]), 'partner_can_edit_vat': True, 'pos_order': pos_order, 'invoice_required_fields': additional_invoice_fields, 'partner_required_fields': additional_partner_fields, 'access_token': access_token, **form_values, }) def _get_invoice(self, partner_values, invoice_values, pos_order, additional_invoice_fields, kwargs): # If the user is not connected, then we will simply create a new partner with the form values. # Matching with existing partner was tried, but we then can't update the values, and it would force the user to use the ones from the first invoicing. if request.env.user._is_public() and not pos_order.partner_id.id: partner_values.update({key: kwargs[key] for key in self.MANDATORY_BILLING_FIELDS}) partner_values.update({key: kwargs[key] for key in self.OPTIONAL_BILLING_FIELDS if key in kwargs}) for field in {'country_id', 'state_id'} & set(partner_values.keys()): try: partner_values[field] = int(partner_values[field]) except Exception: partner_values[field] = False partner_values.update({'zip': partner_values.pop('zipcode', '')}) partner = request.env['res.partner'].sudo().create(partner_values) # In this case, partner_values contains the whole partner info form. # If the user is connected, then we can update if needed its fields with the additional localized fields if any, then proceed. else: partner = pos_order.partner_id or (not request.env.user._is_public() and request.env.user.partner_id) partner.write(partner_values) # In this case, partner_values only contains the additional fields that can be updated. pos_order.partner_id = partner # Get the required fields for the invoice and add them to the context as default values. with_context = {} for field in additional_invoice_fields: with_context.update({f'default_{field.name}': invoice_values.get(field.name)}) # Allowing default values for moves is important for some localizations that would need specific fields to be set on the invoice, such as Mexico. pos_order.with_context(with_context).action_pos_order_invoice() return request.redirect('/my/invoices/%s?access_token=%s' % (pos_order.account_move.id, pos_order.account_move._portal_ensure_token()))