diff --git a/delivery_fedex_hibou/models/delivery_fedex.py b/delivery_fedex_hibou/models/delivery_fedex.py index 538f9323..c5fc5284 100644 --- a/delivery_fedex_hibou/models/delivery_fedex.py +++ b/delivery_fedex_hibou/models/delivery_fedex.py @@ -454,3 +454,205 @@ class DeliveryFedex(models.Model): raise UserError(_('No packages for this picking')) return res + + def fedex_rate_shipment_multi(self, order=None, picking=None): + if order: + max_weight = self._fedex_convert_weight(self.fedex_default_packaging_id.max_weight, self.fedex_weight_unit) + is_india = order.partner_shipping_id.country_id.code == 'IN' and order.company_id.partner_id.country_id.code == 'IN' + est_weight_value = sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0 + weight_value = self._fedex_convert_weight(est_weight_value, self.fedex_weight_unit) + order_currency = order.currency_id + else: + # max_weight = self._fedex_convert_weight(self.fedex_default_packaging_id.max_weight, self.fedex_weight_unit) + is_india = picking.partner_id.country_id.code == 'IN' and picking.company_id.partner_id.country_id.code == 'IN' + # TODO must be per-package eventually + # theoretically just sum of all packages weights, but the rating itself will also need to change... + est_weight_value = sum([(line.product_id.weight * (line.qty_done or line.product_uom_qty)) for line in picking.move_line_ids]) or 0.0 + weight_value = self._fedex_convert_weight(est_weight_value, self.fedex_weight_unit) + order_currency = picking.sale_id.currency_id if picking.sale_id else picking.company_id.currency_id + + price = 0.0 + + # Some users may want to ship very lightweight items; in order to give them a rating, we round the + # converted weight of the shipping to the smallest value accepted by FedEx: 0.01 kg or lb. + # (in the case where the weight is actually 0.0 because weights are not set, don't do this) + if weight_value > 0.0: + weight_value = max(weight_value, 0.01) + + superself = self.sudo() + + # Hibou Delivery methods for collecting details in an overridable way + shipper_company = superself.get_shipper_company(order=order, picking=picking) + shipper_warehouse = superself.get_shipper_warehouse(order=order, picking=picking) + recipient = superself.get_recipient(order=order, picking=picking) + acc_number = superself._get_fedex_account_number(order=order, picking=picking) + meter_number = superself._get_fedex_meter_number(order=order, picking=picking) + order_name = superself.get_order_name(order=order, picking=picking) + insurance_value = superself.get_insurance_value(order=order, picking=picking) + residential = self._get_fedex_recipient_is_residential(recipient) + date_planned = fields.Datetime.now() + if self.env.context.get('date_planned'): + date_planned = self.env.context.get('date_planned') + + # Authentication stuff + srm = FedexRequest(self.log_xml, request_type="rating", prod_environment=self.prod_environment) + srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password) + srm.client_detail(acc_number, meter_number) + + # Build basic rating request and set addresses + srm.transaction_detail(order_name) + + # TODO what shipment requests can we make? + # TODO package and main weights? + # TODO need package & weight count to pass in + srm.shipment_request( + self.fedex_droppoff_type, + None, # because we want all of the rates back... + self.fedex_default_packaging_id.shipper_package_code, + self.fedex_weight_unit, + self.fedex_saturday_delivery, + ship_timestamp=date_planned, + ) + pkg = self.fedex_default_packaging_id + + srm.set_currency(_convert_curr_iso_fdx(order_currency.name)) + srm.set_shipper(shipper_company, shipper_warehouse) + srm.set_recipient(recipient, residential=residential) + + if order and max_weight and weight_value > max_weight: + total_package = int(weight_value / max_weight) + last_package_weight = weight_value % max_weight + + for sequence in range(1, total_package + 1): + srm.add_package( + max_weight, + package_code=pkg.shipper_package_code, + package_height=pkg.height, + package_width=pkg.width, + package_length=pkg.length, + sequence_number=sequence, + mode='rating', + ) + if last_package_weight: + total_package = total_package + 1 + srm.add_package( + last_package_weight, + package_code=pkg.shipper_package_code, + package_height=pkg.height, + package_width=pkg.width, + package_length=pkg.length, + sequence_number=total_package, + mode='rating', + ) + srm.set_master_package(weight_value, total_package) + elif order: + srm.add_package( + weight_value, + package_code=pkg.shipper_package_code, + package_height=pkg.height, + package_width=pkg.width, + package_length=pkg.length, + mode='rating', + ) + srm.set_master_package(weight_value, 1) + else: + for sequence, package in enumerate(picking.package_ids, start=1): + package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit) + packaging = package.packaging_id + + srm.add_package( + package_weight, + mode='rating', + package_code=packaging.shipper_package_code, + package_height=packaging.height, + package_width=packaging.width, + package_length=packaging.length, + sequence_number=sequence, + # po_number=po_number, + # dept_number=dept_number, + ref=('%s-%d' % (order_name, sequence)), + insurance=insurance_value + ) + + + # Commodities for customs declaration (international shipping) + # TODO Intestigate rating if needed... + # if self.fedex_service_type in ['INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY'] or is_india: + # total_commodities_amount = 0.0 + # commodity_country_of_manufacture = order.warehouse_id.partner_id.country_id.code + # + # for line in order.order_line.filtered(lambda l: l.product_id.type in ['product', 'consu']): + # commodity_amount = line.price_total / line.product_uom_qty + # total_commodities_amount += (commodity_amount * line.product_uom_qty) + # commodity_description = line.product_id.name + # commodity_number_of_piece = '1' + # commodity_weight_units = self.fedex_weight_unit + # commodity_weight_value = self._fedex_convert_weight(line.product_id.weight * line.product_uom_qty, + # self.fedex_weight_unit) + # commodity_quantity = line.product_uom_qty + # commodity_quantity_units = 'EA' + # # DO NOT FORWARD PORT AFTER 12.0 + # if getattr(line.product_id, 'hs_code', False): + # commodity_harmonized_code = line.product_id.hs_code or '' + # else: + # commodity_harmonized_code = '' + # srm._commodities(_convert_curr_iso_fdx(order_currency.name), commodity_amount, + # commodity_number_of_piece, commodity_weight_units, commodity_weight_value, + # commodity_description, commodity_country_of_manufacture, commodity_quantity, + # commodity_quantity_units, commodity_harmonized_code) + # srm.customs_value(_convert_curr_iso_fdx(order_currency.name), total_commodities_amount, "NON_DOCUMENTS") + # srm.duties_payment(order.warehouse_id.partner_id.country_id.code, superself.fedex_account_number) + + request_list = srm.rate(date_planned=date_planned, multi=True) + result = [] + for request in request_list: + price = 0.0 + warnings = request.get('warnings_message') + if warnings: + _logger.info(warnings) + + if not request.get('errors_message'): + if _convert_curr_iso_fdx(order_currency.name) in request['price']: + price = request['price'][_convert_curr_iso_fdx(order_currency.name)] + else: + _logger.info("Preferred currency has not been found in FedEx response") + company_currency = order.company_id.currency_id + if _convert_curr_iso_fdx(company_currency.name) in request['price']: + amount = request['price'][_convert_curr_iso_fdx(company_currency.name)] + price = company_currency._convert(amount, order_currency, order.company_id, + order.date_order or fields.Date.today()) + else: + amount = request['price']['USD'] + price = company_currency._convert(amount, order_currency, order.company_id, + order.date_order or fields.Date.today()) + else: + result.append({'carrier': self, + 'success': False, + 'price': 0.0, + 'error_message': _('Error:\n%s') % request['errors_message'], + 'warning_message': False, + 'service_code': request['service_code'], + }) + service_code = request['service_code'] + carrier = self.fedex_find_delivery_carrier_for_service(service_code) + if carrier: + result.append({'carrier': carrier, + 'success': True, + 'price': price, + 'error_message': False, + 'transit_days': request.get('transit_days', False), + 'date_delivered': request.get('date_delivered', False), + 'date_planned': date_planned, + 'warning_message': _('Warning:\n%s') % warnings if warnings else False, + 'service_code': request['service_code'], + }) + return result + + def fedex_find_delivery_carrier_for_service(self, service_code): + if self.fedex_service_type == service_code: + return self + # arbitrary decision, lets find the same account number + carrier = self.search([('fedex_account_number', '=', self.fedex_account_number), + ('fedex_service_type', '=', service_code) + ], limit=1) + return carrier diff --git a/delivery_fedex_hibou/models/fedex_request.py b/delivery_fedex_hibou/models/fedex_request.py index a355ec83..c525b345 100644 --- a/delivery_fedex_hibou/models/fedex_request.py +++ b/delivery_fedex_hibou/models/fedex_request.py @@ -1,4 +1,6 @@ import suds +from datetime import datetime +from copy import deepcopy from odoo.addons.delivery_fedex.models import fedex_request import logging _logger = logging.getLogger(__name__) @@ -139,14 +141,37 @@ class FedexRequest(fedex_request.FedexRequest): Payor.ResponsibleParty.AccountNumber = shipping_charges_payment_account self.RequestedShipment.ShippingChargesPayment.Payor = Payor + def shipment_request(self, dropoff_type, service_type, packaging_type, overall_weight_unit, saturday_delivery, ship_timestamp=None): + self.RequestedShipment = self.client.factory.create('RequestedShipment') + self.RequestedShipment.ShipTimestamp = ship_timestamp or datetime.now() + self.RequestedShipment.DropoffType = dropoff_type + self.RequestedShipment.ServiceType = service_type + self.RequestedShipment.PackagingType = packaging_type + # Resuest estimation of duties and taxes for international shipping + if service_type in ['INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY']: + self.RequestedShipment.EdtRequestType = 'ALL' + else: + self.RequestedShipment.EdtRequestType = 'NONE' + self.RequestedShipment.PackageCount = 0 + self.RequestedShipment.TotalWeight.Units = overall_weight_unit + self.RequestedShipment.TotalWeight.Value = 0 + self.listCommodities = [] + if saturday_delivery: + timestamp_day = self.RequestedShipment.ShipTimestamp.strftime("%A") + if (service_type == 'FEDEX_2_DAY' and timestamp_day == 'Thursday') or (service_type in ['PRIORITY_OVERNIGHT', 'FIRST_OVERNIGHT', 'INTERNATIONAL_PRIORITY'] and timestamp_day == 'Friday'): + SpecialServiceTypes = self.client.factory.create('ShipmentSpecialServiceType') + self.RequestedShipment.SpecialServicesRequested.SpecialServiceTypes = [SpecialServiceTypes.SATURDAY_DELIVERY] + # Rating stuff - def rate(self, date_planned=None): + def rate(self, date_planned=None, multi=False): """ Response will contain 'transit_days' key with number of days. :param date_planned: Planned Outgoing shipment. Used to have FedEx tell us how long it will take for the package to arrive. :return: """ + if multi: + multi_result = [] if date_planned: self.RequestedShipment.ShipTimestamp = date_planned @@ -162,25 +187,52 @@ class FedexRequest(fedex_request.FedexRequest): Version=self.VersionId, RequestedShipment=self.RequestedShipment, ReturnTransitAndCommit=True) # New ReturnTransitAndCommit for CommitDetails in response + # _logger.warn(self.response) + if (self.response.HighestSeverity != 'ERROR' and self.response.HighestSeverity != 'FAILURE'): if not getattr(self.response, "RateReplyDetails", False): raise Exception("No rating found") - for rating in self.response.RateReplyDetails[0].RatedShipmentDetails: - formatted_response['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = rating.ShipmentRateDetail.TotalNetFedExCharge.Amount - if len(self.response.RateReplyDetails[0].RatedShipmentDetails) == 1: - if 'CurrencyExchangeRate' in self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail: - formatted_response['price'][self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount / self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.Rate - # Hibou Delivery Planning - if hasattr(self.response.RateReplyDetails[0], 'DeliveryTimestamp') and self.response.RateReplyDetails[0].DeliveryTimestamp: - formatted_response['date_delivered'] = self.response.RateReplyDetails[0].DeliveryTimestamp - elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'CommitTimestamp'): - formatted_response['date_delivered'] = self.response.RateReplyDetails[0].CommitDetails[0].CommitTimestamp - formatted_response['transit_days'] = self._service_transit_days.get(self.response.RateReplyDetails[0].CommitDetails[0].ServiceType, 0) - elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'TransitTime'): - transit_days = self.response.RateReplyDetails[0].CommitDetails[0].TransitTime - transit_days = self._transit_days.get(transit_days, 0) - formatted_response['transit_days'] = transit_days + if not multi: + for rating in self.response.RateReplyDetails[0].RatedShipmentDetails: + formatted_response['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = rating.ShipmentRateDetail.TotalNetFedExCharge.Amount + if len(self.response.RateReplyDetails[0].RatedShipmentDetails) == 1: + if 'CurrencyExchangeRate' in self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail: + formatted_response['price'][self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount / self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.Rate + + # Hibou Delivery Planning + if hasattr(self.response.RateReplyDetails[0], 'DeliveryTimestamp') and self.response.RateReplyDetails[0].DeliveryTimestamp: + formatted_response['date_delivered'] = self.response.RateReplyDetails[0].DeliveryTimestamp + elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'CommitTimestamp'): + formatted_response['date_delivered'] = self.response.RateReplyDetails[0].CommitDetails[0].CommitTimestamp + formatted_response['transit_days'] = self._service_transit_days.get(self.response.RateReplyDetails[0].CommitDetails[0].ServiceType, 0) + elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'TransitTime'): + transit_days = self.response.RateReplyDetails[0].CommitDetails[0].TransitTime + transit_days = self._transit_days.get(transit_days, 0) + formatted_response['transit_days'] = transit_days + else: + for rate_reply_detail in self.response.RateReplyDetails: + _logger.warn('iterated RateReplyDetail: ' + str(rate_reply_detail)) + res = deepcopy(formatted_response) + res['service_code'] = rate_reply_detail.ServiceType + for rating in rate_reply_detail.RatedShipmentDetails: + res['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = rating.ShipmentRateDetail.TotalNetFedExCharge.Amount + _logger.warn(' inside rating iteration res: ' + str(res)) + if len(rate_reply_detail.RatedShipmentDetails) == 1: + if 'CurrencyExchangeRate' in rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail: + res['price'][rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount / rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.Rate + _logger.warn('end rate ' + str(res)) + # Hibou Delivery Planning + if hasattr(rate_reply_detail, 'DeliveryTimestamp') and rate_reply_detail.DeliveryTimestamp: + res['date_delivered'] = rate_reply_detail.DeliveryTimestamp + elif hasattr(rate_reply_detail, 'CommitDetails') and hasattr(rate_reply_detail.CommitDetails[0], 'CommitTimestamp'): + res['date_delivered'] = rate_reply_detail.CommitDetails[0].CommitTimestamp + res['transit_days'] = self._service_transit_days.get(rate_reply_detail.CommitDetails[0].ServiceType, 0) + elif hasattr(rate_reply_detail, 'CommitDetails') and hasattr(rate_reply_detail.CommitDetails[0], 'TransitTime'): + transit_days = rate_reply_detail.CommitDetails[0].TransitTime + transit_days = self._transit_days.get(transit_days, 0) + res['transit_days'] = transit_days + multi_result.append(res) else: errors_message = '\n'.join([("%s: %s" % (n.Code, n.Message)) for n in self.response.Notifications if (n.Severity == 'ERROR' or n.Severity == 'FAILURE')]) @@ -197,4 +249,6 @@ class FedexRequest(fedex_request.FedexRequest): except Exception as e: formatted_response['errors_message'] = e.args[0] + if multi: + return multi_result return formatted_response diff --git a/delivery_hibou/models/__init__.py b/delivery_hibou/models/__init__.py index 29ce1386..6d2ecab6 100644 --- a/delivery_hibou/models/__init__.py +++ b/delivery_hibou/models/__init__.py @@ -1,2 +1,3 @@ from . import delivery +from . import sale from . import stock diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 87060dc1..8f34b2c3 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES @@ -157,3 +157,46 @@ class DeliveryCarrier(models.Model): def _get_recipient_out(self, picking): return picking.partner_id + + # -------------------------- # + # API for external providers # + # -------------------------- # + @api.multi + def rate_shipment_multi(self, order=None, picking=None): + ''' Compute the price of the order shipment + + :param order: record of sale.order or None + :param picking: record of stock.picking or None + :return list: dict: { + 'carrier': delivery.carrier(), + 'success': boolean, + 'price': a float, + 'error_message': a string containing an error message, + 'warning_message': a string containing a warning message, + 'date_planned': a datetime for when the shipment is supposed to leave, + 'requested_date': a datetime for when the shipment is supposed to arrive, + 'transit_days': a Float for how many days it takes in transit, + 'service_code': a string that represents the service level/agreement, + } + + e.g. self == delivery.carrier(5, 6) + then return might be: + [ + {'carrier': delivery.carrier(5), 'price': 10.50, 'service_code': 'GROUND_HOME_DELIVERY', ...}, + {'carrier': delivery.carrier(7), 'price': 12.99, 'service_code': 'FEDEX_EXPRESS_SAVER', ...}, # NEW! + {'carrier': delivery.carrier(6), 'price': 8.0, 'service_code': 'USPS_PRI', ...}, + ] + ''' + self.ensure_one() + + if picking: + self = self.with_context(date_planned=fields.Datetime.now()) + else: + self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now())) + + res = [] + for carrier in self: + if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type): + carrier_rates = getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, picking=picking) + res += carrier_rates + return res diff --git a/delivery_hibou/models/sale.py b/delivery_hibou/models/sale.py new file mode 100644 index 00000000..2d570a7a --- /dev/null +++ b/delivery_hibou/models/sale.py @@ -0,0 +1,10 @@ +from odoo import api, models +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + @api.multi + def button_test_rate_multi(self): + raise UserError(str(self.carrier_id.rate_shipment_multi(order=self))) diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index c1a6a792..d2149b33 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -1,4 +1,5 @@ from odoo import api, fields, models +from odoo.exceptions import UserError class StockPicking(models.Model): @@ -12,6 +13,10 @@ class StockPicking(models.Model): ], string='Require Insurance', default='auto', help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.') + @api.multi + def button_test_rate_multi(self): + raise UserError(str(self.carrier_id.rate_shipment_multi(picking=self))) + @api.one @api.depends('move_lines.priority', 'carrier_id') def _compute_priority(self):