import requests import re import logging from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError _logger = logging.getLogger(__name__) PRICER_REQUESTS_TIMEOUT = 10 def setup_requests_session(requests_session, pricer_login, pricer_password, auth_url): """ Setup the jwt token for the authentification in requests_session """ try: # Get a new JWT token response = requests_session.get(auth_url, auth=(pricer_login, pricer_password), timeout=PRICER_REQUESTS_TIMEOUT) response.raise_for_status() jwt_token = response.json().get('token') requests_session.headers.update({'Authorization': f'Bearer {jwt_token}'}) except requests.exceptions.RequestException as e: _logger.warning("Failed to update the jwt token through Pricer API URL: %s: %s", auth_url, e) class PricerStore(models.Model): _name = 'pricer.store' _description = 'Pricer Store regrouping pricer tags' # The name of the Pricer store in Odoo name = fields.Char( string='Store Name', help='Pricer Store name in Odoo database', required=True ) # Fields used for Pricer API requests # All the 4 following fields need to be provided to clients by Pricer pricer_store_identifier = fields.Char( string='Pricer Store ID', help='Identifier of the store in the Pricer system', required=True ) pricer_tenant_name = fields.Char( string='Pricer Tenant Name', help='Your company identifier at Pricer', required=True ) pricer_login = fields.Char( string='Pricer Login', help='Login of your Pricer account', required=True ) pricer_password = fields.Char( string='Pricer Password', help='Password of your Pricer account', required=True ) # Products and Pricer tags associated to the Pricer Store product_ids = fields.One2many( comodel_name='product.product', inverse_name='pricer_store_id', string='Products', ) pricer_tag_ids = fields.One2many( comodel_name='pricer.tag', inverse_name='pricer_store_id', string='Pricer Tags', ) # Update status fields last_update_datetime = fields.Datetime( string="Last Update", help='Date and time of the last synchronization with Pricer', readonly=True, ) last_update_status_message = fields.Char( string='Last Update Status', help='Status message of the last synchronization with Pricer', readonly=True, ) # Dummy fields for quick pairing dummy_prod_barcode = fields.Char( string='Product barcode', help='Scan the product barcode here', store=False, ) dummy_tag_barcode = fields.Char( string='Pricer tag barcode', help='Scan the Pricer tag barcode here', store=False, ) # ------------------------- PRICER API URLs ------------------------- # For authentification: "https://central-manager.[PRICER_TENANT_NAME].pcm.pricer-plaza.com[PRICER_API_SUFFIX]" # For other requests: "https://[PRICER_STORE_NAME].[PRICER_TENANT_NAME].pcm.pricer-plaza.com[PRICER_API_SUFFIX]" # Example: "https://1234.odoo-be.pcm.pricer-plaza.com/api/public/core/v1/items" auth_url = fields.Char(compute='_compute_auth_url') create_or_update_products_url = fields.Char(compute='_compute_create_or_update_products_url') link_tags_url = fields.Char(compute='_compute_link_tags_url') # Their "compute" methods @api.depends('pricer_tenant_name') def _compute_auth_url(self): for record in self: record.auth_url = f'https://central-manager.{record.pricer_tenant_name}.pcm.pricer-plaza.com/api/public/auth/v1/login' @api.depends('pricer_store_identifier', 'pricer_tenant_name') def _compute_create_or_update_products_url(self): for record in self: record.create_or_update_products_url = f'https://{record.pricer_store_identifier}.{record.pricer_tenant_name}.pcm.pricer-plaza.com/api/public/core/v1/items' @api.depends('pricer_store_identifier', 'pricer_tenant_name') def _compute_link_tags_url(self): for record in self: record.link_tags_url = f'https://{record.pricer_store_identifier}.{record.pricer_tenant_name}.pcm.pricer-plaza.com/api/public/core/v1/labels' # ------------------------- CONSTRAINS ------------------------- @api.constrains('pricer_store_identifier') def _check_pricer_store_identifier(self): """ Pricer Store ID must: 1) Consist of: a-z, 0-9 or '-' 2) Must start with a-z or 0-9 """ for record in self: if not re.fullmatch(r'^[a-z0-9][a-z0-9-]*$', record.pricer_store_identifier): raise ValidationError(_("Pricer Store ID must only contain lowercase a-z, 0-9 or '-' and not start with '-'")) # ------------------------- API METHODS ------------------------- def unlink_label(self, pricer_tag_id): """ Stop displaying product infromation on a pricer tag when deleting it from Odoo database """ unlink_tag_url = f'https://{self.pricer_store_identifier}.{self.pricer_tenant_name}.pcm.pricer-plaza.com/api/public/core/v1/labels/{pricer_tag_id}/links' with requests.Session() as requests_session: try: setup_requests_session(requests_session, self.pricer_login, self.pricer_password, self.auth_url) response = requests_session.delete(unlink_tag_url, timeout=PRICER_REQUESTS_TIMEOUT) response.raise_for_status() _logger.info("Succesfully unlinked products from Pricer tag %s at Pricer API URL: %s", pricer_tag_id, unlink_tag_url) except requests.exceptions.RequestException as e: _logger.warning("Failed to unlink product from Pricer tag %s at Pricer API URL: %s: %s", pricer_tag_id, unlink_tag_url, e) raise UserError(_('Failed to unlink Pricer tag %(pricer_tag)s at API url %(api_url)s', pricer_tag=pricer_tag_id, api_url=unlink_tag_url)) def action_button_update_pricer_tags(self): """Manually call Pricer API to update products on Pricer tags""" updated = self._update_pricer_tags(update_all=False) message = ( self.last_update_status_message if updated else _('Everything is already up to date with Pricer. No update needed.') ) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'message': message, 'type': 'danger' if message.startswith(_('Error')) else 'info', }, } def _update_pricer_tags(self, update_all): """This method is regularly called by a cron in pricer module The interval is defined in data/pricer_ir_cron.xml This is done to avoid doing excessive API requests on every action while maintaining user's pricer tags synchronized with Odoo database A button on list view of pricer stores allows the user to manually call this method If called manually, instead of going through ALL the pricer stores in the database, it will only check the pricer stores specified in its "pricer_stores_to_update" argument :param bool update_all: only kept not to change the method signature """ anything_to_update = False # Go through every Pricer store and build a JSON request body for it with requests.Session() as requests_session: for store in self: products = store.product_ids.filtered("pricer_product_to_create_or_update") tags = products.pricer_tag_ids.filtered("pricer_product_to_link") if not products and not tags: continue # This store is up to date anything_to_update = True # Call the Pricer api to update/create products and link the price tags try: setup_requests_session(requests_session, store.pricer_login, store.pricer_password, store.auth_url) # Create / Update products at Pricer database if products: requests_session.patch( store.create_or_update_products_url, json=[p._get_create_or_update_body() for p in products], timeout=PRICER_REQUESTS_TIMEOUT, ).raise_for_status() products.write({'pricer_product_to_create_or_update': False}) _logger.info( "Successfully created/updated products information for %s at Pricer API url: %s", store.name, store.create_or_update_products_url, ) # Link the new Pricer tags to the existing products on their database if tags: requests_session.patch( store.link_tags_url, json=[t._get_link_body() for t in tags], timeout=PRICER_REQUESTS_TIMEOUT, ).raise_for_status() tags.write({'pricer_product_to_link': False}) _logger.info( "Successfully linked Pricer labels to products for %s at Pricer API URL: %s", store.name, store.link_tags_url, ) store.last_update_status_message = _("Update successfully sent to Pricer") except requests.exceptions.RequestException as e: _logger.warning(e) if e.response: store.last_update_status_message = _( "Error: %(code)s - %(reason)s", code=e.response.status_code, reason=e.response.reason, ) else: store.last_update_status_message = _("Error: check Pricer credentials") finally: store.last_update_datetime = fields.Datetime.now() return anything_to_update