def get_estimated_rates(from_address, to_address, package, doc, items, confirmation): from_postal_code = (from_address.get("pincode") or "").strip() from_country_code = get_country_code(from_address.get("country", "")) if from_country_code == "us": from_state = get_iso_3166_2_state_code(from_address) else: from_state = from_address.get("state") to_postal_code = (to_address.get("pincode") or "").strip() to_country_code = get_country_code(to_address.get("country", "")) if to_country_code == "us": to_state = get_iso_3166_2_state_code(to_address) else: to_state = to_address.get("state") data = { "carrier_id": frappe.conf.get("shipengine_fedex_carrier_id"), "from_country_code": from_country_code.upper(), "from_postal_code": from_postal_code, "from_city_locality": from_address.get("city"), "from_state_province": from_state, "to_country_code": to_country_code.upper(), "to_postal_code": to_postal_code, "to_city_locality": to_address.get("city"), "to_state_province": to_state, "weight": package.get("weight", {}), "confirmation": confirmation, "address_residential_indicator": "unknown", "ship_date": doc.get("delivery_date") if doc else None } url = "https://{base_url}/v1/rates/estimate".format( base_url=SHIPENGINE_BASE_URL) headers = { 'Host': SHIPENGINE_BASE_URL, 'API-Key': frappe.conf.get("shipengine_api_key"), 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=json.dumps(data)) rates = response.json() return rates
def get_shipper(delivery_note_name): shipper = RequestedShipment() delivery_note_company = frappe.db.get_value("Delivery Note", delivery_note_name, "company") if delivery_note_company: shipper.contact.PersonName = delivery_note_company shipper.contact.CompanyName = delivery_note_company company = frappe.db.get_values("Company", delivery_note_company, ["phone_no", "country"], as_dict=True) if company: shipper.contact.PhoneNumber = company[0].phone_no shipper.address.Country = company[0].country shipper.address.CountryCode = get_country_code( shipper.address.Country) shipper_address = None company_address = get_company_address( delivery_note_company).company_address if company_address: shipper_address = frappe.get_doc("Address", company_address) if shipper_address: shipper.address.StreetLines.append( shipper_address.address_line1) if shipper_address.address_line2: shipper.address.StreetLines.append( shipper_address.address_line2) shipper.address.City = shipper_address.city shipper.address.PostalCode = shipper_address.pincode shipper.address.StateOrProvinceCode = shipper_address.state return shipper
def get_rates(from_address, to_address, items=None, doc=None, packaging_type="YOUR_PACKAGING"): """Simple wrapper over fedex rating service. It takes the standard address field values for the from_ and to_ addresses to keep a consistent address api. """ # quick hack to package all items into one box for quick shipping quotations # packages = find_packages(items) packages = [] package = { "weight_value": 0, "weight_units": "LB", "physical_packaging": "BOX", "group_package_count": 0, "insured_amount": 0 } item_values = frappe.get_all( "Item", fields=["insured_declared_value", "name", "net_weight"]) item_values = {elem.pop("name"): elem for elem in item_values} if doc and not items: items = doc.get("items") # Set the item weights, quantity and insured amounts in the package(s). # For repairs, only process packages once for each warranty claim. processed_claims = [] weight_value = group_package_count = insured_amount = 0 for item in items: if item.get("warranty_claim") and item.get( "warranty_claim") not in processed_claims: repair_items = frappe.db.get_value("Warranty Claim", item.get("warranty_claim"), ["item_code", "cable", "case"]) repair_items = list(filter(None, repair_items)) group_package_count = len(repair_items) for repair_item in repair_items: weight_value += item_values.get(repair_item, {}).get("net_weight", 0) insured_amount += item_values.get(repair_item, {}).get( "insured_declared_value", 0) processed_claims.append(item.get("warranty_claim")) else: group_package_count += item.get("qty", 0) weight_value += item_values.get(item.get("item_code"), {}).get( "net_weight", 0) * item.get("qty", 0) insured_amount += item_values.get(item.get("item_code"), {}).get( "insured_declared_value", 0) * item.get("qty", 0) package["weight_value"] = max(1, ceil(weight_value)) package["group_package_count"] = group_package_count package["insured_amount"] = insured_amount packages.append(package) # to try and keep some form of standardization we'll minimally require # a weight_value. Any other values will be passed as is to the rates service. surcharge = 0 for package in packages: if package.get("weight_value") is None or package.get( "weight_units") is None: raise frappe.exceptions.ValidationError( "Missing weight_value data") # if not package.get("group_package_count"): # keep count on 1 as we don't care about package groups package["group_package_count"] = 1 if not package.get("insured_amount"): package["insured_amount"] = 0 if not package.get("physical_packaging"): package["physical_packaging"] = "BOX" surcharge = surcharge + package.get("surcharge", 0) # check item conditions for applying Fedex One Rate pricing rate_settings = frappe.get_single("Shipment Rate Settings") RecipientCountryCode = get_country_code(to_address.get("country", "")) flat_rate = False signature_option = "DIRECT" packaging = packaging_type if RecipientCountryCode.lower( ) == "us": # One Rate only applies for intra-US deliveries flat_rate_items = { item.item: item.max_qty for item in rate_settings.items } for item in items: if item.get("qty", 0) < flat_rate_items.get( item.get("item_code"), 0): flat_rate = True signature_option = None packaging = frappe.db.get_value( "Shipment Rate Item Settings", {"item": item.get("item_code")}, "packaging") packaging = frappe.db.get_value("Shipping Package", packaging, "box_code") else: flat_rate = False signature_option = "DIRECT" packaging = packaging_type break # form rate request arguments rate_exceptions = [] args = dict( DropoffType='REGULAR_PICKUP', PackagingType=packaging, EdtRequestType='NONE', PaymentType='SENDER', # Shipper ShipperPostalCode=(from_address.get("pincode") or "").strip(), ShipperCountryCode=get_country_code(from_address.get("country")), # Recipient RecipientPostalCode=(to_address.get("pincode") or "").strip(), IsResidential=to_address.get("is_residential"), RecipientCountryCode=RecipientCountryCode, # Delivery options package_list=packages, ignoreErrors=True, signature_option=signature_option, exceptions=rate_exceptions, delivery_date=doc.get("delivery_date", "") if doc else "", saturday_delivery=doc.get("saturday_delivery", "") if doc else "", flat_rate=flat_rate) if to_address: rates = get_fedex_packages_rate(**args) or [] # since we're working on v18 of Fedex's rate service, which is incompatible with # getting One Rate and non-One Rate prices in the same query, we do another query # to get the non-One Rate prices and update the existing rates if flat_rate: non_flat_rate_args = args.copy() non_flat_rate_args.update({ "flat_rate": False, "signature_option": "DIRECT", "PackagingType": packaging_type }) flat_rates = get_fedex_packages_rate(**non_flat_rate_args) or [] rates.extend(flat_rates) else: rates = [] if rates: sorted_rates = [] unique_labels = [] for rate in sorted(rates, key=lambda rate: rate["fee"]): # remove duplicate shipping methods if rate["label"] in unique_labels: continue # disallow FEDEX GROUND for Canada if RecipientCountryCode.lower( ) == "ca" and rate['label'] == "FEDEX GROUND": continue unique_labels.append(rate["label"]) rate["fee"] += surcharge if rate_settings.upcharge_type == "Percentage": rate["fee"] += (rate["fee"] * (rate_settings.upcharge / 100)) elif rate_settings.upcharge_type == "Actual": rate["fee"] += rate_settings.upcharge rate['fee'] = round(rate['fee'], 2) sorted_rates.append(rate) return sorted_rates else: msg = "Could not get rates, please check your Shipping Address" if len(rate_exceptions) > 0: for ex in rate_exceptions: if ex["type"] == "request": msg = str(ex["exception"]) break frappe.throw(msg, title="Error")
def get_shipengine_rates(from_address, to_address, items=None, doc=None, estimate=False): """ Return shipment rates from Fedex using the ShipEngine API. Args: from_address (dict): The shipper's address. to_address (dict): The reciever's address. items (list of dict, optional): The list of items to be shipped. Defaults to None. doc (dict): The order details. Defaults to None. estimate (bool, optional): True if estimated shipping rates are required, without any additional charges, such as surcharges, insurance, customs, etc., otherwise False to receive all data. Defaults to False. Returns: list of dict: Returns the list of rates based on the shipping address """ package = { "weight": { "value": 0, "unit": "pound" }, "insured_value": { "currency": "usd", "amount": 0 } } item_values = frappe.get_all( "Item", fields=["insured_declared_value", "name", "net_weight"]) item_values = {elem.pop("name"): elem for elem in item_values} if doc and not items: items = doc.get("items") # Set the item weights, quantity and insured amounts in the package(s). # For repairs, only process packages once for each warranty claim. processed_claims = [] weight_value = insured_amount = 0 for item in items: if item.get("warranty_claim") and item.get( "warranty_claim") not in processed_claims: repair_items = frappe.db.get_value("Warranty Claim", item.get("warranty_claim"), ["item_code", "cable", "case"]) repair_items = list(filter(None, repair_items)) for repair_item in repair_items: weight_value += item_values.get(repair_item, {}).get("net_weight", 0) insured_amount += item_values.get(repair_item, {}).get( "insured_declared_value", 0) processed_claims.append(item.get("warranty_claim")) else: weight_value += item_values.get(item.get("item_code"), {}).get( "net_weight", 0) * item.get("qty", 0) insured_amount += item_values.get(item.get("item_code"), {}).get( "insured_declared_value", 0) * item.get("qty", 0) package["weight"]["value"] = max(1, ceil(weight_value)) package["insured_value"]["amount"] = insured_amount or 0 # check item conditions for applying Fedex One Rate pricing rate_settings = frappe.get_single("Shipment Rate Settings") flat_rate = False confirmation = DEFAULT_CONFIRMATION_TYPE packaging = DEFAULT_FEDEX_PACKAGE to_country_code = get_country_code(to_address.get("country", "")) if to_country_code.lower( ) == "us": # One Rate only applies for intra-US deliveries flat_rate_items = { item.item: item.max_qty for item in rate_settings.items } for item in items: if item.get("qty", 0) < flat_rate_items.get( item.get("item_code"), 0): flat_rate = True shipping_package = frappe.db.get_value( "Shipment Rate Item Settings", {"item": item.get("item_code")}, "packaging") packaging = frappe.db.get_value("Shipping Package", shipping_package, "box_code") else: flat_rate = False packaging = DEFAULT_FEDEX_PACKAGE break # even if one item is not eligible for One Rate, break if flat_rate: confirmation = "none" packaging = packaging.lower() + "_onerate" package["package_code"] = packaging # Temporary fix: Do not use estimation for South Korean addresses as ShipEngine breaks on them. if to_country_code.lower() == "kr": estimate = False # make the request to ShipEngine if estimate: rates = get_estimated_rates(from_address, to_address, package, doc, items, confirmation) else: rates = get_shipping_rates(from_address, to_address, package, doc, items, confirmation) if isinstance(rates, dict) and rates.get("errors"): frappe.throw(rates.get("errors")) # process all the returned rates if not rates: frappe.throw( "Could not get rates, please re-check your shipping address.") shipping_rates = [] for rate in rates: # disallow FEDEX GROUND for Canada if to_country_code.lower() == "ca" and rate.get( "service_code") == "fedex_ground": continue fee = sum([(rate.get(rate_type) or {}).get("amount", 0.0) for rate_type in ("shipping_amount", "insurance_amount", "confirmation_amount", "other_amount")]) if rate_settings.upcharge_type == "Percentage": fee += (fee * (rate_settings.upcharge / 100)) elif rate_settings.upcharge_type == "Actual": fee += rate_settings.upcharge fee = round(fee, 2) shipping_rates.append({ "name": rate.get("service_code"), "label": rate.get("service_type"), "fee": fee, "days": rate.get("delivery_days"), "estimated_arrival": rate.get("carrier_delivery_days") }) shipping_rates.sort(key=lambda rate: rate.get("fee")) return shipping_rates
def get_shipping_rates(from_address, to_address, package, doc, items, confirmation): from_postal_code = (from_address.get("pincode") or "").strip() from_country_code = get_country_code(from_address.get("country", "")) if from_country_code == "us": from_state = get_iso_3166_2_state_code(from_address) else: from_state = from_address.get("state") to_postal_code = (to_address.get("pincode") or "").strip() to_country_code = get_country_code(to_address.get("country", "")) if to_country_code == "us": to_state = get_iso_3166_2_state_code(to_address) else: to_state = to_address.get("state") # build a list of items for international shipments customs_items = [] items = items or doc.get("items") for item in items: customs_items.append({ # TODO: future dev, please use a better description # this currently fails at trying to parse illegal # characters (double-quotes, etc.) "description": item.get("item_code")[:100], "quantity": int(item.get("qty")), "value": item.get("rate"), "country_of_origin": from_country_code.upper(), "sku": item.get("item_code") }) data = { "shipment": { "carrier_id": frappe.conf.get("shipengine_fedex_carrier_id"), "ship_date": doc.get("delivery_date") if doc else None, "ship_to": { "name": doc.get("customer_name"), "phone": to_address.get("phone"), "address_line1": to_address.get("address_line1"), "city_locality": to_address.get("city"), "state_province": to_state, "postal_code": to_postal_code, "country_code": to_country_code.upper(), "address_residential_indicator": "unknown" }, "ship_from": { "name": get_default_company(), "phone": from_address.get("phone"), "company_name": get_default_company(), "address_line1": from_address.get("address_line1"), "city_locality": from_address.get("city"), "state_province": from_state, "postal_code": from_postal_code, "country_code": from_country_code.upper(), "address_residential_indicator": "unknown" }, "packages": [package], "confirmation": confirmation, "customs": { "contents": "merchandise", "non_delivery": "return_to_sender", "customs_items": customs_items }, "insurance_provider": "third_party", "advanced_options": { "saturday_delivery": doc and doc.get("saturday_delivery") or False } } } url = "https://{base_url}/v1/rates".format(base_url=SHIPENGINE_BASE_URL) headers = { 'Host': SHIPENGINE_BASE_URL, 'API-Key': frappe.conf.get("shipengine_api_key"), 'Content-Type': 'application/json' } response = requests.post(url, headers=headers, data=json.dumps(data)) response = response.json() # throw any errors to the user errors = None if response.get("errors"): errors = response.get("errors") elif response.get("rate_response").get("errors"): errors = response.get("rate_response").get("errors") if errors: frappe.throw(_(errors[0].get("message", ""))) rates = response.get("rate_response").get("rates") return rates
def get_recipient(delivery_note_name): recipient = RequestedShipment() recipient.contact.CompanyName = frappe.db.get_value( "Delivery Note", delivery_note_name, "customer") contact_person = frappe.db.get_value("Delivery Note", delivery_note_name, "contact_person") if contact_person: primary_contact = frappe.get_doc("Contact", contact_person) else: primary_contact = frappe.get_doc({ "doctype": "Contact", "is_primary_contact": 1, "links": [{ "link_doctype": "Customer", "link_name": recipient.contact.PersonName }] }) if frappe.db.exists("Contact", primary_contact.name): if not recipient.contact.PhoneNumber: person_name = primary_contact.first_name if primary_contact.last_name: person_name += " {}".format(primary_contact.last_name) recipient.contact.PersonName = person_name recipient.contact.PhoneNumber = primary_contact.phone delivery_address = frappe.db.get_value("Delivery Note", delivery_note_name, "shipping_address_name") if delivery_address: shipping_address = frappe.get_doc("Address", delivery_address) if shipping_address.email_id: recipient.contact.Email_List.append(shipping_address.email_id) recipient.address.StreetLines.append(shipping_address.address_line1) if shipping_address.address_line2: recipient.address.StreetLines.append( shipping_address.address_line2) recipient.address.City = shipping_address.city recipient.address.PostalCode = shipping_address.pincode recipient.address.Country = shipping_address.country recipient.address.CountryCode = get_country_code( recipient.address.Country) recipient.address.StateOrProvinceCode = shipping_address.state recipient.address.Residential = shipping_address.is_residential if primary_contact.email_id and (primary_contact.email_id != shipping_address.email_id): recipient.contact.Email_List.append(primary_contact.email_id) return recipient