Exemplo n.º 1
0
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
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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")
Exemplo n.º 4
0
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
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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