diff --git a/delivery_dhl_parcel_de/README.rst b/delivery_dhl_parcel_de/README.rst index abd263d8..f9ea7428 100644 --- a/delivery_dhl_parcel_de/README.rst +++ b/delivery_dhl_parcel_de/README.rst @@ -15,34 +15,30 @@ Note that shipping rates are not retrieved from DHL but need to be configured. Configuration ------------- -Company -^^^^^^^ - -1. "Settings" -> "Users & Companies" -> "Companies" -2. Select or create a company -3. "DHL Parcel DE Configuration" tab -> set the "Use DHL Parcel DE Shipping Provider" as active -4. Fill out the necessary information. The information below is only meant for testing purposes: - - * DHL API URL: ``https://api-sandbox.dhl.com`` - * DHL UserId: ``user-valid`` - * DHL Password: ``SandboxPasswort2023!`` - * DHL API Key: Add your own API key here - * DHL Tracking URL: (optional) Leave empty or use: ``https://www.dhl.de/en/privatkunden/pakete-empfangen/verfolgen.html?piececode=`` - -5. Save your changes - Create Delivery Methods ^^^^^^^^^^^^^^^^^^^^^^^ 1. "Inventory" -> "Configuration" -> "Delivery" -> "Delivery Methods" 2. Select or create a method 3. Set the "Provider" to "DHL Parcel DE" -4. Fill out the necessary information. The information below is only meant as an example: +4. Select or create an 'Account'. + 1. Choose the Delivery Type 'DHL Parcel DE' in the Account form view to display the DHL fields + + 2. Fill in the necessary information in the 'Account' form. The values below are for testing purposes only: + + * DHL API URL: ``https://api-sandbox.dhl.com`` + * DHL UserId: ``user-valid`` + * DHL Password: ``SandboxPasswort2023!`` + * DHL Account number: ``3333333333`` (sandbox account) + * DHL API Key: Add your own API key here + * DHL API Secret: Add your own API secret here + * DHL Tracking URL: (optional) Leave empty or use: ``https://www.dhl.de/en/privatkunden/pakete-empfangen/verfolgen.html?piececode=`` + +5. Fill out the necessary information. The information below is only meant as an example: * Company: Company Name * DHL Weight UOM: ``KG`` * DHL Services Name: ``V53WPAK-DHL Paket`` - * DHL Account number: ``3333333333`` (sandbox account) * DHL Procedure number: ``01`` * DHL Participation number: ``02`` * DHL Package Info: Create package type @@ -60,7 +56,7 @@ Create Delivery Methods 3. Save your changes -5. Save your changes +6. Save your changes Testing diff --git a/delivery_dhl_parcel_de/__manifest__.py b/delivery_dhl_parcel_de/__manifest__.py index 8e7ce40a..63b80d19 100644 --- a/delivery_dhl_parcel_de/__manifest__.py +++ b/delivery_dhl_parcel_de/__manifest__.py @@ -1,7 +1,7 @@ { "name": "DHL Parcel (Post & Parcel Germany) Integration", "category": "Website", - "version": "18.0.1.0.0", + "version": "18.0.1.0.2", "summary": """ Create DHL Parce DE shipments from Odoo, update tracking information in Odoo from DHL, @@ -10,12 +10,14 @@ "license": "AGPL-3", "depends": [ "stock_delivery", + "delivery_carrier_account", "base_iso3166", "stock_picking_declared_value", ], "data": [ + "data/data.xml", "data/ir_cron.xml", - "views/res_company.xml", + "views/carrier_account_views.xml", "views/delivery_carrier.xml", "views/stock_picking.xml", ], diff --git a/delivery_dhl_parcel_de/data/data.xml b/delivery_dhl_parcel_de/data/data.xml new file mode 100644 index 00000000..55a4d724 --- /dev/null +++ b/delivery_dhl_parcel_de/data/data.xml @@ -0,0 +1,45 @@ + + + DHL Parcel DE + dhl_parcel_de_provider + 5.00 + 5.00 + 5.00 + 0.50 + 10.00 + DHL-Paket + + + + DHL Parcel DE Account + dhl_parcel_de_provider + 3333333333 + dummy + + + + DHL Parcel + dhl_parcel_de_provider + rate_and_ship + + 15 + + + kg + V01PAK + + DHL Parcel + 01 + 01 + + RETURN + PDF + OTHER + + diff --git a/delivery_dhl_parcel_de/data/ir_cron.xml b/delivery_dhl_parcel_de/data/ir_cron.xml index 73d5db84..52c254e8 100644 --- a/delivery_dhl_parcel_de/data/ir_cron.xml +++ b/delivery_dhl_parcel_de/data/ir_cron.xml @@ -1,11 +1,14 @@ - + DHL Parcel DE Authentication Process 10 minutes - + model.dhl_parcel_de_get_access_token_cron() code diff --git a/delivery_dhl_parcel_de/data/neutralize.sql b/delivery_dhl_parcel_de/data/neutralize.sql index 88b64cdb..1f88cd45 100644 --- a/delivery_dhl_parcel_de/data/neutralize.sql +++ b/delivery_dhl_parcel_de/data/neutralize.sql @@ -1,12 +1,12 @@ --- remove DHL credentials and DHL account number on delivery carriers -UPDATE res_company - SET dhl_userid = NULL, - dhl_password = NULL, +UPDATE carrier_account + SET account = '3333333333', + dhl_userid = NULL, + password = 'SandboxPasswort2023!', dhl_api_key = NULL, dhl_api_secret = NULL, dhl_access_token = NULL, - dhl_parcel_de_api_url = 'https://api-sandbox.dhl.com'; + dhl_parcel_de_api_url = 'https://api-sandbox.dhl.com' + WHERE delivery_type = 'dhl_parcel_de_provider'; UPDATE delivery_carrier - SET dhl_account_no = NULL, - prod_environment = false + SET prod_environment = false WHERE delivery_type = 'dhl_parcel_de_provider'; diff --git a/delivery_dhl_parcel_de/migrations/18.0.1.0.2/post-migration.py b/delivery_dhl_parcel_de/migrations/18.0.1.0.2/post-migration.py new file mode 100644 index 00000000..ae13e76b --- /dev/null +++ b/delivery_dhl_parcel_de/migrations/18.0.1.0.2/post-migration.py @@ -0,0 +1,108 @@ +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Transfer DHL data from res.company to carrier.account""" + _logger.info("Starting post-migration: Transferring DHL data to carrier.account") + + env = api.Environment(cr, SUPERUSER_ID, {}) + + # Validate prerequisites + if "carrier.account" not in env: + return _logger.warning("carrier.account model not found, skipping migration") + + cr.execute( + "SELECT EXISTS (" + "SELECT FROM information_schema.tables " + "WHERE table_name = 'temp_dhl_company_data'" + ")" + ) + if not cr.fetchone()[0]: + return _logger.error("Temporary table 'temp_dhl_company_data' does not exist") + + # Get the DHL delivery carrier(s) + dhl_carrier = env.ref("delivery_dhl_parcel_de.dhl_parcel_delivery_type", False) + if not dhl_carrier: + return _logger.warning("DHL delivery carrier not found, skipping migration") + _logger.info(f"Found {len(dhl_carrier)} DHL carrier") + + # Get company data + cr.execute("SELECT * FROM temp_dhl_company_data") + companies_data = cr.dictfetchall() + if not companies_data: + return _logger.info("No company data found in temporary table") + + # Get carrier.account fields + cr.execute( + "SELECT column_name " + "FROM information_schema.columns " + "WHERE table_name = 'carrier_account'" + ) + carrier_fields = {row[0] for row in cr.fetchall()} + + field_mapping = [ + "dhl_parcel_de_api_url", + "dhl_userid", + "password", + "dhl_api_key", + "dhl_api_secret", + "dhl_tracking_url", + "dhl_access_token", + ] + + success = failed = 0 + + for data in companies_data: + try: + company_id = data.pop("company_id") + + existing = env.ref("delivery_dhl_parcel_de.dhl_carrier_account") + if ( + existing + and existing.dhl_userid + and existing.password + and existing.dhl_api_key + ): + _logger.info("Already migrated") + continue + + account_vals = { + "company_id": company_id, + "carrier_id": dhl_carrier.id, + **{ + k: v + for k, v in data.items() + if v and k in carrier_fields and k in field_mapping + }, + } + + if existing: + update_vals = { + k: v + for k, v in account_vals.items() + if k not in ["company_id", "carrier_id"] + } + if update_vals: + existing.write(update_vals) + success += 1 + else: + env["carrier.account"].create(account_vals) + success += 1 + + except Exception as e: + failed += 1 + _logger.error( + f"Error processing company {data.get('company_id', 'unknown')}: {e}" + ) + + _logger.info(f"Migration completed: {success} successful, {failed} failed") + + # Archive temp table + cr.execute( + "ALTER TABLE IF EXISTS temp_dhl_company_data " + "RENAME TO temp_dhl_company_data_archived" + ) diff --git a/delivery_dhl_parcel_de/migrations/18.0.1.0.2/pre-migration.py b/delivery_dhl_parcel_de/migrations/18.0.1.0.2/pre-migration.py new file mode 100644 index 00000000..33c8fc9a --- /dev/null +++ b/delivery_dhl_parcel_de/migrations/18.0.1.0.2/pre-migration.py @@ -0,0 +1,92 @@ +import logging + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + """Store res.company DHL data before carrier.account exists""" + _logger.info("Starting pre-migration: Storing DHL data from res.company") + + # First, check if the fields exist in res.company + cr.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'res_company' + AND column_name IN ( + 'use_dhl_parcel_de_shipping_provider', + 'dhl_parcel_de_api_url', + 'dhl_userid', + 'dhl_password', + 'dhl_api_key', + 'dhl_api_secret', + 'dhl_tracking_url', + 'dhl_access_token' + ) + """) + + existing_columns = [row[0] for row in cr.fetchall()] + if existing_columns: + _logger.info(f"Found columns in res.company: {existing_columns}") + # Create a temporary table to store the data + # Using IF NOT EXISTS to avoid errors if script runs multiple times + cr.execute(""" + CREATE TEMP TABLE IF NOT EXISTS temp_dhl_company_data ( + company_id INTEGER, + use_dhl_parcel_de_shipping_provider BOOLEAN, + dhl_parcel_de_api_url VARCHAR, + dhl_userid VARCHAR, + password VARCHAR, + dhl_api_key VARCHAR, + dhl_api_secret VARCHAR, + dhl_tracking_url VARCHAR, + dhl_access_token VARCHAR + ) + """) + + # Clear existing data in temp table (in case of re-run) + cr.execute("DELETE FROM temp_dhl_company_data") + + # Insert data from res.company + cr.execute(""" + INSERT INTO temp_dhl_company_data ( + company_id, + use_dhl_parcel_de_shipping_provider, + dhl_parcel_de_api_url, + dhl_userid, + password, + dhl_api_key, + dhl_api_secret, + dhl_tracking_url, + dhl_access_token + ) + SELECT + id as company_id, + use_dhl_parcel_de_shipping_provider, + dhl_parcel_de_api_url, + dhl_userid, + dhl_password, + dhl_api_key, + dhl_api_secret, + dhl_tracking_url, + dhl_access_token + FROM res_company + WHERE use_dhl_parcel_de_shipping_provider = true + """) + + # Get count of records migrated + cr.execute("SELECT COUNT(*) FROM temp_dhl_company_data") + count = cr.fetchone()[0] + + _logger.info( + f"Successfully stored {count} companies with DHL enabled in temporary table" + ) + + # Optional: Verify the data was stored correctly + if count > 0: + cr.execute( + "SELECT company_id, dhl_userid FROM temp_dhl_company_data LIMIT 5" + ) + sample_data = cr.fetchall() + _logger.info(f"Sample data from temp table: {sample_data}") + else: + _logger.warning("Not found columns in res.company") diff --git a/delivery_dhl_parcel_de/models/__init__.py b/delivery_dhl_parcel_de/models/__init__.py index 482467fd..c19b3fce 100644 --- a/delivery_dhl_parcel_de/models/__init__.py +++ b/delivery_dhl_parcel_de/models/__init__.py @@ -1,4 +1,4 @@ from . import delivery_carrier from . import package_details -from . import res_company +from . import carrier_account from . import stock_picking diff --git a/delivery_dhl_parcel_de/models/res_company.py b/delivery_dhl_parcel_de/models/carrier_account.py similarity index 67% rename from delivery_dhl_parcel_de/models/res_company.py rename to delivery_dhl_parcel_de/models/carrier_account.py index ccaa3e5d..098cfbea 100644 --- a/delivery_dhl_parcel_de/models/res_company.py +++ b/delivery_dhl_parcel_de/models/carrier_account.py @@ -4,15 +4,9 @@ from odoo.exceptions import ValidationError -class ResCompany(models.Model): - _inherit = "res.company" +class CarrierAccount(models.Model): + _inherit = "carrier.account" - use_dhl_parcel_de_shipping_provider = fields.Boolean( - copy=False, - string="Use DHL Parcel DE Shipping Provider", - help="If use DHL Parcel DE shipping provider than value set TRUE.", - default=False, - ) dhl_parcel_de_api_url = fields.Char( string="DHL API URL", copy=False, default="https://api-sandbox.dhl.com" ) @@ -22,13 +16,6 @@ class ResCompany(models.Model): help="When use the sandbox account developer id use as the userId." "When use the live account application id use as the userId.", ) - dhl_password = fields.Char( - "DHL Password", - copy=False, - help="When use the sandbox account developer portal password use " - "to as the password.When use the live account application " - "token use to as the password.", - ) dhl_api_key = fields.Char( "DHL API Key", copy=False, @@ -39,12 +26,6 @@ class ResCompany(models.Model): copy=False, help="Obtained via Get Access! (app creation) and manually approved by DHL.", ) - dhl_tracking_url = fields.Char( - "DHL Tracking URL", - copy=False, - default="https://www.dhl.de/en/privatkunden/pakete-empfangen/verfolgen.html?piececode=", - help="Obtained via Get Access! (app creation) and manually approved by DHL.", - ) dhl_access_token = fields.Char( "DHL Access Token", copy=False, @@ -60,7 +41,7 @@ def dhl_parcel_de_get_access_token(self): payload = { "grant_type": "password", "username": self.dhl_userid, - "password": self.dhl_password, + "password": self.password, "client_id": self.dhl_api_key, "client_secret": self.dhl_api_secret, } @@ -87,14 +68,14 @@ def dhl_parcel_de_get_access_token(self): } } else: - raise ValidationError(response_data) # noqa: B904 + raise ValidationError(str(response_data)) # noqa: B904 else: raise ValidationError(response.text) except Exception as e: - raise ValidationError(e) # noqa: B904 + raise ValidationError(str(e)) # noqa: B904 def dhl_parcel_de_get_access_token_cron(self): - for company_id in self.search( - [("use_dhl_parcel_de_shipping_provider", "=", True)] + for carrier_account_id in self.search( + [("delivery_type", "=", "dhl_parcel_de_provider")] ): - company_id.dhl_parcel_de_get_access_token() + carrier_account_id.dhl_parcel_de_get_access_token() diff --git a/delivery_dhl_parcel_de/models/delivery_carrier.py b/delivery_dhl_parcel_de/models/delivery_carrier.py index 2f6e7c28..f8ce2aef 100644 --- a/delivery_dhl_parcel_de/models/delivery_carrier.py +++ b/delivery_dhl_parcel_de/models/delivery_carrier.py @@ -36,11 +36,6 @@ class DeliveryCarrier(models.Model): string="Product Name", help="Shipping Services those are accepted by DHL.", ) - dhl_account_no = fields.Char( - string="DHL Account Number", - help="The Account(EKP) number sent to you by DHL and it must " - "be maximum 10 digit allow.", - ) dhl_procedure_no = fields.Char( string="DHL Procedure Number", help="The Procedure refers to DHL products that are used for " @@ -86,6 +81,12 @@ class DeliveryCarrier(models.Model): dhl_return_receiver_id = fields.Char( string="Receiver ID", help="The receiver id of the return shipment." ) + dhl_tracking_url = fields.Char( + "DHL Tracking URL", + copy=False, + default="https://www.dhl.de/en/privatkunden/pakete-empfangen/verfolgen.html?piececode=", + help="Obtained via Get Access! (app creation) and manually approved by DHL.", + ) def _calculate_package_insurance(self, picking, package_weight, total_weight): """ @@ -159,7 +160,7 @@ def prepare_product_data_request(self, picking): product_request = { "itemDescription": rec.product_id.name, - "packagedQuantity": rec.qty_done, + "packagedQuantity": rec.quantity, "itemValue": { "currency": self.company_id and self.company_id.currency_id @@ -249,7 +250,7 @@ def dhl_parcel_de_provider_rate_shipment(self, order): "warning_message": False, } - def dhl_parcel_de_provider_retrive_package_info(self, picking, insurance_value): + def dhl_parcel_de_provider_get_package_info(self, picking, insurance_value): shipper_address_id = ( picking.picking_type_id and picking.picking_type_id.warehouse_id @@ -287,7 +288,9 @@ def dhl_parcel_de_provider_retrive_package_info(self, picking, insurance_value): receiver_phone = recipient_address_id.phone or "" receiver_email = recipient_address_id.email or "" billingNumber = ( - self.dhl_account_no + self.dhl_procedure_no + self.dhl_participation_no + self.carrier_account_id.account + + self.dhl_procedure_no + + self.dhl_participation_no ) package_data = { @@ -441,7 +444,7 @@ def dhl_parcel_de_provider_packages(self, picking): package_data = self.create_dhl_de_package_dict( height, length, width, weight ) - request_data = self.dhl_parcel_de_provider_retrive_package_info( + request_data = self.dhl_parcel_de_provider_get_package_info( picking, insurance_value ) package_data.update(request_data) @@ -501,15 +504,20 @@ def dhl_parcel_de_provider_send_shipping(self, picking): else "", ) ) + if not self.carrier_account_id: + raise ValidationError( + _("Carrier account is not set for DHL delivery method.") + ) + packages = self.dhl_parcel_de_provider_packages(picking) request_data = json.dumps({"shipments": packages}) try: header = { "accept": "application/json", "Content-Type": "application/json", - "Authorization": f"Bearer {self.company_id.dhl_access_token}", + "Authorization": f"Bearer {self.carrier_account_id.dhl_access_token}", } - api_url = f"{self.company_id.dhl_parcel_de_api_url}/parcel/de/shipping/v2/orders?docFormat={self.dhl_document_format}" # noqa: E501 + api_url = f"{self.carrier_account_id.dhl_parcel_de_api_url}/parcel/de/shipping/v2/orders?docFormat={self.dhl_document_format}" # noqa: E501 request_type = "POST" ( response_status, @@ -580,9 +588,9 @@ def dhl_parcel_de_provider_send_shipping(self, picking): raise ValidationError(e) from e def dhl_parcel_de_provider_cancel_shipment(self, picking): - company_id = self.company_id + carrier_account_id = self.carrier_account_id try: - api_url = f"{self.company_id.dhl_parcel_de_api_url}/parcel/de/shipping/v2/orders?profile=STANDARD_GRUPPENPROFIL" # noqa: E501 + api_url = f"{carrier_account_id.dhl_parcel_de_api_url}/parcel/de/shipping/v2/orders?profile=STANDARD_GRUPPENPROFIL" # noqa: E501 awb_numbers = picking.carrier_tracking_ref.split(",") for shipment in awb_numbers: api_url += f"&shipment={shipment}" @@ -590,7 +598,7 @@ def dhl_parcel_de_provider_cancel_shipment(self, picking): header = { "accept": "application/json", "Content-Type": "application/json", - "Authorization": f"Bearer {company_id.dhl_access_token}", + "Authorization": f"Bearer {carrier_account_id.dhl_access_token}", } request_type = "DELETE" ( @@ -609,9 +617,11 @@ def dhl_parcel_de_provider_cancel_shipment(self, picking): raise ValidationError(e) from e def dhl_parcel_de_provider_get_tracking_link(self, picking): - if self.company_id and self.company_id.dhl_tracking_url: - tracking_no = (picking.carrier_tracking_ref).split(",") - for number in tracking_no: - return f"{self.company_id.dhl_tracking_url}{number}" + if self.dhl_tracking_url: + # Return link only for the latest tracking number + tracking_no = picking.carrier_tracking_ref.split(",")[-1] + return f"{self.dhl_tracking_url}{tracking_no}" else: - raise ValidationError(_("Please Set Tracking URL In Company")) + raise ValidationError( + _("Please set tracking URL in DHL delivery method settings.") + ) diff --git a/delivery_dhl_parcel_de/models/stock_picking.py b/delivery_dhl_parcel_de/models/stock_picking.py index 6e00659d..c5e5bf69 100644 --- a/delivery_dhl_parcel_de/models/stock_picking.py +++ b/delivery_dhl_parcel_de/models/stock_picking.py @@ -22,7 +22,7 @@ def dhl_parcel_de_provider_return_shipment(self): """ try: carrier_id = self.carrier_id - company_id = carrier_id.company_id + carrier_account_id = carrier_id.carrier_account_id shipper_address_error = carrier_id.check_address_details( self.partner_id, ["zip", "city", "street"] ) @@ -70,9 +70,9 @@ def dhl_parcel_de_provider_return_shipment(self): ) headers = { "content-type": "application/json", - "Authorization": f"Bearer {company_id.dhl_access_token}", + "Authorization": f"Bearer {carrier_account_id.dhl_access_token}", } - api_url = f"{company_id.dhl_parcel_de_api_url}/parcel/de/shipping/returns/v1/orders?labelType=SHIPMENT_LABEL" # noqa: E501 + api_url = f"{carrier_account_id.dhl_parcel_de_api_url}/parcel/de/shipping/returns/v1/orders?labelType=SHIPMENT_LABEL" # noqa: E501 request_type = "POST" response_status, response_data = ( carrier_id.dhl_parcel_de_provider_create_shipment( diff --git a/delivery_dhl_parcel_de/views/carrier_account_views.xml b/delivery_dhl_parcel_de/views/carrier_account_views.xml new file mode 100644 index 00000000..8dad449a --- /dev/null +++ b/delivery_dhl_parcel_de/views/carrier_account_views.xml @@ -0,0 +1,48 @@ + + + + carrier.account.form.inherit.dhl + carrier.account + + + + + + + +