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 + + + +