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.template', 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, ) # ------------------------- 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 %s at API url %s', pricer_tag_id, unlink_tag_url)) def action_button_update_pricer_tags(self): """ Action assigned to buttons which allow the user to manually call Pricer API self here contains the recordset of selected Pricer Stores in their list view """ # Recover arguments from xml context self._update_pricer_tags(self.env.context.get('update_all') or False) 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 """ # Go through every Pricer store and build a JSON request body for it with requests.Session() as requests_session: for pricer_store in self: create_or_update_request_body = [] link_request_body = [] for product_id in pricer_store.product_ids: # Check if the product needs to be created / update on Pricer db if product_id.pricer_product_to_create_or_update or update_all: create_or_update_request_body.append(product_id._get_create_or_update_body()) product_id.pricer_product_to_create_or_update = False for pricer_tag_id in product_id.pricer_tag_ids: # Check if the pricer tag needs to be linked to a product if pricer_tag_id.pricer_product_to_link or update_all: link_request_body.append(pricer_tag_id._get_link_body()) pricer_tag_id.pricer_product_to_link = False # If needed, call the Pricer api to update/create products and link the price tags if create_or_update_request_body or link_request_body: try: setup_requests_session(requests_session, pricer_store.pricer_login, pricer_store.pricer_password, pricer_store.auth_url) # Create / Update products at Pricer database if create_or_update_request_body: response = requests_session.patch(pricer_store.create_or_update_products_url, json=create_or_update_request_body, timeout=PRICER_REQUESTS_TIMEOUT) response.raise_for_status() _logger.info("Succesfully created/updated products information for %s at Pricer API url: %s", pricer_store.name, pricer_store.create_or_update_products_url) # Link the new Pricer tags to the existing products on their database if link_request_body: response = requests_session.patch(pricer_store.link_tags_url, json=link_request_body, timeout=PRICER_REQUESTS_TIMEOUT) response.raise_for_status() _logger.info("Succesfully linked Pricer labels to products for %s at Pricer API URL: %s", pricer_store.name, pricer_store.link_tags_url) pricer_store.last_update_status_message = _("Update successfully sent to Pricer") except requests.exceptions.RequestException as e: _logger.warning(e) if e.response: pricer_store.last_update_status_message = _("Error: %s - %s", e.response.status_code, e.response.reason) else: pricer_store.last_update_status_message = _("Error: check Pricer credentials") finally: pricer_store.last_update_datetime = fields.Datetime.now()