diff --git a/delivery_fedex_hibou/models/delivery_fedex.py b/delivery_fedex_hibou/models/delivery_fedex.py
index 1ade20d6..10eec70a 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..ad8d59e4 100644
--- a/delivery_fedex_hibou/models/fedex_request.py
+++ b/delivery_fedex_hibou/models/fedex_request.py
@@ -1,7 +1,7 @@
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__)
STATECODE_REQUIRED_COUNTRIES = fedex_request.STATECODE_REQUIRED_COUNTRIES
@@ -139,14 +139,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 +185,48 @@ class FedexRequest(fedex_request.FedexRequest):
Version=self.VersionId,
RequestedShipment=self.RequestedShipment,
ReturnTransitAndCommit=True) # New ReturnTransitAndCommit for CommitDetails in 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:
+ 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
+ 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
+ # 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 +243,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_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py
index e553445e..6e34425a 100644
--- a/delivery_gso/models/delivery_gso.py
+++ b/delivery_gso/models/delivery_gso.py
@@ -7,6 +7,8 @@ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
from .requests_gso import GSORequest
+import logging
+_logger = logging.getLogger(__name__)
GSO_TZ = 'PST8PDT'
@@ -319,3 +321,88 @@ class ProviderGSO(models.Model):
for _ in pickings:
res.append('https://www.gso.com/Tracking')
return res
+
+ def gso_rate_shipment_multi(self, order=None, picking=None):
+ sudoself = self.sudo()
+ service = sudoself._get_gso_service()
+ from_ = sudoself.get_shipper_warehouse(order=order, picking=picking)
+ to = sudoself.get_recipient(order=order, picking=picking)
+ address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R'
+
+ if order:
+ est_weight_value = self._gso_convert_weight(
+ sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0)
+ else:
+ est_weight_value = self._gso_convert_weight(picking.shipping_weight)
+
+ date_planned = fields.Datetime.now()
+ if self.env.context.get('date_planned'):
+ date_planned = self.env.context.get('date_planned')
+
+ ship_date_utc = fields.Datetime.from_string(date_planned if date_planned else fields.Datetime.now())
+ ship_date_utc = ship_date_utc.replace(tzinfo=pytz.utc)
+ ship_date_gso = ship_date_utc.astimezone(pytz.timezone(GSO_TZ))
+ ship_date_gso = fields.Datetime.to_string(ship_date_gso)
+
+ if picking and picking.package_ids:
+ package_dimensions = self._gso_get_package_dimensions(package=picking.package_ids[0])
+ else:
+ package_dimensions = self._gso_get_package_dimensions()
+
+ request_body = {
+ 'AccountNumber': sudoself.gso_account_number,
+ 'OriginZip': from_.zip,
+ 'DestinationZip': to.zip,
+ 'ShipDate': ship_date_gso,
+ 'PackageDimension': package_dimensions,
+ 'PackageWeight': est_weight_value,
+ 'DeliveryAddressType': address_type,
+ }
+
+ try:
+ result = service.get_rates_and_transit_time(request_body)
+ # _logger.warn('GSO result:\n%s' % result)
+ except HTTPError as e:
+ _logger.error(e)
+ return [{
+ 'success': False,
+ 'price': 0.0,
+ 'error_message': _('GSO web service returned an error.'),
+ 'warning_message': False,
+ }]
+
+ # delivery = list(filter(lambda d: d['ServiceCode'] == sudoself.gso_service_type, result['DeliveryServiceTypes']))
+ # if delivery:
+ rates = []
+ for delivery in result['DeliveryServiceTypes']:
+ delivery_date_gso = delivery['DeliveryDate'].replace('T', ' ')
+ delivery_date_gso = fields.Datetime.from_string(delivery_date_gso)
+ delivery_date_gso = delivery_date_gso.replace(tzinfo=pytz.timezone(GSO_TZ))
+ delivery_date_utc = delivery_date_gso.astimezone(pytz.utc)
+ delivery_date_utc = fields.Datetime.to_string(delivery_date_utc)
+ price = delivery.get('ShipmentCharges', {}).get('TotalCharge', 0.0)
+
+ carrier = self.gso_find_delivery_carrier_for_service(delivery['ServiceCode'])
+ if carrier:
+ rates.append({
+ 'carrier': carrier,
+ 'success': True,
+ 'price': price,
+ 'error_message': False,
+ 'warning_message': _('TotalCharge not found.') if price == 0.0 else False,
+ 'date_planned': date_planned,
+ 'date_delivered': delivery_date_utc,
+ 'transit_days': False,
+ 'service_code': delivery['ServiceCode'],
+ })
+
+ return rates
+
+ def gso_find_delivery_carrier_for_service(self, service_code):
+ if self.gso_service_type == service_code:
+ return self
+ # arbitrary decision, lets find the same account number
+ carrier = self.search([('gso_account_number', '=', self.gso_account_number),
+ ('gso_service_type', '=', service_code)
+ ], limit=1)
+ return carrier
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..763bafbf 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,
+ 'date_delivered': 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):
diff --git a/delivery_stamps/models/delivery_stamps.py b/delivery_stamps/models/delivery_stamps.py
index 0267756f..4f8c0bf1 100644
--- a/delivery_stamps/models/delivery_stamps.py
+++ b/delivery_stamps/models/delivery_stamps.py
@@ -143,6 +143,28 @@ class ProviderStamps(models.Model):
ret_val.ContentType = self._stamps_content_type()
return ret_val
+ def _get_stamps_shipping_multi(self, service, date_planned, order=False, picking=False):
+ if order:
+ weight = sum([(line.product_id.weight * line.product_qty) for line in order.order_line]) or 0.0
+ else:
+ weight = picking.shipping_weight
+ weight = self._stamps_convert_weight(weight)
+
+ shipper = self.get_shipper_warehouse(order=order, picking=picking)
+ recipient = self.get_recipient(order=order, picking=picking)
+
+ if not all((shipper.zip, recipient.zip)):
+ raise ValidationError('Stamps needs ZIP. From: ' + str(shipper.zip) + ' To: ' + str(recipient.zip))
+
+ ret_val = service.create_shipping()
+ ret_val.ShipDate = date_planned.strftime('%Y-%m-%d') if date_planned else date.today().isoformat()
+ ret_val.FromZIPCode = shipper.zip.split('-')[0]
+ ret_val.ToZIPCode = recipient.zip.split('-')[0]
+ ret_val.PackageType = self._stamps_package_type()
+ ret_val.WeightLb = weight
+ ret_val.ContentType = 'Merchandise'
+ return ret_val
+
def _stamps_get_addresses_for_picking(self, picking):
company = self.get_shipper_company(picking=picking)
from_ = self.get_shipper_warehouse(picking=picking)
@@ -426,3 +448,63 @@ class ProviderStamps(models.Model):
'carrier_price': 0.0})
except WebFault as e:
raise ValidationError(e)
+
+ def stamps_rate_shipment_multi(self, order=None, picking=None):
+ self.ensure_one()
+ date_planned = fields.Datetime.now()
+ if self.env.context.get('date_planned'):
+ date_planned = self.env.context.get('date_planned')
+ res = []
+ service = self._get_stamps_service()
+ shipping = self._get_stamps_shipping_multi(service, date_planned, order=order, picking=picking)
+ rates = service.get_rates(shipping)
+ for rate in rates:
+ price = float(rate.Amount)
+ if order:
+ currency = order.currency_id
+ else:
+ currency = picking.sale_id.currency_id if picking.sale_id else picking.company_id.currency_id
+ if currency.name != 'USD':
+ quote_currency = self.env['res.currency'].search([('name', '=', 'USD')], limit=1)
+ price = quote_currency.compute(rate.Amount, currency)
+
+ delivery_days = rate.DeliverDays
+ if delivery_days.find('-') >= 0:
+ delivery_days = delivery_days.split('-')
+ transit_days = int(delivery_days[-1])
+ else:
+ transit_days = int(delivery_days)
+ date_delivered = None
+ if transit_days > 0:
+ date_delivered = self.calculate_date_delivered(date_planned, transit_days)
+ service_code = rate.ServiceType
+ carrier = self.stamps_find_delivery_carrier_for_service(service_code)
+ if carrier:
+ res.append({
+ 'carrier': carrier,
+ 'success': True,
+ 'price': price,
+ 'error_message': False,
+ 'warning_message': False,
+ 'transit_days': transit_days,
+ 'date_delivered': date_delivered,
+ 'date_planned': date_planned,
+ 'service_code': service_code,
+ })
+ if not res:
+ res.append({
+ 'success': False,
+ 'price': 0.0,
+ 'error_message': 'No valid rates returned from Stamps.com',
+ 'warning_message': False
+ })
+ return res
+
+ def stamps_find_delivery_carrier_for_service(self, service_code):
+ if self.stamps_service_type == service_code:
+ return self
+ # arbitrary decision, lets find the same user name
+ carrier = self.search([('stamps_username', '=', self.stamps_username),
+ ('stamps_service_type', '=', service_code)
+ ], limit=1)
+ return carrier
diff --git a/delivery_ups_hibou/models/delivery_ups.py b/delivery_ups_hibou/models/delivery_ups.py
index 8285cdd6..245f29ba 100644
--- a/delivery_ups_hibou/models/delivery_ups.py
+++ b/delivery_ups_hibou/models/delivery_ups.py
@@ -2,6 +2,8 @@ from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.addons.delivery_ups.models.ups_request import UPSRequest, Package
from odoo.tools import pdf
+import logging
+_logger = logging.getLogger(__name__)
class ProviderUPS(models.Model):
@@ -216,3 +218,122 @@ class ProviderUPS(models.Model):
'tracking_number': carrier_tracking_ref}
res = res + [shipping_data]
return res
+
+ def ups_rate_shipment_multi(self, order=None, picking=None):
+ superself = self.sudo()
+ srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
+ ResCurrency = self.env['res.currency']
+ max_weight = self.ups_default_packaging_id.max_weight
+ packages = []
+ if order:
+ currency = order.currency_id
+ company = order.company_id
+ date_order = order.date_order or fields.Date.today()
+ total_qty = 0
+ total_weight = 0
+ for line in order.order_line.filtered(lambda line: not line.is_delivery):
+ total_qty += line.product_uom_qty
+ total_weight += line.product_id.weight * line.product_qty
+
+ if max_weight and total_weight > max_weight:
+ total_package = int(total_weight / max_weight)
+ last_package_weight = total_weight % max_weight
+
+ for seq in range(total_package):
+ packages.append(Package(self, max_weight))
+ if last_package_weight:
+ packages.append(Package(self, last_package_weight))
+ else:
+ packages.append(Package(self, total_weight))
+ else:
+ currency = picking.sale_id.currency_id if picking.sale_id else picking.company_id.currency_id
+ company = picking.company_id
+ date_order = picking.sale_id.date_order or fields.Date.today() if picking.sale_id else fields.Date.today()
+ # Is total quantity the number of packages or the number of items being shipped?
+ total_qty = len(picking.package_ids)
+ packages = [Package(self, package.shipping_weight) for package in picking.package_ids]
+
+
+ shipment_info = {
+ 'total_qty': total_qty # required when service type = 'UPS Worldwide Express Freight'
+ }
+
+ if self.ups_cod:
+ cod_info = {
+ 'currency': currency.name,
+ 'monetary_value': order.amount_total if order else picking.sale_id.amount_total,
+ 'funds_code': self.ups_cod_funds_code,
+ }
+ else:
+ cod_info = None
+
+ # Hibou Delivery
+ shipper_company = self.get_shipper_company(order=order, picking=picking)
+ shipper_warehouse = self.get_shipper_warehouse(order=order, picking=picking)
+ recipient = self.get_recipient(order=order, picking=picking)
+ date_planned = fields.Datetime.now()
+ if self.env.context.get('date_planned'):
+ date_planned = self.env.context.get('date_planned')
+
+ check_value = srm.check_required_value(shipper_company, shipper_warehouse, recipient, order=order, picking=picking)
+ if check_value:
+ return [{'success': False,
+ 'price': 0.0,
+ 'error_message': check_value,
+ 'warning_message': False,
+ }]
+
+ #ups_service_type = order.ups_service_type or self.ups_default_service_type
+ ups_service_type = None # See if this gets us all service types
+ result = srm.get_shipping_price(
+ shipment_info=shipment_info, packages=packages, shipper=shipper_company, ship_from=shipper_warehouse,
+ ship_to=recipient, packaging_type=self.ups_default_packaging_id.shipper_package_code, service_type=ups_service_type,
+ saturday_delivery=self.ups_saturday_delivery, cod_info=cod_info, date_planned=date_planned, multi=True)
+ # Hibou Delivery
+ is_third_party = self._get_ups_is_third_party(order=order, picking=picking)
+
+ response = []
+ for rate in result:
+ if rate.get('error_message'):
+ _logger.error('UPS error: %s' % rate['error_message'])
+ response.append({
+ 'success': False, 'price': 0.0,
+ 'error_message': _('Error:\n%s') % rate['error_message'],
+ 'warning_message': False,
+ })
+ else:
+ if currency.name == rate['currency_code']:
+ price = float(rate['price'])
+ else:
+ quote_currency = ResCurrency.search([('name', '=', rate['currency_code'])], limit=1)
+ price = quote_currency._convert(
+ float(rate['price']), currency, company, date_order)
+
+ if is_third_party:
+ # Don't show delivery amount, if ups bill my account option is true
+ price = 0.0
+
+ service_code = rate['service_code']
+ carrier = self.ups_find_delivery_carrier_for_service(service_code)
+ if carrier:
+ response.append({
+ 'carrier': carrier,
+ 'success': True,
+ 'price': price,
+ 'error_message': False,
+ 'warning_message': False,
+ 'date_planned': date_planned,
+ 'date_delivered': None,
+ 'transit_days': rate.get('transit_days', 0),
+ 'service_code': service_code,
+ })
+ return response
+
+ def ups_find_delivery_carrier_for_service(self, service_code):
+ if self.ups_default_service_type == service_code:
+ return self
+ # arbitrary decision, lets find the same account number
+ carrier = self.search([('ups_shipper_number', '=', self.ups_shipper_number),
+ ('ups_default_service_type', '=', service_code)
+ ], limit=1)
+ return carrier
diff --git a/delivery_ups_hibou/models/ups_request_patch.py b/delivery_ups_hibou/models/ups_request_patch.py
index 3104662e..1b811f16 100644
--- a/delivery_ups_hibou/models/ups_request_patch.py
+++ b/delivery_ups_hibou/models/ups_request_patch.py
@@ -1,15 +1,20 @@
import suds
from odoo.addons.delivery_ups.models.ups_request import UPSRequest
+import logging
+_logger = logging.getLogger(__name__)
SUDS_VERSION = suds.__version__
def patched_get_shipping_price(self, shipment_info, packages, shipper, ship_from, ship_to, packaging_type, service_type,
- saturday_delivery, cod_info, date_planned=False):
+ saturday_delivery, cod_info, date_planned=False, multi=False):
client = self._set_client(self.rate_wsdl, 'Rate', 'RateRequest')
request = client.factory.create('ns0:RequestType')
- request.RequestOption = 'Rate'
+ if multi:
+ request.RequestOption = 'Shop'
+ else:
+ request.RequestOption = 'Rate'
classification = client.factory.create('ns2:CodeDescriptionType')
classification.Code = '00' # Get rates for the shipper account
@@ -60,10 +65,11 @@ def patched_get_shipping_price(self, shipment_info, packages, shipper, ship_from
if not ship_to.commercial_partner_id.is_company:
shipment.ShipTo.Address.ResidentialAddressIndicator = suds.null()
- shipment.Service.Code = service_type or ''
- shipment.Service.Description = 'Service Code'
- if service_type == "96":
- shipment.NumOfPieces = int(shipment_info.get('total_qty'))
+ if not multi:
+ shipment.Service.Code = service_type or ''
+ shipment.Service.Description = 'Service Code'
+ if service_type == "96":
+ shipment.NumOfPieces = int(shipment_info.get('total_qty'))
if saturday_delivery:
shipment.ShipmentServiceOptions.SaturdayDeliveryIndicator = saturday_delivery
@@ -78,23 +84,45 @@ def patched_get_shipping_price(self, shipment_info, packages, shipper, ship_from
# Check if ProcessRate is not success then return reason for that
if response.Response.ResponseStatus.Code != "1":
- return self.get_error_message(response.Response.ResponseStatus.Code,
- response.Response.ResponseStatus.Description)
+ error_message = self.get_error_message(response.Response.ResponseStatus.Code,
+ response.Response.ResponseStatus.Description)
+ if multi:
+ return [error_message]
+ return error_message
- result = {}
- result['currency_code'] = response.RatedShipment[0].TotalCharges.CurrencyCode
+ if not multi:
+ result = {}
+ result['currency_code'] = response.RatedShipment[0].TotalCharges.CurrencyCode
- # Some users are qualified to receive negotiated rates
- negotiated_rate = 'NegotiatedRateCharges' in response.RatedShipment[0] and response.RatedShipment[
- 0].NegotiatedRateCharges.TotalCharge.MonetaryValue or None
+ # Some users are qualified to receive negotiated rates
+ negotiated_rate = 'NegotiatedRateCharges' in response.RatedShipment[0] and response.RatedShipment[
+ 0].NegotiatedRateCharges.TotalCharge.MonetaryValue or None
- result['price'] = negotiated_rate or response.RatedShipment[0].TotalCharges.MonetaryValue
+ result['price'] = negotiated_rate or response.RatedShipment[0].TotalCharges.MonetaryValue
- # Hibou Delivery
- if hasattr(response.RatedShipment[0], 'GuaranteedDelivery') and hasattr(response.RatedShipment[0].GuaranteedDelivery, 'BusinessDaysInTransit'):
- result['transit_days'] = int(response.RatedShipment[0].GuaranteedDelivery.BusinessDaysInTransit)
- # End
+ # Hibou Delivery
+ if hasattr(response.RatedShipment[0], 'GuaranteedDelivery') and hasattr(response.RatedShipment[0].GuaranteedDelivery, 'BusinessDaysInTransit'):
+ result['transit_days'] = int(response.RatedShipment[0].GuaranteedDelivery.BusinessDaysInTransit)
+ # End
+ else:
+ result = []
+ for rated_shipment in response.RatedShipment:
+ rate = {}
+ rate['currency_code'] = rated_shipment.TotalCharges.CurrencyCode
+ # Some users are qualified to receive negotiated rates
+ negotiated_rate = 'NegotiatedRateCharges' in rated_shipment and response.RatedShipment[
+ 0].NegotiatedRateCharges.TotalCharge.MonetaryValue or None
+
+ rate['price'] = negotiated_rate or rated_shipment.TotalCharges.MonetaryValue
+
+ # Hibou Delivery
+ if hasattr(rated_shipment, 'GuaranteedDelivery') and hasattr(
+ rated_shipment.GuaranteedDelivery, 'BusinessDaysInTransit'):
+ rate['transit_days'] = int(rated_shipment.GuaranteedDelivery.BusinessDaysInTransit)
+ # End
+ rate['service_code'] = rated_shipment.Service.Code
+ result.append(rate)
return result
except suds.WebFault as e:
@@ -102,11 +130,17 @@ def patched_get_shipping_price(self, shipment_info, packages, shipper, ship_from
prefix = ''
if SUDS_VERSION >= "0.6":
prefix = '/Envelope/Body/Fault'
- return self.get_error_message(
+ error_message = self.get_error_message(
e.document.childAtPath(prefix + '/detail/Errors/ErrorDetail/PrimaryErrorCode/Code').getText(),
e.document.childAtPath(prefix + '/detail/Errors/ErrorDetail/PrimaryErrorCode/Description').getText())
+ if multi:
+ return [error_message]
+ return error_message
except IOError as e:
- return self.get_error_message('0', 'UPS Server Not Found:\n%s' % e)
+ error_message = self.get_error_message('0', 'UPS Server Not Found:\n%s' % e)
+ if multi:
+ return [error_message]
+ return error_message
UPSRequest.get_shipping_price = patched_get_shipping_price
diff --git a/stock_delivery_planner/__init__.py b/stock_delivery_planner/__init__.py
new file mode 100644
index 00000000..9b429614
--- /dev/null
+++ b/stock_delivery_planner/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/stock_delivery_planner/__manifest__.py b/stock_delivery_planner/__manifest__.py
new file mode 100644
index 00000000..ded10ba9
--- /dev/null
+++ b/stock_delivery_planner/__manifest__.py
@@ -0,0 +1,27 @@
+{
+ 'name': 'Stock Delivery Planner',
+ 'summary': 'Get rates and choose carrier for delivery.',
+ 'version': '12.0.1.0.0',
+ 'author': "Hibou Corp.",
+ 'category': 'Warehouse',
+ 'license': 'AGPL-3',
+ 'website': "https://hibou.io",
+ 'description': """
+Stock Delivery Planner
+======================
+
+Re-rate deliveries at packing time to find lowest-priced delivery method that still meets the expected delivery date.
+
+""",
+ 'depends': [
+ 'delivery_hibou',
+ 'sale_planner',
+ 'stock',
+ ],
+ 'data': [
+ 'views/stock_views.xml',
+ 'wizard/stock_delivery_planner_views.xml',
+ ],
+ 'auto_install': False,
+ 'installable': True,
+}
diff --git a/stock_delivery_planner/models/__init__.py b/stock_delivery_planner/models/__init__.py
new file mode 100644
index 00000000..12bab770
--- /dev/null
+++ b/stock_delivery_planner/models/__init__.py
@@ -0,0 +1 @@
+from . import stock
diff --git a/stock_delivery_planner/models/stock.py b/stock_delivery_planner/models/stock.py
new file mode 100644
index 00000000..ca771553
--- /dev/null
+++ b/stock_delivery_planner/models/stock.py
@@ -0,0 +1,48 @@
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import UserError
+
+
+class StockPicking(models.Model):
+ _inherit = 'stock.picking'
+
+ @api.multi
+ def action_plan_delivery(self):
+ context = dict(self.env.context or {})
+ planner_model = self.env['stock.delivery.planner']
+ for picking in self:
+ planner = planner_model.create({
+ 'picking_id': picking.id,
+ })
+ return {
+ 'name': _('Plan Delivery'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'stock.delivery.planner',
+ 'res_id': planner.id,
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'view_id': self.env.ref('stock_delivery_planner.view_stock_delivery_planner').id,
+ 'target': 'new',
+ 'context': context,
+ }
+
+ # def get_shipping_carriers(self, carrier_id=None, domain=None):
+ def get_shipping_carriers(self):
+ Carrier = self.env['delivery.carrier'].sudo()
+ # if carrier_id:
+ # return Carrier.browse(carrier_id)
+ #
+ # if domain:
+ # if not isinstance(domain, (list, tuple)):
+ # domain = tools.safe_eval(domain)
+ # else:
+ domain = []
+
+ if self.env.context.get('carrier_domain'):
+ # potential bug here if this is textual
+ domain.extend(self.env.context.get('carrier_domain'))
+
+ irconfig_parameter = self.env['ir.config_parameter'].sudo()
+ if irconfig_parameter.get_param('sale.order.planner.carrier_domain'):
+ domain.extend(tools.safe_eval(irconfig_parameter.get_param('sale.order.planner.carrier_domain')))
+
+ return Carrier.search(domain)
diff --git a/stock_delivery_planner/tests/__init__.py b/stock_delivery_planner/tests/__init__.py
new file mode 100644
index 00000000..83aa212b
--- /dev/null
+++ b/stock_delivery_planner/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_stock_delivery_planner
diff --git a/stock_delivery_planner/tests/test_stock_delivery_planner.py b/stock_delivery_planner/tests/test_stock_delivery_planner.py
new file mode 100644
index 00000000..c642de7b
--- /dev/null
+++ b/stock_delivery_planner/tests/test_stock_delivery_planner.py
@@ -0,0 +1,133 @@
+from odoo import fields
+from odoo.tests.common import Form, TransactionCase
+
+
+class TestStockDeliveryPlanner(TransactionCase):
+ def setUp(self):
+ super(TestStockDeliveryPlanner, self).setUp()
+ try:
+ self.fedex_delivery = self.browse_ref('delivery_fedex.delivery_carrier_fedex_us')
+ except ValueError:
+ self.skipTest('FedEx Shipping Connector demo data is required to run this test.')
+ self.env['ir.config_parameter'].sudo().set_param('sale.order.planner.carrier_domain',
+ "[('id', 'in', (%d,))]" % self.fedex_delivery.id)
+ self.env['ir.config_parameter'].sudo().set_param('stock.delivery.planner.carrier_domain',
+ "[('id', 'in', (%d,))]" % self.fedex_delivery.id)
+ # Does it make sense to set default package in fedex_rate_shipment_multi
+ # instead of relying on a correctly configured delivery method?
+ self.fedex_package = self.browse_ref('delivery_fedex.fedex_packaging_FEDEX_25KG_BOX')
+ self.default_package = self.browse_ref('delivery_fedex.fedex_packaging_YOUR_PACKAGING')
+ self.fedex_delivery.fedex_default_packaging_id = self.default_package
+ # PRIORITY_OVERNIGHT might not be available depending on time of day?
+ self.fedex_delivery.fedex_service_type = 'GROUND_HOME_DELIVERY'
+ self.fedex_delivery_express = self.fedex_delivery.fedex_find_delivery_carrier_for_service('FEDEX_EXPRESS_SAVER')
+ if self.fedex_delivery_express:
+ self.fedex_delivery_express.fedex_default_packaging_id = self.default_package
+ else:
+ self.fedex_delivery_express = self.fedex_delivery.copy()
+ self.fedex_delivery_express.name = 'Test FedEx Delivery'
+ self.fedex_delivery_express.fedex_service_type = 'FEDEX_EXPRESS_SAVER'
+
+ delivery_calendar = self.env['resource.calendar'].create({
+ 'name': 'Test Delivery Calendar',
+ 'tz': 'US/Central',
+ 'attendance_ids': [
+ (0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
+ (0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
+ (0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
+ (0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
+ (0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
+ ],
+ })
+ self.fedex_delivery.delivery_calendar_id = delivery_calendar
+ self.fedex_delivery_express.delivery_calendar_id = delivery_calendar
+
+ # needs a valid address for sender and recipient
+ self.country_usa = self.env['res.country'].search([('name', '=', 'United States')], limit=1)
+ self.state_wa = self.env['res.country.state'].search([('name', '=', 'Washington')], limit=1)
+ self.state_ia = self.env['res.country.state'].search([('name', '=', 'Iowa')], limit=1)
+ self.env.user.company_id.partner_id.write({
+ 'street': '321 1st St',
+ 'city': 'Ames',
+ 'state_id': self.state_ia.id,
+ 'zip': '50010',
+ 'country_id': self.country_usa.id,
+ })
+ self.partner = self.env['res.partner'].create({
+ 'name': 'Test Customer',
+ 'street': '1234 Test Street',
+ 'city': 'Marysville',
+ 'state_id': self.state_wa.id,
+ 'zip': '98270',
+ 'country_id': self.country_usa.id,
+ # 'partner_latitude': 48.05636,
+ # 'partner_longitude': -122.14922,
+ 'customer': True,
+ })
+
+ # self.product = self.browse_ref('product.product_product_27') # [FURN_8855] Drawer
+ # self.product.weight = 5.0
+ # self.product.volume = 0.1
+ self.env['ir.config_parameter'].sudo().set_param('product.weight_in_lbs', '1')
+ self.product = self.env['product.product'].create({
+ 'name': 'Test Ship Product',
+ 'type': 'product',
+ 'weight': 1.0,
+ })
+ self.env['stock.change.product.qty'].create({
+ 'product_id': self.product.id,
+ 'new_quantity': 10.0,
+ }).change_product_qty()
+
+ so = Form(self.env['sale.order'])
+ so.partner_id = self.partner
+ so.carrier_id = self.env['delivery.carrier'].browse()
+ with so.order_line.new() as line:
+ line.product_id = self.product
+ line.product_uom_qty = 5.0
+ line.price_unit = 100.0
+ self.sale_order = so.save()
+
+ order_plan_action = self.sale_order.action_planorder()
+ order_plan = self.env[order_plan_action['res_model']].browse(order_plan_action['res_id'])
+ order_plan.planning_option_ids.filtered(lambda o: o.carrier_id == self.fedex_delivery).select_plan()
+
+ self.sale_order.action_confirm()
+ self.picking = self.sale_order.picking_ids
+
+ def test_00_test_one_package(self):
+ """Delivery is packed in one package"""
+ self.assertTrue(self.sale_order.requested_date, 'Order has not been planned')
+ self.assertEqual(len(self.picking), 1)
+ grp_pack = self.env.ref('stock.group_tracking_lot')
+ self.env.user.write({'groups_id': [(4, grp_pack.id)]})
+
+ self.assertEqual(self.picking.carrier_id, self.fedex_delivery, 'Carrier did not carry over to Delivery Order')
+ self.assertEqual(self.picking.weight, 5.0)
+ self.assertEqual(self.picking.shipping_weight, 0.0)
+
+ self.picking.move_line_ids.filtered(lambda ml: ml.product_id == self.product).qty_done = 5.0
+ packing_action = self.picking.put_in_pack()
+ packing_wizard = Form(self.env[packing_action['res_model']].with_context(packing_action['context']))
+ packing_wizard.delivery_packaging_id = self.fedex_package
+ choose_delivery_package = packing_wizard.save()
+ choose_delivery_package.put_in_pack()
+ self.assertEqual(self.picking.shipping_weight, 5.0)
+
+ action = self.picking.action_plan_delivery()
+ planner = self.env[action['res_model']].browse(action['res_id'])
+
+ self.assertEqual(planner.picking_id, self.picking)
+ self.assertGreater(len(planner.plan_option_ids), 1)
+
+ plan_option = planner.plan_option_ids.filtered(lambda o: o.carrier_id == self.fedex_delivery_express)
+ self.assertEqual(len(plan_option), 1)
+ self.assertGreater(plan_option.price, 0.0)
+ self.assertEqual(plan_option.date_planned.date(), fields.Date().today())
+ self.assertTrue(plan_option.requested_date)
+ self.assertEqual(plan_option.transit_days, 3)
+ self.assertEqual(plan_option.sale_requested_date, self.sale_order.requested_date)
+ self.assertEqual(plan_option.days_different, 2)
+
+ plan_option.select_plan()
+ self.assertEqual(self.picking.carrier_id, self.fedex_delivery_express)
diff --git a/stock_delivery_planner/views/stock_views.xml b/stock_delivery_planner/views/stock_views.xml
new file mode 100644
index 00000000..985dbd14
--- /dev/null
+++ b/stock_delivery_planner/views/stock_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ stock.picking.form.inherit.delivery.planner
+ stock.picking
+
+
+
+
+
+
+
+
diff --git a/stock_delivery_planner/wizard/__init__.py b/stock_delivery_planner/wizard/__init__.py
new file mode 100644
index 00000000..66c71ade
--- /dev/null
+++ b/stock_delivery_planner/wizard/__init__.py
@@ -0,0 +1 @@
+from . import stock_delivery_planner
diff --git a/stock_delivery_planner/wizard/stock_delivery_planner.py b/stock_delivery_planner/wizard/stock_delivery_planner.py
new file mode 100644
index 00000000..06e060f8
--- /dev/null
+++ b/stock_delivery_planner/wizard/stock_delivery_planner.py
@@ -0,0 +1,78 @@
+from odoo import api, fields, models, tools
+import logging
+_logger = logging.getLogger(__name__)
+
+
+class StockDeliveryPlanner(models.TransientModel):
+ _name = 'stock.delivery.planner'
+ _description = 'Stock Delivery Planner Wizard'
+
+ picking_id = fields.Many2one('stock.picking', 'Transfer')
+ plan_option_ids = fields.One2many('stock.delivery.planner.option', 'plan_id', 'Options')
+
+ def create(self, values):
+ planner = super(StockDeliveryPlanner, self).create(values)
+
+ base_carriers = self.env['delivery.carrier']
+ carrier_domain = self.env['ir.config_parameter'].sudo().get_param('stock.delivery.planner.carrier_domain')
+ if carrier_domain:
+ base_carriers = base_carriers.search(tools.safe_eval(carrier_domain))
+
+ for carrier in base_carriers:
+ rates = carrier.rate_shipment_multi(picking=planner.picking_id)
+ for rate in filter(lambda r: not r.get('success'), rates):
+ _logger.warning(rate.get('error_message'))
+ for rate in filter(lambda r: r.get('success'), rates):
+ rate = self.calculate_delivery_window(rate)
+ planner.plan_option_ids |= planner.plan_option_ids.create({
+ 'plan_id': self.id,
+ 'carrier_id': rate['carrier'].id,
+ 'price': rate['price'],
+ 'date_planned': rate['date_planned'],
+ 'requested_date': rate['date_delivered'],
+ 'transit_days': rate['transit_days'],
+ })
+ return planner
+
+ @api.model
+ def calculate_delivery_window(self, rate):
+ carrier = rate['carrier']
+ date_planned = rate['date_planned']
+ if rate.get('date_delivered'):
+ date_delivered = rate['date_delivered']
+ transit_days = carrier.calculate_transit_days(date_planned, date_delivered)
+ if not rate.get('transit_days') or transit_days < rate.get('transit_days'):
+ rate['transit_days'] = transit_days
+ elif rate.get('transit_days'):
+ rate['date_delivered'] = carrier.calculate_date_delivered(date_planned, rate.get('transit_days'))
+ return rate
+
+
+class StockDeliveryOption(models.TransientModel):
+ _name = 'stock.delivery.planner.option'
+ _description = 'Stock Delivery Planner Option'
+
+ plan_id = fields.Many2one('stock.delivery.planner', 'Plan', ondelete='cascade')
+ carrier_id = fields.Many2one('delivery.carrier', 'Delivery Method')
+ price = fields.Float('Shipping Price')
+ date_planned = fields.Datetime('Planned Date')
+ requested_date = fields.Datetime('Expected Delivery Date')
+ transit_days = fields.Integer('Transit Days')
+ sale_requested_date = fields.Datetime('Sale Order Delivery Date', related='plan_id.picking_id.sale_id.requested_date')
+ days_different = fields.Float('Days Different', compute='_compute_days_different') # use carrier calendar
+
+ @api.multi
+ def select_plan(self):
+ for option in self.filtered('carrier_id'):
+ option.plan_id.picking_id.carrier_id = option.carrier_id
+ return
+
+ @api.depends('requested_date', 'sale_requested_date', 'carrier_id')
+ def _compute_days_different(self):
+ for option in self:
+ if not option.requested_date or not option.sale_requested_date or option.requested_date.date() == option.sale_requested_date.date():
+ option.days_different = 0
+ elif option.requested_date < option.sale_requested_date:
+ option.days_different = -1 * option.carrier_id.calculate_transit_days(option.requested_date, option.sale_requested_date)
+ else:
+ option.days_different = option.carrier_id.calculate_transit_days(option.sale_requested_date, option.requested_date)
diff --git a/stock_delivery_planner/wizard/stock_delivery_planner_views.xml b/stock_delivery_planner/wizard/stock_delivery_planner_views.xml
new file mode 100644
index 00000000..c7e26afd
--- /dev/null
+++ b/stock_delivery_planner/wizard/stock_delivery_planner_views.xml
@@ -0,0 +1,28 @@
+
+
+
+ view.stock.delivery.planner
+ stock.delivery.planner
+ form
+
+
+
+
+