mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'imp/11.0/delivery_planner_backports' into '11.0-test'
imp/11.0/delivery_planner_backports into 11.0-test See merge request hibou-io/hibou-odoo/suite!1077
This commit is contained in:
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
'name': 'Hibou Fedex Shipping',
|
||||
'version': '11.0.1.0.0',
|
||||
'version': '11.0.1.1.0',
|
||||
'category': 'Stock',
|
||||
'author': "Hibou Corp.",
|
||||
'license': 'AGPL-3',
|
||||
'license': 'OPL-1',
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': [
|
||||
'delivery_fedex',
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import delivery_fedex
|
||||
from . import stock
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import time
|
||||
from odoo import fields, models, tools, _
|
||||
@@ -17,6 +19,10 @@ class DeliveryFedex(models.Model):
|
||||
('FEDEX_EXPRESS_SAVER', 'FEDEX_EXPRESS_SAVER'),
|
||||
])
|
||||
|
||||
def _fedex_convert_weight(self, weight, unit):
|
||||
# dummy converter
|
||||
return weight
|
||||
|
||||
def _get_fedex_is_third_party(self, order=None, picking=None):
|
||||
third_party_account = self.get_third_party_account(order=order, picking=picking)
|
||||
if third_party_account:
|
||||
@@ -112,6 +118,8 @@ class DeliveryFedex(models.Model):
|
||||
date_planned = None
|
||||
if self.env.context.get('date_planned'):
|
||||
date_planned = self.env.context.get('date_planned')
|
||||
if date_planned and isinstance(date_planned, str):
|
||||
date_planned = fields.Datetime.from_string(date_planned)
|
||||
|
||||
|
||||
# Authentication stuff
|
||||
@@ -205,6 +213,15 @@ class DeliveryFedex(models.Model):
|
||||
srm = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
|
||||
superself = self.sudo()
|
||||
|
||||
picking_packages = picking.package_ids
|
||||
package_carriers = picking_packages.mapped('carrier_id')
|
||||
if package_carriers:
|
||||
# only ship ours
|
||||
picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
|
||||
|
||||
if package_carriers and not picking_packages:
|
||||
continue
|
||||
|
||||
shipper_company = superself.get_shipper_company(picking=picking)
|
||||
shipper_warehouse = superself.get_shipper_warehouse(picking=picking)
|
||||
recipient = superself.get_recipient(picking=picking)
|
||||
@@ -222,7 +239,7 @@ class DeliveryFedex(models.Model):
|
||||
# Not the actual reference. Using `shipment_name` during `add_package` calls.
|
||||
srm.transaction_detail(picking.id)
|
||||
|
||||
package_type = picking.package_ids and picking.package_ids[0].packaging_id.shipper_package_code or self.fedex_default_packaging_id.shipper_package_code
|
||||
package_type = picking_packages and picking_packages[0].packaging_id.shipper_package_code or self.fedex_default_packaging_id.shipper_package_code
|
||||
srm.shipment_request(self.fedex_droppoff_type, self.fedex_service_type, package_type, self.fedex_weight_unit, self.fedex_saturday_delivery)
|
||||
srm.set_currency(_convert_curr_iso_fdx(picking.company_id.currency_id.name))
|
||||
srm.set_shipper(shipper_company, shipper_warehouse)
|
||||
@@ -258,7 +275,7 @@ class DeliveryFedex(models.Model):
|
||||
srm.customs_value(_convert_curr_iso_fdx(commodity_currency.name), total_commodities_amount, "NON_DOCUMENTS")
|
||||
srm.duties_payment(picking.picking_type_id.warehouse_id.partner_id.country_id.code, superself.fedex_account_number)
|
||||
|
||||
package_count = len(picking.package_ids) or 1
|
||||
package_count = len(picking_packages) or 1
|
||||
|
||||
# TODO RIM master: factorize the following crap
|
||||
|
||||
@@ -280,13 +297,23 @@ class DeliveryFedex(models.Model):
|
||||
package_labels = []
|
||||
carrier_tracking_ref = ""
|
||||
|
||||
for sequence, package in enumerate(picking.package_ids, start=1):
|
||||
|
||||
package_weight = _convert_weight(package.shipping_weight, self.fedex_weight_unit)
|
||||
for sequence, package in enumerate(picking_packages, start=1):
|
||||
package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit)
|
||||
packaging = package.packaging_id
|
||||
packaging_code = packaging.shipper_package_code if (packaging.package_carrier_type == 'fedex' and packaging.shipper_package_code) else self.fedex_default_packaging_id.shipper_package_code
|
||||
|
||||
# Hibou Delivery
|
||||
# Add more details to package.
|
||||
srm.add_package(package_weight, sequence_number=sequence, ref=('%s-%d' % (order_name, sequence)), insurance=insurance_value)
|
||||
srm.add_package(
|
||||
package_weight,
|
||||
# package_code=packaging_code,
|
||||
# package_height=packaging.height,
|
||||
# package_width=packaging.width,
|
||||
# package_length=packaging.length,
|
||||
sequence_number=sequence,
|
||||
ref=('%s-%d' % (order_name, sequence)),
|
||||
insurance=insurance_value
|
||||
)
|
||||
srm.set_master_package(net_weight, package_count, master_tracking_id=master_tracking_id)
|
||||
request = srm.process_shipment()
|
||||
package_name = package.name or sequence
|
||||
@@ -350,9 +377,17 @@ class DeliveryFedex(models.Model):
|
||||
# One package #
|
||||
###############
|
||||
elif package_count == 1:
|
||||
# Hibou Delivery
|
||||
# Add more details to package.
|
||||
srm.add_package(net_weight, ref=order_name, insurance=insurance_value)
|
||||
packaging = picking_packages[:1].packaging_id or self.fedex_default_packaging_id
|
||||
packaging_code = packaging.shipper_package_code if packaging.package_carrier_type == 'fedex' else self.fedex_default_packaging_id.shipper_package_code
|
||||
srm.add_package(
|
||||
net_weight,
|
||||
# package_code=packaging_code,
|
||||
# package_height=packaging.height,
|
||||
# package_width=packaging.width,
|
||||
# package_length=packaging.length,
|
||||
ref=order_name,
|
||||
insurance=insurance_value
|
||||
)
|
||||
srm.set_master_package(net_weight, 1)
|
||||
|
||||
# Ask the shipping to fedex
|
||||
@@ -394,3 +429,238 @@ class DeliveryFedex(models.Model):
|
||||
raise UserError(_('No packages for this picking'))
|
||||
|
||||
return res
|
||||
|
||||
def fedex_rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||
if not packages:
|
||||
return self._fedex_rate_shipment_multi_package(order=order, picking=picking)
|
||||
else:
|
||||
rates = []
|
||||
for package in packages:
|
||||
rates += self._fedex_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||
return rates
|
||||
|
||||
def _fedex_rate_shipment_multi_package(self, order=None, picking=None, package=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
|
||||
elif not package:
|
||||
is_india = picking.partner_id.country_id.code == 'IN' and picking.company_id.partner_id.country_id.code == 'IN'
|
||||
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
|
||||
else:
|
||||
is_india = picking.partner_id.country_id.code == 'IN' and picking.company_id.partner_id.country_id.code == 'IN'
|
||||
order_currency = picking.sale_id.currency_id if picking.sale_id else picking.company_id.currency_id
|
||||
est_weight_value = package.shipping_weight or package.weight
|
||||
weight_value = self._fedex_convert_weight(est_weight_value, self.fedex_weight_unit)
|
||||
|
||||
|
||||
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')
|
||||
if date_planned and isinstance(date_planned, str):
|
||||
date_planned = fields.Datetime.from_string(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:
|
||||
if package:
|
||||
package_weight = self._fedex_convert_weight(package.shipping_weight or package.weight, self.fedex_weight_unit)
|
||||
packaging = package.packaging_id
|
||||
package_code = package.packaging_id.shipper_package_code if packaging.package_carrier_type == 'fedex' else self.fedex_default_packaging_id.shipper_package_code
|
||||
|
||||
srm.add_package(
|
||||
package_weight,
|
||||
mode='rating',
|
||||
# package_code=package_code,
|
||||
# package_height=packaging.height,
|
||||
# package_width=packaging.width,
|
||||
# package_length=packaging.length,
|
||||
sequence_number=1,
|
||||
ref=('%s-%d' % (order_name, 1)),
|
||||
insurance=insurance_value
|
||||
)
|
||||
else:
|
||||
# deliver all together...
|
||||
package_weight = self._fedex_convert_weight(picking.shipping_weight or picking.weight, self.fedex_weight_unit)
|
||||
packaging = self.fedex_default_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=1,
|
||||
# po_number=po_number,
|
||||
# dept_number=dept_number,
|
||||
ref=('%s-%d' % (order_name, 1)),
|
||||
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:
|
||||
date_delivered = request.get('date_delivered', False)
|
||||
result.append({'carrier': carrier,
|
||||
'package': package or self.env['stock.quant.package'].browse(),
|
||||
'success': True,
|
||||
'price': price,
|
||||
'error_message': False,
|
||||
'transit_days': request.get('transit_days', False),
|
||||
'date_delivered': date_delivered,
|
||||
'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
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import suds
|
||||
from odoo.addons.delivery_fedex.models import fedex_request
|
||||
from datetime import datetime
|
||||
from copy import deepcopy
|
||||
from pprint import pformat
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# logging.getLogger('suds.client').setLevel(logging.DEBUG)
|
||||
STATECODE_REQUIRED_COUNTRIES = fedex_request.STATECODE_REQUIRED_COUNTRIES
|
||||
|
||||
|
||||
@@ -130,20 +134,44 @@ 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
|
||||
|
||||
formatted_response = {'price': {}}
|
||||
del self.ClientDetail.Region
|
||||
|
||||
if date_planned:
|
||||
# though Fedex sends BACK timestamps like `2020-01-01 00:00:00` they EXPECT `2020-01-01T00:00:00`
|
||||
self.RequestedShipment.ShipTimestamp = date_planned.replace(' ', 'T')
|
||||
if self.hasCommodities:
|
||||
self.RequestedShipment.CustomsClearanceDetail.Commodities = self.listCommodities
|
||||
|
||||
try:
|
||||
self.response = self.client.service.getRates(WebAuthenticationDetail=self.WebAuthenticationDetail,
|
||||
@@ -152,23 +180,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'):
|
||||
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[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[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 (self.response.HighestSeverity != 'ERROR' and self.response.HighestSeverity != 'FAILURE'):
|
||||
if not getattr(self.response, "RateReplyDetails", False):
|
||||
raise Exception("No rating found")
|
||||
|
||||
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')])
|
||||
@@ -182,5 +235,9 @@ class FedexRequest(fedex_request.FedexRequest):
|
||||
formatted_response['errors_message'] = fault
|
||||
except IOError:
|
||||
formatted_response['errors_message'] = "Fedex Server Not Found"
|
||||
except Exception as e:
|
||||
formatted_response['errors_message'] = e.args[0]
|
||||
|
||||
if multi:
|
||||
return multi_result
|
||||
return formatted_response
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
'name': 'Golden State Overnight (gso.com) Shipping',
|
||||
'summary': 'Send your shippings through gso.com and track them online.',
|
||||
'version': '11.0.1.0.0',
|
||||
'version': '11.0.1.1.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Warehouse',
|
||||
'license': 'AGPL-3',
|
||||
'license': 'OPL-1',
|
||||
'images': [],
|
||||
'website': "https://hibou.io",
|
||||
'description': """
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import delivery_gso
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
from math import ceil
|
||||
from requests import HTTPError
|
||||
@@ -160,7 +162,7 @@ class ProviderGSO(models.Model):
|
||||
company = self.get_shipper_company(picking=picking)
|
||||
from_ = self.get_shipper_warehouse(picking=picking)
|
||||
to = self.get_recipient(picking=picking)
|
||||
address_type = 'B' if bool(to.company or to.parent_id.company) else 'R'
|
||||
address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R'
|
||||
|
||||
request_body = {
|
||||
'AccountNumber': sudoself.gso_account_number,
|
||||
@@ -188,9 +190,15 @@ class ProviderGSO(models.Model):
|
||||
'thermal': [],
|
||||
'paper': [],
|
||||
}
|
||||
if picking.package_ids:
|
||||
picking_packages = picking.package_ids
|
||||
package_carriers = picking_packages.mapped('carrier_id')
|
||||
if package_carriers:
|
||||
# only ship ours
|
||||
picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
|
||||
|
||||
if picking_packages:
|
||||
# Every package will be a transaction
|
||||
for package in picking.package_ids:
|
||||
for package in picking_packages:
|
||||
request_body['Shipment']['Weight'] = self._gso_convert_weight(package.shipping_weight)
|
||||
request_body['Shipment'].update(self._gso_get_package_dimensions(package))
|
||||
request_body['Shipment']['ShipmentReference'] = package.name
|
||||
@@ -207,7 +215,8 @@ class ProviderGSO(models.Model):
|
||||
cost += response['ShipmentCharges']['TotalCharge']
|
||||
except HTTPError as e:
|
||||
raise ValidationError(e)
|
||||
else:
|
||||
elif not package_carriers:
|
||||
# ship the whole picking
|
||||
request_body['Shipment']['Weight'] = self._gso_convert_weight(picking.shipping_weight)
|
||||
request_body['Shipment'].update(self._gso_get_package_dimensions())
|
||||
request_body['Shipment']['ShipmentReference'] = picking.name
|
||||
@@ -224,6 +233,8 @@ class ProviderGSO(models.Model):
|
||||
cost += response['ShipmentCharges']['TotalCharge']
|
||||
except HTTPError as e:
|
||||
raise ValidationError(e)
|
||||
else:
|
||||
continue
|
||||
|
||||
# Handle results
|
||||
trackings = [l[0] for l in labels['thermal']] + [l(0) for l in labels['paper']]
|
||||
@@ -251,7 +262,7 @@ class ProviderGSO(models.Model):
|
||||
}
|
||||
for tracking in picking.carrier_tracking_ref.split(','):
|
||||
request_body['TrackingNumber'] = tracking
|
||||
_ = service.delete_shipment(request_body)
|
||||
cancel_res = service.delete_shipment(request_body)
|
||||
except HTTPError as e:
|
||||
raise ValidationError(e)
|
||||
picking.message_post(body=(_('Shipment N° %s has been cancelled') % (picking.carrier_tracking_ref, )))
|
||||
@@ -262,7 +273,7 @@ class ProviderGSO(models.Model):
|
||||
service = sudoself._get_gso_service()
|
||||
from_ = sudoself.get_shipper_warehouse(order=order)
|
||||
to = sudoself.get_recipient(order=order)
|
||||
address_type = 'B' if bool(to.company or to.parent_id.company) else 'R'
|
||||
address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R'
|
||||
|
||||
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)
|
||||
@@ -319,3 +330,106 @@ 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, packages=None):
|
||||
if not packages:
|
||||
return self._gso_rate_shipment_multi_package(order=order, picking=picking)
|
||||
else:
|
||||
rates = []
|
||||
for package in packages:
|
||||
rates += self._gso_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||
return rates
|
||||
|
||||
def _gso_rate_shipment_multi_package(self, order=None, picking=None, package=None):
|
||||
sudoself = self.sudo()
|
||||
try:
|
||||
service = sudoself._get_gso_service()
|
||||
except HTTPError as e:
|
||||
_logger.error(e)
|
||||
return [{
|
||||
'success': False,
|
||||
'price': 0.0,
|
||||
'error_message': _('GSO web service returned an error. ' + str(e)),
|
||||
'warning_message': False,
|
||||
}]
|
||||
|
||||
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'
|
||||
package_dimensions = self._gso_get_package_dimensions(package=package)
|
||||
|
||||
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 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)
|
||||
elif not package:
|
||||
est_weight_value = self._gso_convert_weight(picking.shipping_weight)
|
||||
else:
|
||||
est_weight_value = self._gso_convert_weight(package.shipping_weight or package.weight)
|
||||
|
||||
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,
|
||||
'package': package or self.env['stock.quant.package'].browse(),
|
||||
'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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
'name': 'Delivery Hibou',
|
||||
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
|
||||
'version': '11.0.1.0.0',
|
||||
'version': '11.0.1.1.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Stock',
|
||||
'license': 'AGPL-3',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class DeliveryCarrier(models.Model):
|
||||
@@ -157,3 +158,89 @@ 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, packages=None):
|
||||
''' Compute the price of the order shipment
|
||||
|
||||
:param order: record of sale.order or None
|
||||
:param picking: record of stock.picking or None
|
||||
:param packages: recordset of stock.quant.package or None (requires picking also set)
|
||||
: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,
|
||||
'package': stock.quant.package(),
|
||||
}
|
||||
|
||||
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())
|
||||
if not packages:
|
||||
packages = picking.package_ids
|
||||
else:
|
||||
if packages:
|
||||
raise UserError('Cannot rate package without picking.')
|
||||
self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now()))
|
||||
|
||||
res = []
|
||||
for carrier in self:
|
||||
carrier_packages = packages.filtered(lambda p: not p.carrier_tracking_ref and
|
||||
(not p.carrier_id or p.carrier_id == carrier) and
|
||||
p.packaging_id.package_carrier_type in (False, '', 'none', carrier.delivery_type))
|
||||
if packages and not carrier_packages:
|
||||
continue
|
||||
if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type):
|
||||
try:
|
||||
res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order,
|
||||
picking=picking,
|
||||
packages=carrier_packages)
|
||||
except TypeError:
|
||||
# TODO remove catch if after Odoo 14
|
||||
# This is intended to find ones that don't support packages= kwarg
|
||||
res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order,
|
||||
picking=picking)
|
||||
|
||||
return res
|
||||
|
||||
def cancel_shipment(self, pickings, packages=None):
|
||||
''' Cancel a shipment
|
||||
|
||||
:param pickings: A recordset of pickings
|
||||
:param packages: Optional recordset of packages (should be for this carrier)
|
||||
'''
|
||||
self.ensure_one()
|
||||
if hasattr(self, '%s_cancel_shipment' % self.delivery_type):
|
||||
# No good way to tell if this method takes the kwarg for packages
|
||||
if packages:
|
||||
try:
|
||||
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings, packages=packages)
|
||||
except TypeError:
|
||||
# we won't be able to cancel the packages properly
|
||||
# here we will TRY to make a good call here where we put the package references into the picking
|
||||
# and let the original mechanisms try to work here
|
||||
tracking_ref = ','.join(packages.mapped('carrier_tracking_ref'))
|
||||
pickings.write({
|
||||
'carrier_id': self.id,
|
||||
'carrier_tracking_ref': tracking_ref,
|
||||
})
|
||||
|
||||
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings)
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockQuantPackage(models.Model):
|
||||
_inherit = 'stock.quant.package'
|
||||
|
||||
carrier_id = fields.Many2one('delivery.carrier', string='Carrier')
|
||||
carrier_tracking_ref = fields.Char(string='Tracking Reference')
|
||||
|
||||
def _get_active_picking(self):
|
||||
picking_id = self._context.get('active_id')
|
||||
picking_model = self._context.get('active_model')
|
||||
if not picking_id or picking_model != 'stock.picking':
|
||||
raise UserError('Cannot cancel package other than through shipment/picking.')
|
||||
return self.env['stock.picking'].browse(picking_id)
|
||||
|
||||
def send_to_shipper(self):
|
||||
picking = self._get_active_picking()
|
||||
picking.with_context(packages=self).send_to_shipper()
|
||||
|
||||
def cancel_shipment(self):
|
||||
picking = self._get_active_picking()
|
||||
picking.with_context(packages=self).cancel_shipment()
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
@@ -11,6 +34,22 @@ class StockPicking(models.Model):
|
||||
('no', 'No'),
|
||||
], string='Require Insurance', default='auto',
|
||||
help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.')
|
||||
package_carrier_tracking_ref = fields.Char(string='Package Tracking Numbers', compute='_compute_package_carrier_tracking_ref')
|
||||
|
||||
@api.depends('package_ids.carrier_tracking_ref')
|
||||
def _compute_package_carrier_tracking_ref(self):
|
||||
for picking in self:
|
||||
package_refs = picking.package_ids.filtered('carrier_tracking_ref').mapped('carrier_tracking_ref')
|
||||
if package_refs:
|
||||
picking.package_carrier_tracking_ref = ','.join(package_refs)
|
||||
else:
|
||||
picking.package_carrier_tracking_ref = False
|
||||
|
||||
@api.onchange('carrier_id')
|
||||
def _onchange_carrier_id_for_priority(self):
|
||||
for picking in self:
|
||||
if picking.carrier_id and picking.carrier_id.procurement_priority:
|
||||
picking.priority = picking.carrier_id.procurement_priority
|
||||
|
||||
@api.one
|
||||
@api.depends('move_lines.priority', 'carrier_id')
|
||||
@@ -27,6 +66,11 @@ class StockPicking(models.Model):
|
||||
so = self.env['sale.order'].search([('name', '=', str(origin))], limit=1)
|
||||
if so and so.shipping_account_id:
|
||||
values['shipping_account_id'] = so.shipping_account_id.id
|
||||
carrier_id = values.get('carrier_id')
|
||||
if carrier_id:
|
||||
carrier = self.env['delivery.carrier'].browse(carrier_id)
|
||||
if carrier.procurement_priority:
|
||||
values['priority'] = carrier.procurement_priority
|
||||
|
||||
res = super(StockPicking, self).create(values)
|
||||
return res
|
||||
@@ -39,6 +83,83 @@ class StockPicking(models.Model):
|
||||
cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0])
|
||||
return cost
|
||||
|
||||
def clear_carrier_tracking_ref(self):
|
||||
self.write({'carrier_tracking_ref': False})
|
||||
|
||||
def reset_carrier_tracking_ref(self):
|
||||
for picking in self:
|
||||
picking.carrier_tracking_ref = picking.package_carrier_tracking_ref
|
||||
|
||||
# Override to send to specific packaging carriers
|
||||
def send_to_shipper(self):
|
||||
packages = self._context.get('packages')
|
||||
self.ensure_one()
|
||||
if not packages:
|
||||
packages = self.package_ids
|
||||
package_carriers = packages.mapped('carrier_id')
|
||||
if not package_carriers:
|
||||
# Original behavior
|
||||
return super().send_to_shipper()
|
||||
|
||||
tracking_numbers = []
|
||||
carrier_prices = []
|
||||
order_currency = self.sale_id.currency_id or self.company_id.currency_id
|
||||
for carrier in package_carriers:
|
||||
self.carrier_id = carrier
|
||||
carrier_packages = packages.filtered(lambda p: p.carrier_id == carrier)
|
||||
res = carrier.send_shipping(self)
|
||||
if res:
|
||||
res = res[0]
|
||||
if carrier.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= carrier.amount:
|
||||
res['exact_price'] = 0.0
|
||||
carrier_price = float(res['exact_price']) * (1.0 + (self.carrier_id.margin / 100.0))
|
||||
carrier_prices.append(carrier_price)
|
||||
tracking_number = ''
|
||||
if res['tracking_number']:
|
||||
tracking_number = res['tracking_number']
|
||||
tracking_numbers.append(tracking_number)
|
||||
# Try to add tracking to the individual packages.
|
||||
potential_tracking_numbers = tracking_number.split(',')
|
||||
if len(potential_tracking_numbers) >= len(carrier_packages):
|
||||
for t, p in zip(potential_tracking_numbers, carrier_packages):
|
||||
p.carrier_tracking_ref = t
|
||||
else:
|
||||
carrier_packages.write({'carrier_tracking_ref': tracking_number})
|
||||
msg = _("Shipment sent to carrier %s for shipping with tracking number %s<br/>Cost: %.2f %s") % (carrier.name, tracking_number, carrier_price, order_currency.name)
|
||||
self.message_post(body=msg)
|
||||
|
||||
self.carrier_price = sum(carrier_prices or [0.0])
|
||||
self.carrier_tracking_ref = ','.join(tracking_numbers or [''])
|
||||
|
||||
# Override to provide per-package versions...
|
||||
def cancel_shipment(self):
|
||||
packages = self._context.get('packages')
|
||||
pickings_with_package_tracking = self.filtered(lambda p: p.package_carrier_tracking_ref)
|
||||
for picking in pickings_with_package_tracking:
|
||||
if packages:
|
||||
current_packages = packages
|
||||
else:
|
||||
current_packages = picking.package_ids
|
||||
# Packages without a carrier can just be cleared
|
||||
packages_without_carrier = current_packages.filtered(lambda p: not p.carrier_id and p.carrier_tracking_ref)
|
||||
packages_without_carrier.write({
|
||||
'carrier_tracking_ref': False,
|
||||
})
|
||||
# Packages with carrier can use the carrier method
|
||||
packages_with_carrier = current_packages.filtered(lambda p: p.carrier_id and p.carrier_tracking_ref)
|
||||
carriers = packages_with_carrier.mapped('carrier_id')
|
||||
for carrier in carriers:
|
||||
carrier_packages = packages_with_carrier.filtered(lambda p: p.carrier_id == carrier)
|
||||
carrier.cancel_shipment(self, packages=carrier_packages)
|
||||
package_refs = ','.join(carrier_packages.mapped('carrier_tracking_ref'))
|
||||
msg = "Shipment %s cancelled" % package_refs
|
||||
picking.message_post(body=msg)
|
||||
carrier_packages.write({'carrier_tracking_ref': False})
|
||||
|
||||
pickings_without_package_tracking = self - pickings_with_package_tracking
|
||||
if pickings_without_package_tracking:
|
||||
# use original on these
|
||||
super(StockPicking, pickings_without_package_tracking).cancel_shipment()
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
|
||||
@@ -24,11 +24,11 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
|
||||
def test_delivery_hibou(self):
|
||||
# Assign a new shipping account
|
||||
self.partner.shipping_account_id = self.shipping_account
|
||||
self.partner.shipping_account_ids = self.shipping_account
|
||||
|
||||
# Assign values to new Carrier
|
||||
test_insurance_value = 600
|
||||
test_procurement_priority = '2'
|
||||
test_procurement_priority = '1'
|
||||
self.carrier.automatic_insurance_value = test_insurance_value
|
||||
self.carrier.procurement_priority = test_procurement_priority
|
||||
|
||||
@@ -128,9 +128,9 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
picking_in.carrier_id = self.carrier
|
||||
# This relies heavily on the 'stock' demo data.
|
||||
# Should only have a single move_line_ids and it should not be done at all.
|
||||
self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0])
|
||||
self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0])
|
||||
self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0])
|
||||
self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0, 0.0, 0.0])
|
||||
self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0, 10.0, 12.0])
|
||||
self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0, 35.0, 1700.0])
|
||||
|
||||
self.assertEqual(picking_in.carrier_id._classify_picking(picking=picking_in), 'in')
|
||||
self.assertEqual(picking_in.carrier_id.get_shipper_company(picking=picking_in),
|
||||
|
||||
@@ -11,4 +11,16 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="choose_delivery_package_view_form" model="ir.ui.view">
|
||||
<field name="name">hibou.choose.delivery.package.form</field>
|
||||
<field name="model">choose.delivery.package</field>
|
||||
<field name="inherit_id" ref="delivery.choose_delivery_package_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='delivery_packaging_id']" position="attributes">
|
||||
<attribute name="domain">[('product_id', '=', False)]</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,14 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="hibou_view_quant_package_form" model="ir.ui.view">
|
||||
<field name="name">hibou.stock.quant.package.form</field>
|
||||
<field name="model">stock.quant.package</field>
|
||||
<field name="inherit_id" ref="stock.view_quant_package_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='location_id']" position="after">
|
||||
<label for="carrier_id"/>
|
||||
<div name="carrier">
|
||||
<field name="carrier_id" class="oe_inline"/>
|
||||
<button type="object" class="fa fa-arrow-right oe_link" name="send_to_shipper" string="Ship" attrs="{'invisible':['|',('carrier_tracking_ref','!=',False),('carrier_id','=', False)]}"/>
|
||||
</div>
|
||||
<label for="carrier_tracking_ref"/>
|
||||
<div name="tracking">
|
||||
<field name="carrier_tracking_ref" class="oe_inline" />
|
||||
<button type="object" class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|',('carrier_tracking_ref','=',False),('carrier_id','=', False)]}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
|
||||
<field name="name">hibou.delivery.stock.picking_withcarrier.form.view</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form" />
|
||||
<field name="priority" eval="200" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='carrier_id']" position="before">
|
||||
<field name="require_insurance" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
domain="['|', ('partner_id', '=', False), ('partner_id', '=', partner_id)]"/>
|
||||
<field name="package_carrier_tracking_ref" attrs="{'invisible': [('package_carrier_tracking_ref', '=', False)]}" />
|
||||
<button name="clear_carrier_tracking_ref" type="object" string="Clear Tracking" attrs="{'invisible': [('carrier_tracking_ref', '!=', False)]}" />
|
||||
<button name="reset_carrier_tracking_ref" type="object" string="Reset Tracking" attrs="{'invisible': [('package_carrier_tracking_ref', '!=', False)]}" />
|
||||
<field name="package_ids" attrs="{'invisible': [('package_carrier_tracking_ref', '=', False)]}" context="{'active_id': id, 'active_model': 'stock.picking'}" nolabel="1" colspan="2">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="carrier_id" />
|
||||
<field name="carrier_tracking_ref" />
|
||||
<button type="object" name="cancel_shipment" string="Cancel" />
|
||||
</tree>
|
||||
</field>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Stamps.com (USPS) Shipping',
|
||||
'summary': 'Send your shippings through Stamps.com and track them online.',
|
||||
'version': '11.0.1.0.0',
|
||||
'version': '11.0.2.1.1',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Warehouse',
|
||||
'license': 'AGPL-3',
|
||||
'license': 'OPL-1',
|
||||
'images': [],
|
||||
'website': "https://hibou.io",
|
||||
'description': """
|
||||
@@ -15,7 +17,6 @@ Send your shippings through Stamps.com and track them online.
|
||||
|
||||
""",
|
||||
'depends': [
|
||||
'delivery',
|
||||
'delivery_hibou',
|
||||
],
|
||||
'demo': [],
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import delivery_stamps
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
Copyright (c) 2019 by Hibou Corp.
|
||||
Copyright (c) 2014 by Jonathan Zempel.
|
||||
|
||||
Some rights reserved.
|
||||
|
||||
@@ -15,7 +15,7 @@ from urllib.parse import urljoin
|
||||
import os
|
||||
|
||||
|
||||
VERSION = 84
|
||||
VERSION = 111
|
||||
|
||||
|
||||
class StampsConfiguration(object):
|
||||
|
||||
@@ -145,12 +145,12 @@ class StampsService(BaseService):
|
||||
def create_add_on(self):
|
||||
"""Create a new add-on object.
|
||||
"""
|
||||
return self.create("AddOnV15")
|
||||
return self.create("AddOnV17")
|
||||
|
||||
def create_customs(self):
|
||||
"""Create a new customs object.
|
||||
"""
|
||||
return self.create("CustomsV3")
|
||||
return self.create("CustomsV7")
|
||||
|
||||
def create_array_of_customs_lines(self):
|
||||
"""Create a new array of customs objects.
|
||||
@@ -188,7 +188,7 @@ class StampsService(BaseService):
|
||||
def create_shipping(self):
|
||||
"""Create a new shipping object.
|
||||
"""
|
||||
return self.create("RateV31")
|
||||
return self.create("RateV40")
|
||||
|
||||
def get_address(self, address):
|
||||
"""Get a shipping address.
|
||||
@@ -202,7 +202,7 @@ class StampsService(BaseService):
|
||||
"""
|
||||
return self.call("GetAccountInfo")
|
||||
|
||||
def get_label(self, from_address, to_address, rate, transaction_id, image_type=None,
|
||||
def get_label(self, rate, transaction_id, image_type=None,
|
||||
customs=None, sample=False, extended_postage_info=False):
|
||||
"""Get a shipping label.
|
||||
|
||||
@@ -215,7 +215,7 @@ class StampsService(BaseService):
|
||||
:param sample: Default ``False``. Get a sample label without postage.
|
||||
"""
|
||||
return self.call("CreateIndicium", IntegratorTxID=transaction_id,
|
||||
Rate=rate, From=from_address, To=to_address, ImageType=image_type, Customs=customs,
|
||||
Rate=rate, ImageType=image_type, Customs=customs,
|
||||
SampleOnly=sample, ExtendedPostageInfo=extended_postage_info)
|
||||
|
||||
def get_postage_status(self, transaction_id):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,6 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import hashlib
|
||||
from datetime import date
|
||||
from logging import getLogger
|
||||
from urllib.request import urlopen
|
||||
@@ -31,6 +34,13 @@ STAMPS_PACKAGE_TYPES = [
|
||||
'Regional Rate Box C',
|
||||
]
|
||||
|
||||
STAMPS_CONTENT_TYPES = {
|
||||
'Letter': 'Document',
|
||||
'Postcard': 'Document',
|
||||
}
|
||||
|
||||
STAMPS_INTEGRATION_ID = 'f62cb4f0-aa07-4701-a1dd-f0e7c853ed3c'
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
@@ -44,12 +54,14 @@ class ProviderStamps(models.Model):
|
||||
|
||||
delivery_type = fields.Selection(selection_add=[('stamps', 'Stamps.com (USPS)')])
|
||||
|
||||
stamps_integration_id = fields.Char(string='Stamps.com Integration ID', groups='base.group_system')
|
||||
stamps_username = fields.Char(string='Stamps.com Username', groups='base.group_system')
|
||||
stamps_password = fields.Char(string='Stamps.com Password', groups='base.group_system')
|
||||
|
||||
stamps_service_type = fields.Selection([('US-FC', 'First-Class'),
|
||||
('US-FCI', 'First-Class International'),
|
||||
('US-PM', 'Priority'),
|
||||
('US-PMI', 'Priority Mail International'),
|
||||
('US-EMI', ' Priority Mail Express International'),
|
||||
],
|
||||
required=True, string="Service Type", default="US-PM")
|
||||
stamps_default_packaging_id = fields.Many2one('product.packaging', string='Default Package Type')
|
||||
@@ -67,12 +79,21 @@ class ProviderStamps(models.Model):
|
||||
('BZpl', 'BZPL'),
|
||||
],
|
||||
required=True, string="Image Type", default="Pdf")
|
||||
stamps_addon_sc = fields.Boolean(string='Add Signature Confirmation')
|
||||
stamps_addon_dc = fields.Boolean(string='Add Delivery Confirmation')
|
||||
stamps_addon_hp = fields.Boolean(string='Add Hidden Postage')
|
||||
|
||||
def _stamps_package_type(self, package=None):
|
||||
if not package:
|
||||
return self.stamps_default_packaging_id.shipper_package_code
|
||||
return package.packaging_id.shipper_package_code if package.packaging_id.shipper_package_code in STAMPS_PACKAGE_TYPES else 'Package'
|
||||
|
||||
def _stamps_content_type(self, package=None):
|
||||
package_type = self._stamps_package_type(package=package)
|
||||
if package_type in STAMPS_CONTENT_TYPES:
|
||||
return STAMPS_CONTENT_TYPES[package_type]
|
||||
return 'Merchandise'
|
||||
|
||||
def _stamps_package_is_cubic_pricing(self, package=None):
|
||||
if not package:
|
||||
return self.stamps_default_packaging_id.stamps_cubic_pricing
|
||||
@@ -87,15 +108,21 @@ class ProviderStamps(models.Model):
|
||||
|
||||
def _get_stamps_service(self):
|
||||
sudoself = self.sudo()
|
||||
config = StampsConfiguration(integration_id=sudoself.stamps_integration_id,
|
||||
config = StampsConfiguration(integration_id=STAMPS_INTEGRATION_ID,
|
||||
username=sudoself.stamps_username,
|
||||
password=sudoself.stamps_password)
|
||||
password=sudoself.stamps_password,
|
||||
wsdl=('testing' if not sudoself.prod_environment else None))
|
||||
return StampsService(configuration=config)
|
||||
|
||||
def _stamps_convert_weight(self, weight):
|
||||
""" weight always expressed in KG """
|
||||
""" weight always expressed in database units (KG/LBS) """
|
||||
if self.stamps_default_packaging_id.max_weight and self.stamps_default_packaging_id.max_weight < weight:
|
||||
raise ValidationError('Stamps cannot ship for weight: ' + str(weight) + 'kgs.')
|
||||
raise ValidationError('Stamps cannot ship for weight: ' + str(weight) + ' kgs/lbs.')
|
||||
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
product_weight_in_lbs_param = get_param('product.weight_in_lbs')
|
||||
if product_weight_in_lbs_param == '1':
|
||||
return weight
|
||||
|
||||
weight_in_pounds = weight * 2.20462
|
||||
return weight_in_pounds
|
||||
@@ -108,12 +135,37 @@ class ProviderStamps(models.Model):
|
||||
raise ValidationError('Stamps needs ZIP. From: ' + str(order.warehouse_id.partner_id.zip) + ' To: ' + str(order.partner_shipping_id.zip))
|
||||
|
||||
ret_val = service.create_shipping()
|
||||
ret_val.ShipDate = date_planned.split()[0] if date_planned else date.today().isoformat()
|
||||
ret_val.FromZIPCode = self.get_shipper_warehouse(order=order).zip.split('-')[0]
|
||||
ret_val.ToZIPCode = order.partner_shipping_id.zip.split('-')[0]
|
||||
ret_val.ShipDate = date_planned.split(' ')[0] if date_planned else date.today().isoformat()
|
||||
shipper_partner = self.get_shipper_warehouse(order=order)
|
||||
ret_val.From = self._stamps_address(service, shipper_partner)
|
||||
ret_val.To = self._stamps_address(service, order.partner_shipping_id)
|
||||
ret_val.PackageType = self._stamps_package_type()
|
||||
ret_val.ServiceType = self.stamps_service_type
|
||||
ret_val.WeightLb = weight
|
||||
ret_val.ContentType = self._stamps_content_type()
|
||||
return ret_val
|
||||
|
||||
def _get_stamps_shipping_multi(self, service, date_planned, order=False, picking=False, package=False):
|
||||
if order:
|
||||
weight = sum([(line.product_id.weight * line.product_qty) for line in order.order_line]) or 0.0
|
||||
elif not package:
|
||||
weight = picking.shipping_weight
|
||||
else:
|
||||
weight = package.shipping_weight or package.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.split(' ')[0] if date_planned else date.today().isoformat()
|
||||
ret_val.From = self._stamps_address(service, shipper)
|
||||
ret_val.To = self._stamps_address(service, recipient)
|
||||
ret_val.PackageType = self._stamps_package_type(package=package)
|
||||
ret_val.WeightLb = weight
|
||||
ret_val.ContentType = 'Merchandise'
|
||||
return ret_val
|
||||
|
||||
@@ -123,20 +175,51 @@ class ProviderStamps(models.Model):
|
||||
to = self.get_recipient(picking=picking)
|
||||
return company, from_, to
|
||||
|
||||
def _stamps_address(self, service, partner):
|
||||
address = service.create_address()
|
||||
if not partner.name or len(partner.name) < 2:
|
||||
raise ValidationError('Partner (%s) name must be more than 2 characters.' % (partner, ))
|
||||
address.FullName = partner.name
|
||||
address.Address1 = partner.street
|
||||
if partner.street2:
|
||||
address.Address2 = partner.street2
|
||||
address.City = partner.city
|
||||
address.State = partner.state_id.code
|
||||
if partner.country_id.code == 'US':
|
||||
zip_pieces = partner.zip.split('-')
|
||||
address.ZIPCode = zip_pieces[0]
|
||||
if len(zip_pieces) >= 2:
|
||||
address.ZIPCodeAddOn = zip_pieces[1]
|
||||
else:
|
||||
address.PostalCode = partner.zip or ''
|
||||
address.Country = partner.country_id.code
|
||||
res = service.get_address(address).Address
|
||||
return res
|
||||
|
||||
def _stamps_hash_partner(self, partner):
|
||||
to_hash = ''.join(f[1] if isinstance(f, tuple) else str(f) for f in partner.read(['name', 'street', 'street2', 'city', 'country_id', 'state_id', 'zip', 'phone', 'email'])[0].values())
|
||||
return hashlib.sha1(to_hash.encode()).hexdigest()
|
||||
|
||||
def _stamps_get_shippings_for_picking(self, service, picking):
|
||||
ret = []
|
||||
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
|
||||
if not all((from_partner.zip, to_partner.zip)):
|
||||
raise ValidationError('Stamps needs ZIP. From: ' + str(from_partner.zip) + ' To: ' + str(to_partner.zip))
|
||||
raise ValidationError('Stamps needs ZIP/PostalCode. From: ' + str(from_partner.zip) + ' To: ' + str(to_partner.zip))
|
||||
|
||||
for package in picking.package_ids:
|
||||
picking_packages = picking.package_ids
|
||||
package_carriers = picking_packages.mapped('carrier_id')
|
||||
if package_carriers:
|
||||
# only ship ours
|
||||
picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
|
||||
|
||||
for package in picking_packages:
|
||||
weight = self._stamps_convert_weight(package.shipping_weight)
|
||||
l, w, h = self._stamps_package_dimensions(package=package)
|
||||
|
||||
ret_val = service.create_shipping()
|
||||
ret_val.ShipDate = date.today().isoformat()
|
||||
ret_val.FromZIPCode = from_partner.zip.split('-')[0]
|
||||
ret_val.ToZIPCode = to_partner.zip.split('-')[0]
|
||||
ret_val.From = self._stamps_address(service, from_partner)
|
||||
ret_val.To = self._stamps_address(service, to_partner)
|
||||
ret_val.PackageType = self._stamps_package_type(package=package)
|
||||
ret_val.CubicPricing = self._stamps_package_is_cubic_pricing(package=package)
|
||||
ret_val.Length = l
|
||||
@@ -144,16 +227,16 @@ class ProviderStamps(models.Model):
|
||||
ret_val.Height = h
|
||||
ret_val.ServiceType = self.stamps_service_type
|
||||
ret_val.WeightLb = weight
|
||||
ret_val.ContentType = 'Merchandise'
|
||||
ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val))
|
||||
if not ret:
|
||||
ret_val.ContentType = self._stamps_content_type(package=package)
|
||||
ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
|
||||
if not ret and not package_carriers:
|
||||
weight = self._stamps_convert_weight(picking.shipping_weight)
|
||||
l, w, h = self._stamps_package_dimensions()
|
||||
|
||||
ret_val = service.create_shipping()
|
||||
ret_val.ShipDate = date.today().isoformat()
|
||||
ret_val.FromZIPCode = from_partner.zip.split('-')[0]
|
||||
ret_val.ToZIPCode = to_partner.zip.split('-')[0]
|
||||
ret_val.From = self._stamps_address(service, from_partner)
|
||||
ret_val.To = self._stamps_address(service, to_partner)
|
||||
ret_val.PackageType = self._stamps_package_type()
|
||||
ret_val.CubicPricing = self._stamps_package_is_cubic_pricing()
|
||||
ret_val.Length = l
|
||||
@@ -161,8 +244,8 @@ class ProviderStamps(models.Model):
|
||||
ret_val.Height = h
|
||||
ret_val.ServiceType = self.stamps_service_type
|
||||
ret_val.WeightLb = weight
|
||||
ret_val.ContentType = 'Merchandise'
|
||||
ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val))
|
||||
ret_val.ContentType = self._stamps_content_type()
|
||||
ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
|
||||
|
||||
return ret
|
||||
|
||||
@@ -224,6 +307,9 @@ class ProviderStamps(models.Model):
|
||||
return result
|
||||
return result
|
||||
|
||||
def _stamps_needs_customs(self, from_partner, to_partner):
|
||||
return from_partner.country_id.code != to_partner.country_id.code
|
||||
|
||||
def stamps_send_shipping(self, pickings):
|
||||
res = []
|
||||
service = self._get_stamps_service()
|
||||
@@ -232,33 +318,13 @@ class ProviderStamps(models.Model):
|
||||
package_labels = []
|
||||
|
||||
shippings = self._stamps_get_shippings_for_picking(service, picking)
|
||||
if not shippings:
|
||||
continue
|
||||
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
|
||||
|
||||
from_address = service.create_address()
|
||||
from_address.FullName = company.name
|
||||
from_address.Address1 = from_partner.street
|
||||
if from_partner.street2:
|
||||
from_address.Address2 = from_partner.street2
|
||||
from_address.City = from_partner.city
|
||||
from_address.State = from_partner.state_id.code
|
||||
from_zip_pieces = from_partner.zip.split('-')
|
||||
from_address.ZIPCode = from_zip_pieces[0]
|
||||
if len(from_zip_pieces) >= 2:
|
||||
from_address.ZIPCodeAddOn = from_zip_pieces[1]
|
||||
from_address = service.get_address(from_address).Address
|
||||
|
||||
to_address = service.create_address()
|
||||
to_address.FullName = to_partner.name
|
||||
to_address.Address1 = to_partner.street
|
||||
if to_partner.street2:
|
||||
to_address.Address2 = to_partner.street2
|
||||
to_address.City = to_partner.city
|
||||
to_address.State = to_partner.state_id.code
|
||||
to_zip_pieces = to_partner.zip.split('-')
|
||||
to_address.ZIPCode = to_zip_pieces[0]
|
||||
if len(to_zip_pieces) >= 2:
|
||||
to_address.ZIPCodeAddOn = to_zip_pieces[1]
|
||||
to_address = service.get_address(to_address).Address
|
||||
customs = None
|
||||
if self._stamps_needs_customs(from_partner, to_partner):
|
||||
customs = service.create_customs()
|
||||
|
||||
try:
|
||||
for txn_id, shipping in shippings:
|
||||
@@ -270,20 +336,78 @@ class ProviderStamps(models.Model):
|
||||
shipping.DeliverDays = rate.DeliverDays
|
||||
if hasattr(rate, 'DimWeighting'):
|
||||
shipping.DimWeighting = rate.DimWeighting
|
||||
shipping.Zone = rate.Zone
|
||||
shipping.RateCategory = rate.RateCategory
|
||||
shipping.ToState = rate.ToState
|
||||
add_on = service.create_add_on()
|
||||
add_on.AddOnType = 'US-A-DC'
|
||||
add_on2 = service.create_add_on()
|
||||
add_on2.AddOnType = 'SC-A-HP'
|
||||
shipping.AddOns.AddOnV15 = [add_on, add_on2]
|
||||
# shipping.ToState = rate.ToState
|
||||
addons = []
|
||||
if self.stamps_addon_sc:
|
||||
add_on = service.create_add_on()
|
||||
add_on.AddOnType = 'US-A-SC'
|
||||
addons.append(add_on)
|
||||
if self.stamps_addon_dc:
|
||||
add_on = service.create_add_on()
|
||||
add_on.AddOnType = 'US-A-DC'
|
||||
addons.append(add_on)
|
||||
if self.stamps_addon_hp:
|
||||
add_on = service.create_add_on()
|
||||
add_on.AddOnType = 'SC-A-HP'
|
||||
addons.append(add_on)
|
||||
shipping.AddOns.AddOnV17 = addons
|
||||
|
||||
extended_postage_info = service.create_extended_postage_info()
|
||||
if self.is_amazon(picking=picking):
|
||||
extended_postage_info.bridgeProfileType = 'Amazon MWS'
|
||||
label = service.get_label(from_address, to_address, shipping,
|
||||
|
||||
if customs:
|
||||
customs.ContentType = shipping.ContentType
|
||||
if not picking.package_ids:
|
||||
raise ValidationError('Cannot use customs without packing items to ship first.')
|
||||
customs_total = 0.0
|
||||
product_values = {}
|
||||
# Note multiple packages will result in all product being on customs form.
|
||||
# Recommended to ship one customs international package at a time.
|
||||
for quant in picking.mapped('package_ids.quant_ids'):
|
||||
# Customs should have the price for the destination but we may not be able
|
||||
# to rely on the price from the SO (e.g. kit BoM)
|
||||
product = quant.product_id
|
||||
quantity = quant.quantity
|
||||
price = product.lst_price
|
||||
if to_partner.property_product_pricelist:
|
||||
# Note the quantity is used for the price, but it is per unit
|
||||
price = to_partner.property_product_pricelist.get_product_price(product, quantity, to_partner)
|
||||
if product not in product_values:
|
||||
product_values[product] = {
|
||||
'quantity': 0.0,
|
||||
'value': 0.0,
|
||||
}
|
||||
product_values[product]['quantity'] += quantity
|
||||
product_values[product]['value'] += price * quantity
|
||||
|
||||
# Note that Stamps will not allow you to use the scale weight if it is not equal
|
||||
# to the sum of the customs lines.
|
||||
# Thus we sum the line
|
||||
new_total_weight = 0.0
|
||||
customs_lines = []
|
||||
for product, values in product_values.items():
|
||||
customs_line = service.create_customs_lines()
|
||||
customs_line.Description = product.name
|
||||
customs_line.Quantity = values['quantity']
|
||||
customs_total += round(values['value'], 2)
|
||||
customs_line.Value = round(values['value'], 2)
|
||||
line_weight = round(self._stamps_convert_weight(product.weight * values['quantity']), 2)
|
||||
customs_line.WeightLb = line_weight
|
||||
new_total_weight += line_weight
|
||||
customs_line.HSTariffNumber = product.hs_code or ''
|
||||
# customs_line.CountryOfOrigin =
|
||||
customs_line.sku = product.default_code or ''
|
||||
customs_lines.append(customs_line)
|
||||
customs.CustomsLines.CustomsLine = customs_lines
|
||||
shipping.DeclaredValue = round(customs_total, 2)
|
||||
shipping.WeightLb = round(new_total_weight, 2)
|
||||
|
||||
label = service.get_label(shipping,
|
||||
transaction_id=txn_id, image_type=self.stamps_image_type,
|
||||
extended_postage_info=extended_postage_info)
|
||||
extended_postage_info=extended_postage_info,
|
||||
customs=customs)
|
||||
package_labels.append((txn_id, label))
|
||||
except WebFault as e:
|
||||
_logger.warn(e)
|
||||
@@ -307,9 +431,13 @@ class ProviderStamps(models.Model):
|
||||
carrier_price += float(label.Rate.Amount)
|
||||
url = label.URL
|
||||
|
||||
response = urlopen(url)
|
||||
attachment = response.read()
|
||||
picking.message_post(body=body, attachments=[('LabelStamps-%s.%s' % (label.TrackingNumber, self.stamps_image_type), attachment)])
|
||||
url_spaces = url.split(' ')
|
||||
attachments = []
|
||||
for i, url in enumerate(url_spaces, 1):
|
||||
response = urlopen(url)
|
||||
attachment = response.read()
|
||||
attachments.append(('LabelStamps-%s-%s.%s' % (label.TrackingNumber, i, self.stamps_image_type), attachment))
|
||||
picking.message_post(body=body, attachments=attachments)
|
||||
shipping_data = {'exact_price': carrier_price, 'tracking_number': ','.join(tracking_numbers)}
|
||||
res = res + [shipping_data]
|
||||
return res
|
||||
@@ -324,9 +452,81 @@ class ProviderStamps(models.Model):
|
||||
def stamps_cancel_shipment(self, picking):
|
||||
service = self._get_stamps_service()
|
||||
try:
|
||||
service.remove_label(picking.carrier_tracking_ref)
|
||||
all_tracking = picking.carrier_tracking_ref
|
||||
for tracking in all_tracking.split(','):
|
||||
service.remove_label(tracking.strip())
|
||||
picking.message_post(body=_(u'Shipment N° %s has been cancelled' % picking.carrier_tracking_ref))
|
||||
picking.write({'carrier_tracking_ref': '',
|
||||
'carrier_price': 0.0})
|
||||
except WebFault as e:
|
||||
raise ValidationError(e)
|
||||
|
||||
def stamps_rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||
if not packages:
|
||||
return self._stamps_rate_shipment_multi_package(order=order, picking=picking)
|
||||
else:
|
||||
rates = []
|
||||
for package in packages:
|
||||
rates += self._stamps_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||
return rates
|
||||
|
||||
def _stamps_rate_shipment_multi_package(self, order=None, picking=None, package=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, package=package)
|
||||
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,
|
||||
'package': package or self.env['stock.quant.package'].browse(),
|
||||
'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
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<page string="Stamps.com Configuration" attrs="{'invisible': [('delivery_type', '!=', 'stamps')]}">
|
||||
<group>
|
||||
<group>
|
||||
<field name="stamps_integration_id" attrs="{'required': [('delivery_type', '=', 'stamps')]}" />
|
||||
<field name="stamps_username" attrs="{'required': [('delivery_type', '=', 'stamps')]}" />
|
||||
<field name="stamps_password" attrs="{'required': [('delivery_type', '=', 'stamps')]}" password="True"/>
|
||||
</group>
|
||||
@@ -18,6 +17,9 @@
|
||||
<field name="stamps_service_type" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/>
|
||||
<field name="stamps_default_packaging_id" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/>
|
||||
<field name="stamps_image_type" attrs="{'required': [('delivery_type', '==', 'stamps')]}"/>
|
||||
<field name="stamps_addon_sc"/>
|
||||
<field name="stamps_addon_dc"/>
|
||||
<field name="stamps_addon_hp"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
@@ -30,7 +32,7 @@
|
||||
<field name="model">product.packaging</field>
|
||||
<field name="inherit_id" ref="delivery.product_packaging_delivery_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='max_weight']" position='after'>
|
||||
<xpath expr="//field[@name='barcode']" position='before'>
|
||||
<field name="stamps_cubic_pricing"/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
'name': 'Hibou UPS Shipping',
|
||||
'version': '11.0.1.0.0',
|
||||
'version': '11.0.1.1.0',
|
||||
'category': 'Stock',
|
||||
'author': "Hibou Corp.",
|
||||
'license': 'AGPL-3',
|
||||
'license': 'OPL-1',
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': [
|
||||
'delivery_ups',
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import delivery_ups
|
||||
from . import ups_request_patch
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.addons.delivery_ups.models.ups_request import UPSRequest, Package
|
||||
@@ -216,3 +218,132 @@ 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, packages=None):
|
||||
if not packages:
|
||||
return self._ups_rate_shipment_multi_package(order=order, picking=picking)
|
||||
else:
|
||||
rates = []
|
||||
for package in packages:
|
||||
rates += self._ups_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||
return rates
|
||||
|
||||
def _ups_rate_shipment_multi_package(self, order=None, picking=None, package=None):
|
||||
# TODO package here is ignored, it should not be (UPS is not multi-rating capable until we can get rates for a single package)
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import suds
|
||||
from odoo.addons.delivery_ups.models.ups_request import UPSRequest
|
||||
|
||||
|
||||
4
stock_delivery_planner/__init__.py
Normal file
4
stock_delivery_planner/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
29
stock_delivery_planner/__manifest__.py
Normal file
29
stock_delivery_planner/__manifest__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
'name': 'Stock Delivery Planner',
|
||||
'summary': 'Get rates and choose carrier for delivery.',
|
||||
'version': '11.0.1.0.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Warehouse',
|
||||
'license': 'OPL-1',
|
||||
'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': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/stock_views.xml',
|
||||
'wizard/stock_delivery_planner_views.xml',
|
||||
],
|
||||
'auto_install': False,
|
||||
'installable': True,
|
||||
}
|
||||
4
stock_delivery_planner/models/__init__.py
Normal file
4
stock_delivery_planner/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import res_config_settings
|
||||
from . import stock
|
||||
43
stock_delivery_planner/models/res_config_settings.py
Normal file
43
stock_delivery_planner/models/res_config_settings.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
stock_delivery_planner_base_carrier_ids = fields.Many2many('delivery.carrier',
|
||||
string='Delivery Planner Base Carriers',
|
||||
compute='_compute_stock_delivery_planner_base_carrier_ids',
|
||||
inverse='_inverse_stock_delivery_planner_base_carrier_ids')
|
||||
|
||||
def _compute_stock_delivery_planner_base_carrier_ids_ids(self):
|
||||
# used to compute the field and update in get_values
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
company_id = self.company_id.id or self.env.user.company_id.id
|
||||
carrier_ids = get_param('stock.delivery.planner.carrier_ids.%s' % (company_id,)) or []
|
||||
if carrier_ids and isinstance(carrier_ids, str):
|
||||
try:
|
||||
carrier_ids = [int(c) for c in carrier_ids.split(',')]
|
||||
except:
|
||||
carrier_ids = []
|
||||
return carrier_ids
|
||||
|
||||
def _compute_stock_delivery_planner_base_carrier_ids(self):
|
||||
for settings in self:
|
||||
carrier_ids = settings._compute_stock_delivery_planner_base_carrier_ids_ids()
|
||||
carriers = self.env['delivery.carrier'].browse(carrier_ids)
|
||||
settings.stock_delivery_planner_base_carrier_ids = carriers
|
||||
|
||||
def _inverse_stock_delivery_planner_base_carrier_ids(self):
|
||||
set_param = self.env['ir.config_parameter'].sudo().set_param
|
||||
company_id = self.company_id.id or self.env.user.company_id.id
|
||||
for settings in self:
|
||||
carrier_ids = ','.join(str(i) for i in settings.stock_delivery_planner_base_carrier_ids.ids)
|
||||
set_param('stock.delivery.planner.carrier_ids.%s' % (company_id, ), carrier_ids)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
res['stock_delivery_planner_base_carrier_ids'] = [(6, 0, self._compute_stock_delivery_planner_base_carrier_ids_ids())]
|
||||
return res
|
||||
50
stock_delivery_planner/models/stock.py
Normal file
50
stock_delivery_planner/models/stock.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
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)
|
||||
3
stock_delivery_planner/security/ir.model.access.csv
Normal file
3
stock_delivery_planner/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_stock_delivery_planner_user,stock.delivery.planner user,model_stock_delivery_planner,stock.group_stock_user,1,1,1,0
|
||||
access_stock_delivery_planner_option_user,stock.delivery.planner.option user,model_stock_delivery_planner_option,stock.group_stock_user,1,1,1,0
|
||||
|
1
stock_delivery_planner/tests/__init__.py
Normal file
1
stock_delivery_planner/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_stock_delivery_planner
|
||||
139
stock_delivery_planner/tests/test_stock_delivery_planner.py
Normal file
139
stock_delivery_planner/tests/test_stock_delivery_planner.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import Form, TransactionCase
|
||||
|
||||
|
||||
class TestStockDeliveryPlanner(TransactionCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
NOTE: demo Fedex credentials may not work. Test credentials may not return all service types.
|
||||
Configuring production credentials may be necessary for this test to run.
|
||||
"""
|
||||
super(TestStockDeliveryPlanner, self).setUp()
|
||||
try:
|
||||
self.fedex_ground = 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_ground.id)
|
||||
self.env['ir.config_parameter'].sudo().set_param('stock.delivery.planner.carrier_ids.%s' % (self.env.user.company_id.id, ),
|
||||
"%d" % self.fedex_ground.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_ground.fedex_default_packaging_id = self.default_package
|
||||
self.fedex_ground.fedex_service_type = 'FEDEX_GROUND'
|
||||
|
||||
self.fedex_2_day = self.fedex_ground.copy()
|
||||
self.fedex_2_day.name = 'Test FedEx Delivery'
|
||||
self.fedex_2_day.fedex_service_type = 'FEDEX_2_DAY'
|
||||
|
||||
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': 23.99, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 23.99, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 23.99, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 23.99, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 23.99, 'day_period': 'morning'}),
|
||||
],
|
||||
})
|
||||
self.fedex_ground.delivery_calendar_id = delivery_calendar
|
||||
self.fedex_2_day.delivery_calendar_id = delivery_calendar
|
||||
self.env['stock.warehouse'].search([]).write({'shipping_calendar_id': delivery_calendar.id})
|
||||
|
||||
# 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,
|
||||
'is_company': True,
|
||||
# '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_ground).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_ground, '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.action_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.action_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_2_day)
|
||||
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, 2)
|
||||
self.assertEqual(plan_option.sale_requested_date, self.sale_order.requested_date)
|
||||
# Order Planner expects to ship tomorrow: we are shipping a day early and using
|
||||
# 2-day shipping instead of 3, giving us 2 days difference
|
||||
self.assertEqual(plan_option.days_different, -2.0)
|
||||
|
||||
plan_option.select_plan()
|
||||
self.assertEqual(self.picking.carrier_id, self.fedex_2_day)
|
||||
28
stock_delivery_planner/views/res_config_settings_views.xml
Normal file
28
stock_delivery_planner/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="50"/>
|
||||
<field name="inherit_id" ref="delivery.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='sale_ebay']" position="after">
|
||||
<h2>Delivery Planner</h2>
|
||||
<div class="col-lg-6 col-12 o_setting_box" id="stock_delivery_planner">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="stock_delivery_planner_base_carrier_ids" />
|
||||
<div class="text-muted">
|
||||
Add a carrier that represents the 'base rate' for a carrier's type. <br/>
|
||||
For example, you should add 1 FedEx carrier here and let us build up the
|
||||
rates for your other FedEx shipping methods.
|
||||
</div>
|
||||
<field name="stock_delivery_planner_base_carrier_ids" class="oe_inline" />
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
13
stock_delivery_planner/views/stock_views.xml
Normal file
13
stock_delivery_planner/views/stock_views.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_picking_form" model="ir.ui.view">
|
||||
<field name="name">stock.picking.form.inherit.delivery.planner</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_plan_delivery" type="object" string="Plan Shipment" class="oe_highlight" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
stock_delivery_planner/wizard/__init__.py
Normal file
1
stock_delivery_planner/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import stock_delivery_planner
|
||||
137
stock_delivery_planner/wizard/stock_delivery_planner.py
Normal file
137
stock_delivery_planner/wizard/stock_delivery_planner.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools import safe_eval
|
||||
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')
|
||||
packages_planned = fields.Boolean(compute='_compute_packages_planned')
|
||||
|
||||
@api.depends('plan_option_ids.selection')
|
||||
def _compute_packages_planned(self):
|
||||
for wiz in self:
|
||||
packages = wiz.picking_id.package_ids
|
||||
if not packages:
|
||||
wiz.packages_planned = False
|
||||
selected_options = wiz.plan_option_ids.filtered(lambda p: p.selection == 'selected')
|
||||
wiz.packages_planned = len(selected_options) == len(packages)
|
||||
|
||||
def create(self, values):
|
||||
planner = super(StockDeliveryPlanner, self).create(values)
|
||||
|
||||
base_carriers = self.env['delivery.carrier']
|
||||
carrier_ids = self.env['ir.config_parameter'].sudo().get_param('stock.delivery.planner.carrier_ids.%s' % (self.env.user.company_id.id, ))
|
||||
if carrier_ids:
|
||||
try:
|
||||
carrier_ids = [int(c) for c in carrier_ids.split(',')]
|
||||
base_carriers = base_carriers.browse(carrier_ids)
|
||||
except:
|
||||
pass
|
||||
|
||||
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)
|
||||
# added late in API dev cycle
|
||||
package = rate.get('package') or self.env['stock.quant.package'].browse()
|
||||
planner.plan_option_ids |= planner.plan_option_ids.create({
|
||||
'plan_id': self.id,
|
||||
'carrier_id': rate['carrier'].id,
|
||||
'package_id': package.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
|
||||
|
||||
def action_plan(self):
|
||||
self.ensure_one()
|
||||
selected_package_options = self.plan_option_ids.filtered(lambda o: o.package_id and o.selection == 'selected')
|
||||
selected_package_options._plan()
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
||||
|
||||
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')
|
||||
package_id = fields.Many2one('stock.quant.package', 'Package')
|
||||
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
|
||||
selection = fields.Selection([
|
||||
('', 'None'),
|
||||
('selected', 'Selected'),
|
||||
('deselected', 'De-selected')
|
||||
])
|
||||
|
||||
def _plan(self):
|
||||
# this is intended to be used during selecting a whole plan
|
||||
for option in self:
|
||||
option.package_id.write({
|
||||
'carrier_id': option.carrier_id.id,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def select_plan(self):
|
||||
self.ensure_one()
|
||||
self.selection = 'selected'
|
||||
if self.package_id:
|
||||
# need to deselect other options for this package
|
||||
deselected = self.plan_id.plan_option_ids.filtered(lambda o: o.package_id == self.package_id and o != self)
|
||||
deselected.write({'selection': 'deselected'})
|
||||
return {
|
||||
'name': _('Delivery Rate Planner'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.delivery.planner',
|
||||
'res_id': self.plan_id.id,
|
||||
'target': 'new',
|
||||
}
|
||||
else:
|
||||
# Select plan for whole shipment
|
||||
self.plan_id.picking_id.carrier_id = self.carrier_id
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
||||
@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:
|
||||
option.days_different = 0
|
||||
else:
|
||||
request_date = option.requested_date.split(' ')[0]
|
||||
sale_request_date = option.sale_requested_date.split(' ')[0]
|
||||
if request_date == sale_request_date:
|
||||
option.days_different = 0
|
||||
elif request_date < sale_request_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)
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_stock_delivery_planner" model="ir.ui.view">
|
||||
<field name="name">view.stock.delivery.planner</field>
|
||||
<field name="model">stock.delivery.planner</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="packages_planned" invisible="1" />
|
||||
<group>
|
||||
<field name="plan_option_ids" nolabel="1">
|
||||
<tree decoration-info="selection == 'selected'"
|
||||
decoration-muted="selection == 'deselected'"
|
||||
default_order="package_id, price"
|
||||
create="false" edit="false" delete="false">
|
||||
<field name="package_id" />
|
||||
<field name="carrier_id" />
|
||||
<field name="date_planned" invisible="1" />
|
||||
<field name="requested_date" decoration-danger="days_different > 1.0" decoration-warning="days_different > 0.0" decoration-success="days_different < 0.0" />
|
||||
<field name="transit_days" decoration-warning="transit_days > 2.0" decoration-success="transit_days == 1.0"/>
|
||||
<field name="sale_requested_date" decoration-danger="days_different > 1.0" decoration-warning="days_different > 0.0" decoration-success="days_different < 0.0" />
|
||||
<field name="days_different" decoration-danger="days_different > 1.0" decoration-warning="days_different > 0.0" decoration-success="days_different < 0.0" />
|
||||
<field name="price" decoration-success="price < 10.99" decoration-warning="price > 31.00 and price < 50.00" decoration-danger="price >= 50.00" />
|
||||
<field name="selection" invisible="1" />
|
||||
<button class="eo_highlight"
|
||||
name="select_plan"
|
||||
string="Select"
|
||||
type="object" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button type="object" name="action_plan" string="Plan" class="btn-primary" attrs="{'invisible': [('packages_planned', '=', False)]}"/>
|
||||
<button string="Discard" special="cancel" class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user