mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'imp/15.0/delivery_hibou__sig_req' into '15.0'
WIP: imp/15.0/delivery_hibou__sig_req into 15.0 See merge request hibou-io/hibou-odoo/suite!1357
This commit is contained in:
3
delivery_fedex_hibou/__init__.py
Normal file
3
delivery_fedex_hibou/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
19
delivery_fedex_hibou/__manifest__.py
Normal file
19
delivery_fedex_hibou/__manifest__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
'name': 'Hibou Fedex Shipping',
|
||||
'version': '15.0.1.0.0',
|
||||
'category': 'Stock',
|
||||
'author': "Hibou Corp.",
|
||||
'license': 'OPL-1',
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': [
|
||||
'delivery_fedex',
|
||||
'delivery_hibou',
|
||||
],
|
||||
'data': [
|
||||
'views/stock_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
4
delivery_fedex_hibou/models/__init__.py
Normal file
4
delivery_fedex_hibou/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 delivery_fedex
|
||||
from . import stock
|
||||
742
delivery_fedex_hibou/models/delivery_fedex.py
Normal file
742
delivery_fedex_hibou/models/delivery_fedex.py
Normal file
@@ -0,0 +1,742 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import pytz
|
||||
from odoo import fields, models, tools, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.addons.delivery_fedex.models.delivery_fedex import _convert_curr_iso_fdx
|
||||
from .fedex_request import FedexRequest
|
||||
|
||||
pdf = tools.pdf
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeliveryFedex(models.Model):
|
||||
_inherit = 'delivery.carrier'
|
||||
|
||||
fedex_service_type = fields.Selection(selection_add=[
|
||||
('GROUND_HOME_DELIVERY', 'GROUND_HOME_DELIVERY'),
|
||||
# ('FEDEX_EXPRESS_SAVER', 'FEDEX_EXPRESS_SAVER'), # included in 13.0, ensure it stays there...
|
||||
])
|
||||
|
||||
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:
|
||||
if not third_party_account.delivery_type == 'fedex':
|
||||
raise ValidationError('Non-FedEx Shipping Account indicated during FedEx shipment.')
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_fedex_payment_account_number(self, order=None, picking=None):
|
||||
"""
|
||||
Common hook to customize what Fedex Account number to use.
|
||||
:return: FedEx Account Number
|
||||
"""
|
||||
# Provided by Hibou Odoo Suite `delivery_hibou`
|
||||
third_party_account = self.get_third_party_account(order=order, picking=picking)
|
||||
if third_party_account:
|
||||
if not third_party_account.delivery_type == 'fedex':
|
||||
raise ValidationError('Non-FedEx Shipping Account indicated during FedEx shipment.')
|
||||
return third_party_account.name
|
||||
if picking and picking.picking_type_id.warehouse_id.fedex_account_number:
|
||||
return picking.picking_type_id.warehouse_id.fedex_account_number
|
||||
return self.fedex_account_number
|
||||
|
||||
def _get_fedex_account_number(self, order=None, picking=None):
|
||||
if order:
|
||||
# third_party_account = self.get_third_party_account(order=order, picking=picking)
|
||||
# if third_party_account:
|
||||
# if not third_party_account.delivery_type == 'fedex':
|
||||
# raise ValidationError('Non-FedEx Shipping Account indicated during FedEx shipment.')
|
||||
# return third_party_account.name
|
||||
if order.warehouse_id.fedex_account_number:
|
||||
return order.warehouse_id.fedex_account_number
|
||||
return self.fedex_account_number
|
||||
if picking:
|
||||
if picking.picking_type_id.warehouse_id.fedex_account_number:
|
||||
return picking.picking_type_id.warehouse_id.fedex_account_number
|
||||
return self.fedex_account_number
|
||||
|
||||
def _get_fedex_meter_number(self, order=None, picking=None):
|
||||
if order:
|
||||
if order.warehouse_id.fedex_meter_number:
|
||||
return order.warehouse_id.fedex_meter_number
|
||||
return self.fedex_meter_number
|
||||
if picking:
|
||||
if picking.picking_type_id.warehouse_id.fedex_meter_number:
|
||||
return picking.picking_type_id.warehouse_id.fedex_meter_number
|
||||
return self.fedex_meter_number
|
||||
|
||||
def _get_fedex_recipient_is_residential(self, partner):
|
||||
if self.fedex_service_type.find('HOME') >= 0:
|
||||
return True
|
||||
return not (partner.is_company or partner.parent_id.is_company)
|
||||
|
||||
"""
|
||||
Overrides to use Hibou Delivery methods to get shipper etc. and to add 'transit_days' to result.
|
||||
"""
|
||||
def fedex_rate_shipment(self, order):
|
||||
max_weight = self._fedex_convert_weight(self.fedex_default_package_type_id.max_weight, self.fedex_weight_unit)
|
||||
price = 0.0
|
||||
is_india = order.partner_shipping_id.country_id.code == 'IN' and order.company_id.partner_id.country_id.code == 'IN'
|
||||
|
||||
# Estimate weight of the sales order; will be definitely recomputed on the picking field "weight"
|
||||
est_weight_value = sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line if not line.display_type]) or 0.0
|
||||
weight_value = self._fedex_convert_weight(est_weight_value, self.fedex_weight_unit)
|
||||
|
||||
# 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)
|
||||
|
||||
order_currency = order.currency_id
|
||||
superself = self.sudo()
|
||||
|
||||
# Hibou Delivery methods for collecting details in an overridable way
|
||||
shipper_company = superself.get_shipper_company(order=order)
|
||||
shipper_warehouse = superself.get_shipper_warehouse(order=order)
|
||||
recipient = superself.get_recipient(order=order)
|
||||
acc_number = superself._get_fedex_account_number(order=order)
|
||||
meter_number = superself._get_fedex_meter_number(order=order)
|
||||
order_name = superself.get_order_name(order=order)
|
||||
residential = self._get_fedex_recipient_is_residential(recipient)
|
||||
date_planned = None
|
||||
if self.env.context.get('date_planned'):
|
||||
date_planned = self.env.context.get('date_planned')
|
||||
|
||||
# Authentication stuff
|
||||
srm = FedexRequest(self.log_xml, request_type="rating", prod_environment=self.prod_environment)
|
||||
srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
|
||||
srm.client_detail(acc_number, meter_number)
|
||||
|
||||
# Build basic rating request and set addresses
|
||||
srm.transaction_detail(order_name)
|
||||
srm.shipment_request(
|
||||
self.fedex_droppoff_type,
|
||||
self.fedex_service_type,
|
||||
self.fedex_default_package_type_id.shipper_package_code,
|
||||
self.fedex_weight_unit,
|
||||
self.fedex_saturday_delivery,
|
||||
)
|
||||
pkg = self.fedex_default_package_type_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 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.packaging_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.packaging_length,
|
||||
sequence_number=total_package,
|
||||
mode='rating',
|
||||
)
|
||||
srm.set_master_package(weight_value, total_package)
|
||||
else:
|
||||
srm.add_package(
|
||||
weight_value,
|
||||
package_code=pkg.shipper_package_code,
|
||||
package_height=pkg.height,
|
||||
package_width=pkg.width,
|
||||
package_length=pkg.packaging_length,
|
||||
mode='rating',
|
||||
)
|
||||
srm.set_master_package(weight_value, 1)
|
||||
|
||||
# Commodities for customs declaration (international shipping)
|
||||
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'] and not l.display_type):
|
||||
commodity_amount = line.price_reduce_taxinc
|
||||
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'
|
||||
commodity_harmonized_code = line.product_id.hs_code or ''
|
||||
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, acc_number, superself.fedex_duty_payment)
|
||||
|
||||
request = srm.rate(date_planned=date_planned)
|
||||
|
||||
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:
|
||||
return {'success': False,
|
||||
'price': 0.0,
|
||||
'error_message': _('Error:\n%s') % request['errors_message'],
|
||||
'warning_message': False}
|
||||
|
||||
date_delivered = request.get('date_delivered', False)
|
||||
if date_delivered and 'delivery_calendar_id' in self and self.delivery_calendar_id.tz:
|
||||
tz = pytz.timezone(self.delivery_calendar_id.tz)
|
||||
date_delivered = tz.localize(date_delivered).astimezone(pytz.utc).replace(tzinfo=None)
|
||||
return {'success': True,
|
||||
'price': price,
|
||||
'error_message': False,
|
||||
'transit_days': request.get('transit_days', False),
|
||||
'date_delivered': date_delivered,
|
||||
'warning_message': _('Warning:\n%s') % warnings if warnings else False}
|
||||
|
||||
"""
|
||||
Overrides to use Hibou Delivery methods to get shipper etc. and add insurance.
|
||||
"""
|
||||
def fedex_send_shipping(self, pickings):
|
||||
res = []
|
||||
|
||||
for picking in pickings:
|
||||
|
||||
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)
|
||||
acc_number = superself._get_fedex_account_number(picking=picking)
|
||||
meter_number = superself._get_fedex_meter_number(picking=picking)
|
||||
payment_acc_number = superself._get_fedex_payment_account_number(picking=picking)
|
||||
order_name = superself.get_order_name(picking=picking)
|
||||
attn = superself.get_attn(picking=picking)
|
||||
residential = self._get_fedex_recipient_is_residential(recipient)
|
||||
|
||||
srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
|
||||
srm.client_detail(acc_number, meter_number)
|
||||
|
||||
srm.transaction_detail(picking.id)
|
||||
|
||||
package_type = picking_packages and picking_packages[0].package_type_id.shipper_package_code or self.fedex_default_package_type_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)
|
||||
srm.set_recipient(recipient, attn=attn, residential=residential)
|
||||
|
||||
srm.shipping_charges_payment(payment_acc_number, third_party=bool(self.get_third_party_account(picking=picking)))
|
||||
|
||||
srm.shipment_label('COMMON2D', self.fedex_label_file_type, self.fedex_label_stock_type, 'TOP_EDGE_OF_TEXT_FIRST', 'SHIPPING_LABEL_FIRST')
|
||||
|
||||
order = picking.sale_id
|
||||
company = shipper_company
|
||||
order_currency = picking.sale_id.currency_id or picking.company_id.currency_id
|
||||
|
||||
net_weight = self._fedex_convert_weight(picking.shipping_weight, self.fedex_weight_unit)
|
||||
|
||||
# Commodities for customs declaration (international shipping)
|
||||
if self.fedex_service_type in ['INTERNATIONAL_ECONOMY', 'INTERNATIONAL_PRIORITY'] or (picking.partner_id.country_id.code == 'IN' and picking.picking_type_id.warehouse_id.partner_id.country_id.code == 'IN'):
|
||||
|
||||
commodity_currency = order_currency
|
||||
total_commodities_amount = 0.0
|
||||
commodity_country_of_manufacture = picking.picking_type_id.warehouse_id.partner_id.country_id.code
|
||||
|
||||
for operation in picking.move_line_ids:
|
||||
commodity_amount = operation.move_id.sale_line_id.price_reduce_taxinc or operation.product_id.lst_price
|
||||
total_commodities_amount += (commodity_amount * operation.qty_done)
|
||||
commodity_description = operation.product_id.name
|
||||
commodity_number_of_piece = '1'
|
||||
commodity_weight_units = self.fedex_weight_unit
|
||||
commodity_weight_value = self._fedex_convert_weight(operation.product_id.weight * operation.qty_done, self.fedex_weight_unit)
|
||||
commodity_quantity = operation.qty_done
|
||||
commodity_quantity_units = 'EA'
|
||||
commodity_harmonized_code = operation.product_id.hs_code or ''
|
||||
srm.commodities(_convert_curr_iso_fdx(commodity_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(commodity_currency.name), total_commodities_amount, "NON_DOCUMENTS")
|
||||
srm.duties_payment(shipper_warehouse, acc_number, superself.fedex_duty_payment)
|
||||
send_etd = superself.env['ir.config_parameter'].get_param("delivery_fedex.send_etd")
|
||||
srm.commercial_invoice(self.fedex_document_stock_type, send_etd)
|
||||
|
||||
package_count = len(picking_packages) or 1
|
||||
|
||||
# For india picking courier is not accepted without this details in label.
|
||||
po_number = order.display_name or False
|
||||
dept_number = False
|
||||
if picking.partner_id.country_id.code == 'IN' and picking.picking_type_id.warehouse_id.partner_id.country_id.code == 'IN':
|
||||
po_number = 'B2B' if picking.partner_id.commercial_partner_id.is_company else 'B2C'
|
||||
dept_number = 'BILL D/T: SENDER'
|
||||
|
||||
# TODO RIM master: factorize the following crap
|
||||
|
||||
################
|
||||
# Multipackage #
|
||||
################
|
||||
if package_count > 1:
|
||||
|
||||
# Note: Fedex has a complex multi-piece shipping interface
|
||||
# - Each package has to be sent in a separate request
|
||||
# - First package is called "master" package and holds shipping-
|
||||
# related information, including addresses, customs...
|
||||
# - Last package responses contains shipping price and code
|
||||
# - If a problem happens with a package, every previous package
|
||||
# of the shipping has to be cancelled separately
|
||||
# (Why doing it in a simple way when the complex way exists??)
|
||||
|
||||
master_tracking_id = False
|
||||
package_labels = []
|
||||
carrier_tracking_ref = ""
|
||||
|
||||
for sequence, package in enumerate(picking_packages, start=1):
|
||||
|
||||
package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit)
|
||||
packaging = package.package_type_id
|
||||
packaging_code = packaging.shipper_package_code if (packaging.package_carrier_type == 'fedex' and packaging.shipper_package_code) else self.fedex_default_package_type_id.shipper_package_code
|
||||
|
||||
# Hibou Delivery
|
||||
# Add more details to package.
|
||||
srm._add_package(
|
||||
package_weight,
|
||||
package_code=packaging_code,
|
||||
package_height=packaging.height,
|
||||
package_width=packaging.width,
|
||||
package_length=packaging.packaging_length,
|
||||
sequence_number=sequence,
|
||||
po_number=po_number,
|
||||
dept_number=dept_number,
|
||||
# reference=picking.display_name,
|
||||
reference=('%s-%d' % (order_name, sequence)), # above "reference" is new in 13.0, using new name but old value
|
||||
insurance=superself.get_insurance_value(picking=picking, package=package),
|
||||
signature_required=superself.get_signature_required(picking=picking, package=package)
|
||||
)
|
||||
srm.set_master_package(net_weight, package_count, master_tracking_id=master_tracking_id)
|
||||
request = srm.process_shipment()
|
||||
package_name = package.name or sequence
|
||||
|
||||
warnings = request.get('warnings_message')
|
||||
if warnings:
|
||||
_logger.info(warnings)
|
||||
|
||||
# First package
|
||||
if sequence == 1:
|
||||
if not request.get('errors_message'):
|
||||
master_tracking_id = request['master_tracking_id']
|
||||
package_labels.append((package_name, srm.get_label()))
|
||||
carrier_tracking_ref = request['tracking_number']
|
||||
else:
|
||||
raise UserError(request['errors_message'])
|
||||
|
||||
# Intermediary packages
|
||||
elif sequence > 1 and sequence < package_count:
|
||||
if not request.get('errors_message'):
|
||||
package_labels.append((package_name, srm.get_label()))
|
||||
carrier_tracking_ref = carrier_tracking_ref + "," + request['tracking_number']
|
||||
else:
|
||||
raise UserError(request['errors_message'])
|
||||
|
||||
# Last package
|
||||
elif sequence == package_count:
|
||||
# recuperer le label pdf
|
||||
if not request.get('errors_message'):
|
||||
package_labels.append((package_name, srm.get_label()))
|
||||
|
||||
if _convert_curr_iso_fdx(order_currency.name) in request['price']:
|
||||
carrier_price = request['price'][_convert_curr_iso_fdx(order_currency.name)]
|
||||
else:
|
||||
_logger.info("Preferred currency has not been found in FedEx response")
|
||||
company_currency = picking.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)]
|
||||
carrier_price = company_currency._convert(
|
||||
amount, order_currency, company, order.date_order or fields.Date.today())
|
||||
else:
|
||||
amount = request['price']['USD']
|
||||
carrier_price = company_currency._convert(
|
||||
amount, order_currency, company, order.date_order or fields.Date.today())
|
||||
|
||||
carrier_tracking_ref = carrier_tracking_ref + "," + request['tracking_number']
|
||||
|
||||
logmessage = _("Shipment created into Fedex<br/>"
|
||||
"<b>Tracking Numbers:</b> %s<br/>"
|
||||
"<b>Packages:</b> %s") % (carrier_tracking_ref, ','.join([pl[0] for pl in package_labels]))
|
||||
if self.fedex_label_file_type != 'PDF':
|
||||
attachments = [('LabelFedex-%s.%s' % (pl[0], self.fedex_label_file_type), pl[1]) for pl in package_labels]
|
||||
if self.fedex_label_file_type == 'PDF':
|
||||
attachments = [('LabelFedex.pdf', pdf.merge_pdf([pl[1] for pl in package_labels]))]
|
||||
picking.message_post(body=logmessage, attachments=attachments)
|
||||
shipping_data = {'exact_price': carrier_price,
|
||||
'tracking_number': carrier_tracking_ref}
|
||||
res = res + [shipping_data]
|
||||
else:
|
||||
raise UserError(request['errors_message'])
|
||||
|
||||
# TODO RIM handle if a package is not accepted (others should be deleted)
|
||||
|
||||
###############
|
||||
# One package #
|
||||
###############
|
||||
elif package_count == 1:
|
||||
packaging = picking_packages[:1].package_type_id or self.fedex_default_package_type_id
|
||||
packaging_code = packaging.shipper_package_code if packaging.package_carrier_type == 'fedex' else self.fedex_default_package_type_id.shipper_package_code
|
||||
srm._add_package(
|
||||
net_weight,
|
||||
package_code=packaging_code,
|
||||
package_height=packaging.height,
|
||||
package_width=packaging.width,
|
||||
package_length=packaging.packaging_length,
|
||||
po_number=po_number,
|
||||
dept_number=dept_number,
|
||||
# reference=picking.display_name,
|
||||
reference=order_name, # above "reference" is new in 13.0, using new name but old value
|
||||
insurance=superself.get_insurance_value(picking=picking, package=picking_packages[:1]),
|
||||
signature_required=superself.get_signature_required(picking=picking, package=picking_packages[:1])
|
||||
)
|
||||
srm.set_master_package(net_weight, 1)
|
||||
|
||||
# Ask the shipping to fedex
|
||||
request = srm.process_shipment()
|
||||
|
||||
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']:
|
||||
carrier_price = request['price'][_convert_curr_iso_fdx(order_currency.name)]
|
||||
else:
|
||||
_logger.info("Preferred currency has not been found in FedEx response")
|
||||
company_currency = picking.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)]
|
||||
carrier_price = company_currency._convert(
|
||||
amount, order_currency, company, order.date_order or fields.Date.today())
|
||||
else:
|
||||
amount = request['price']['USD']
|
||||
carrier_price = company_currency._convert(
|
||||
amount, order_currency, company, order.date_order or fields.Date.today())
|
||||
|
||||
carrier_tracking_ref = request['tracking_number']
|
||||
logmessage = (_("Shipment created into Fedex <br/> <b>Tracking Number : </b>%s") % (carrier_tracking_ref))
|
||||
|
||||
fedex_labels = [('LabelFedex-%s-%s.%s' % (carrier_tracking_ref, index, self.fedex_label_file_type), label)
|
||||
for index, label in enumerate(srm._get_labels(self.fedex_label_file_type))]
|
||||
picking.message_post(body=logmessage, attachments=fedex_labels)
|
||||
shipping_data = {'exact_price': carrier_price,
|
||||
'tracking_number': carrier_tracking_ref}
|
||||
res = res + [shipping_data]
|
||||
else:
|
||||
raise UserError(request['errors_message'])
|
||||
|
||||
##############
|
||||
# No package #
|
||||
##############
|
||||
else:
|
||||
raise UserError(_('No packages for this picking'))
|
||||
if self.return_label_on_delivery:
|
||||
self.get_return_label(picking, tracking_number=request['tracking_number'], origin_date=request['date'])
|
||||
commercial_invoice = srm.get_document()
|
||||
if commercial_invoice:
|
||||
fedex_documents = [('DocumentFedex.pdf', commercial_invoice)]
|
||||
picking.message_post(body='Fedex Documents', attachments=fedex_documents)
|
||||
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_package_type_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, package=package)
|
||||
signature_required = superself.get_signature_required(order=order, picking=picking, package=package)
|
||||
residential = self._get_fedex_recipient_is_residential(recipient)
|
||||
date_planned = fields.Datetime.now()
|
||||
if self.env.context.get('date_planned'):
|
||||
date_planned = self.env.context.get('date_planned')
|
||||
|
||||
# Authentication stuff
|
||||
srm = FedexRequest(self.log_xml, request_type="rating", prod_environment=self.prod_environment)
|
||||
srm.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
|
||||
srm.client_detail(acc_number, meter_number)
|
||||
|
||||
# Build basic rating request and set addresses
|
||||
srm.transaction_detail(order_name)
|
||||
|
||||
# TODO what shipment requests can we make?
|
||||
# TODO package and main weights?
|
||||
# TODO need package & weight count to pass in
|
||||
srm.shipment_request(
|
||||
self.fedex_droppoff_type,
|
||||
None, # because we want all of the rates back...
|
||||
self.fedex_default_package_type_id.shipper_package_code,
|
||||
self.fedex_weight_unit,
|
||||
self.fedex_saturday_delivery,
|
||||
ship_timestamp=date_planned,
|
||||
)
|
||||
pkg = self.fedex_default_package_type_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.packaging_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.packaging_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.packaging_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.package_type_id
|
||||
package_code = package.package_type_id.shipper_package_code if packaging.package_carrier_type == 'fedex' else self.fedex_default_package_type_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.packaging_length,
|
||||
sequence_number=1,
|
||||
# po_number=po_number,
|
||||
# dept_number=dept_number,
|
||||
reference=('%s-%d' % (order_name, 1)),
|
||||
insurance=insurance_value,
|
||||
signature_required=signature_required
|
||||
)
|
||||
srm.set_master_package(package_weight, 1)
|
||||
else:
|
||||
# deliver all together...
|
||||
package_weight = self._fedex_convert_weight(picking.shipping_weight or picking.weight, self.fedex_weight_unit)
|
||||
packaging = self.fedex_default_package_type_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.packaging_length,
|
||||
sequence_number=1,
|
||||
# po_number=po_number,
|
||||
# dept_number=dept_number,
|
||||
reference=('%s-%d' % (order_name, 1)),
|
||||
insurance=insurance_value,
|
||||
signature_required=signature_required
|
||||
)
|
||||
srm.set_master_package(package_weight, 1)
|
||||
|
||||
|
||||
# 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)
|
||||
if date_delivered:
|
||||
tz = pytz.timezone(self.delivery_calendar_id.tz)
|
||||
date_delivered = tz.localize(date_delivered).astimezone(pytz.utc).replace(tzinfo=None)
|
||||
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
|
||||
|
||||
def fedex_cancel_shipment(self, picking):
|
||||
# Overriddent to use the correct account numbers for cancelling
|
||||
request = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
|
||||
superself = self.sudo()
|
||||
request.web_authentication_detail(superself.fedex_developer_key, superself.fedex_developer_password)
|
||||
acc_number = superself._get_fedex_account_number(picking=picking)
|
||||
meter_number = superself._get_fedex_meter_number(picking=picking)
|
||||
request.client_detail(acc_number, meter_number)
|
||||
request.transaction_detail(picking.id)
|
||||
|
||||
master_tracking_id = picking.carrier_tracking_ref.split(',')[0]
|
||||
request.set_deletion_details(master_tracking_id)
|
||||
result = request.delete_shipment()
|
||||
|
||||
warnings = result.get('warnings_message')
|
||||
if warnings:
|
||||
_logger.info(warnings)
|
||||
|
||||
if result.get('delete_success') and not result.get('errors_message'):
|
||||
picking.message_post(body=_(u'Shipment #%s has been cancelled', master_tracking_id))
|
||||
picking.write({'carrier_tracking_ref': '',
|
||||
'carrier_price': 0.0})
|
||||
else:
|
||||
raise UserError(result['errors_message'])
|
||||
268
delivery_fedex_hibou/models/fedex_request.py
Normal file
268
delivery_fedex_hibou/models/fedex_request.py
Normal file
@@ -0,0 +1,268 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from zeep.exceptions import Fault
|
||||
from datetime import datetime
|
||||
from copy import deepcopy
|
||||
from odoo.addons.delivery_fedex.models import fedex_request
|
||||
from odoo.tools import remove_accents
|
||||
|
||||
STATECODE_REQUIRED_COUNTRIES = fedex_request.STATECODE_REQUIRED_COUNTRIES
|
||||
|
||||
|
||||
def sanitize_name(name):
|
||||
if isinstance(name, str):
|
||||
return name.replace('[', '').replace(']', '')
|
||||
return 'Unknown'
|
||||
|
||||
|
||||
class FedexRequest(fedex_request.FedexRequest):
|
||||
_transit_days = {
|
||||
'ONE_DAYS': 1,
|
||||
'ONE_DAY': 1,
|
||||
'TWO_DAYS': 2,
|
||||
'THREE_DAYS': 3,
|
||||
'FOUR_DAYS': 4,
|
||||
'FIVE_DAYS': 5,
|
||||
'SIX_DAYS': 6,
|
||||
'SEVEN_DAYS': 7,
|
||||
'EIGHT_DAYS': 8,
|
||||
'NINE_DAYS': 9,
|
||||
'TEN_DAYS': 10,
|
||||
}
|
||||
|
||||
_service_transit_days = {
|
||||
'FEDEX_2_DAY': 2,
|
||||
'FEDEX_2_DAY_AM': 2,
|
||||
'FEDEX_3_DAY_FREIGHT': 3,
|
||||
'FIRST_OVERNIGHT': 1,
|
||||
'PRIORITY_OVERNIGHT': 1,
|
||||
'STANDARD_OVERNIGHT': 1,
|
||||
}
|
||||
|
||||
def set_recipient(self, recipient_partner, attn=None, residential=False):
|
||||
"""
|
||||
Adds ATTN: and sanitizes against known 'illegal' common characters in names.
|
||||
:param recipient_partner: default
|
||||
:param attn: NEW add to contact name as an ' ATTN: $attn'
|
||||
:param residential: NEW allow ground home delivery
|
||||
:return:
|
||||
"""
|
||||
Contact = self.factory.Contact()
|
||||
if recipient_partner.is_company:
|
||||
Contact.PersonName = ''
|
||||
Contact.CompanyName = sanitize_name(remove_accents(recipient_partner.name))
|
||||
else:
|
||||
Contact.PersonName = sanitize_name(remove_accents(recipient_partner.name))
|
||||
Contact.CompanyName = sanitize_name(remove_accents(recipient_partner.parent_id.name)) or ''
|
||||
|
||||
if attn:
|
||||
Contact.PersonName = Contact.PersonName + ' ATTN: ' + str(attn)
|
||||
|
||||
Contact.PhoneNumber = recipient_partner.phone or ''
|
||||
|
||||
Address = self.factory.Address()
|
||||
Address.StreetLines = [remove_accents(recipient_partner.street) or '', remove_accents(recipient_partner.street2) or '']
|
||||
Address.City = remove_accents(recipient_partner.city) or ''
|
||||
if recipient_partner.country_id.code in STATECODE_REQUIRED_COUNTRIES:
|
||||
Address.StateOrProvinceCode = recipient_partner.state_id.code or ''
|
||||
else:
|
||||
Address.StateOrProvinceCode = ''
|
||||
Address.PostalCode = recipient_partner.zip or ''
|
||||
Address.CountryCode = recipient_partner.country_id.code or ''
|
||||
|
||||
if residential:
|
||||
Address.Residential = True
|
||||
|
||||
self.RequestedShipment.Recipient = self.factory.Party()
|
||||
self.RequestedShipment.Recipient.Contact = Contact
|
||||
self.RequestedShipment.Recipient.Address = Address
|
||||
|
||||
def add_package(self, weight_value, package_code=False, package_height=0, package_width=0, package_length=0, sequence_number=False, mode='shipping', reference=False, insurance=False, signature_required=False):
|
||||
# TODO remove in master and change the signature of a public method
|
||||
return self._add_package(weight_value=weight_value, package_code=package_code, package_height=package_height, package_width=package_width,
|
||||
package_length=package_length, sequence_number=sequence_number, mode=mode, po_number=False, dept_number=False, reference=reference, insurance=insurance, signature_required=signature_required)
|
||||
|
||||
def _add_package(self, weight_value, package_code=False, package_height=0, package_width=0, package_length=0, sequence_number=False, mode='shipping', po_number=False, dept_number=False, reference=False, insurance=False, signature_required=False):
|
||||
package = self.factory.RequestedPackageLineItem()
|
||||
package_weight = self.factory.Weight()
|
||||
package_weight.Value = weight_value
|
||||
package_weight.Units = self.RequestedShipment.TotalWeight.Units
|
||||
|
||||
if insurance:
|
||||
insured = self.factory.Money()
|
||||
insured.Amount = insurance
|
||||
# TODO at some point someone might need currency here
|
||||
insured.Currency = 'USD'
|
||||
package.InsuredValue = insured
|
||||
|
||||
special_service = self.factory.PackageSpecialServicesRequested()
|
||||
signature_detail = self.factory.SignatureOptionDetail()
|
||||
signature_detail.OptionType = 'DIRECT' if signature_required else 'NO_SIGNATURE_REQUIRED'
|
||||
special_service.SignatureOptionDetail = signature_detail
|
||||
package.SpecialServicesRequested = special_service
|
||||
|
||||
package.PhysicalPackaging = 'BOX'
|
||||
if package_code == 'YOUR_PACKAGING':
|
||||
package.Dimensions = self.factory.Dimensions()
|
||||
package.Dimensions.Height = package_height
|
||||
package.Dimensions.Width = package_width
|
||||
package.Dimensions.Length = package_length
|
||||
# TODO in master, add unit in product packaging and perform unit conversion
|
||||
package.Dimensions.Units = "IN" if self.RequestedShipment.TotalWeight.Units == 'LB' else 'CM'
|
||||
if po_number:
|
||||
po_reference = self.factory.CustomerReference()
|
||||
po_reference.CustomerReferenceType = 'P_O_NUMBER'
|
||||
po_reference.Value = po_number
|
||||
package.CustomerReferences.append(po_reference)
|
||||
if dept_number:
|
||||
dept_reference = self.factory.CustomerReference()
|
||||
dept_reference.CustomerReferenceType = 'DEPARTMENT_NUMBER'
|
||||
dept_reference.Value = dept_number
|
||||
package.CustomerReferences.append(dept_reference)
|
||||
if reference:
|
||||
customer_reference = self.factory.CustomerReference()
|
||||
customer_reference.CustomerReferenceType = 'CUSTOMER_REFERENCE'
|
||||
customer_reference.Value = reference
|
||||
package.CustomerReferences.append(customer_reference)
|
||||
|
||||
package.Weight = package_weight
|
||||
if mode == 'rating':
|
||||
package.GroupPackageCount = 1
|
||||
if sequence_number:
|
||||
package.SequenceNumber = sequence_number
|
||||
else:
|
||||
self.hasOnePackage = True
|
||||
|
||||
if mode == 'rating':
|
||||
self.RequestedShipment.RequestedPackageLineItems.append(package)
|
||||
else:
|
||||
self.RequestedShipment.RequestedPackageLineItems = package
|
||||
|
||||
def shipping_charges_payment(self, shipping_charges_payment_account, third_party=False):
|
||||
"""
|
||||
Allow 'shipping_charges_payment_account' to be considered 'third_party'
|
||||
:param shipping_charges_payment_account: default
|
||||
:param third_party: NEW add to indicate that the 'shipping_charges_payment_account' is third party.
|
||||
:return:
|
||||
"""
|
||||
self.RequestedShipment.ShippingChargesPayment = self.factory.Payment()
|
||||
self.RequestedShipment.ShippingChargesPayment.PaymentType = 'SENDER' if not third_party else 'THIRD_PARTY'
|
||||
Payor = self.factory.Payor()
|
||||
Payor.ResponsibleParty = self.factory.Party()
|
||||
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.factory.RequestedShipment()
|
||||
self.RequestedShipment.SpecialServicesRequested = self.factory.ShipmentSpecialServicesRequested()
|
||||
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 = self.factory.Weight()
|
||||
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'):
|
||||
self.RequestedShipment.SpecialServicesRequested.SpecialServiceTypes.append('SATURDAY_DELIVERY')
|
||||
|
||||
# Rating stuff
|
||||
|
||||
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 self.hasCommodities:
|
||||
self.RequestedShipment.CustomsClearanceDetail.Commodities = self.listCommodities
|
||||
|
||||
try:
|
||||
self.response = self.client.service.getRates(WebAuthenticationDetail=self.WebAuthenticationDetail,
|
||||
ClientDetail=self.ClientDetail,
|
||||
TransactionDetail=self.TransactionDetail,
|
||||
Version=self.VersionId,
|
||||
RequestedShipment=self.RequestedShipment,
|
||||
ReturnTransitAndCommit=True) # New ReturnTransitAndCommit for CommitDetails in response
|
||||
if (self.response.HighestSeverity != 'ERROR' and self.response.HighestSeverity != 'FAILURE'):
|
||||
if not getattr(self.response, "RateReplyDetails", False):
|
||||
raise Exception("No rating found")
|
||||
if not multi:
|
||||
for rating in self.response.RateReplyDetails[0].RatedShipmentDetails:
|
||||
formatted_response['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = float(rating.ShipmentRateDetail.TotalNetFedExCharge.Amount)
|
||||
if len(self.response.RateReplyDetails[0].RatedShipmentDetails) == 1:
|
||||
if 'CurrencyExchangeRate' in self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail and self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail['CurrencyExchangeRate']:
|
||||
formatted_response['price'][self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = float(self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount) / float(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
|
||||
if 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
|
||||
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] = float(rating.ShipmentRateDetail.TotalNetFedExCharge.Amount)
|
||||
if len(rate_reply_detail.RatedShipmentDetails) == 1:
|
||||
if 'CurrencyExchangeRate' in rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail and rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail['CurrencyExchangeRate']:
|
||||
res['price'][rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = float(rate_reply_detail.RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount) / float(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
|
||||
res['transit_days'] = self._service_transit_days.get(rate_reply_detail.ServiceType, 0)
|
||||
if not res['transit_days'] 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
|
||||
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.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')])
|
||||
formatted_response['errors_message'] = errors_message
|
||||
|
||||
if any([n.Severity == 'WARNING' for n in self.response.Notifications]):
|
||||
warnings_message = '\n'.join([("%s: %s" % (n.Code, n.Message)) for n in self.response.Notifications if n.Severity == 'WARNING'])
|
||||
formatted_response['warnings_message'] = warnings_message
|
||||
|
||||
except Fault as fault:
|
||||
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
|
||||
10
delivery_fedex_hibou/models/stock.py
Normal file
10
delivery_fedex_hibou/models/stock.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
fedex_account_number = fields.Char(string='FedEx Account Number')
|
||||
fedex_meter_number = fields.Char(string='FedEx Meter Number')
|
||||
14
delivery_fedex_hibou/views/stock_views.xml
Normal file
14
delivery_fedex_hibou/views/stock_views.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="fedex_view_warehouse" model="ir.ui.view">
|
||||
<field name="name">stock.warehouse</field>
|
||||
<field name="model">stock.warehouse</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="fedex_account_number"/>
|
||||
<field name="fedex_meter_number"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
'name': 'Golden State Overnight (gso.com) Shipping',
|
||||
'summary': 'Send your shippings through gso.com and track them online.',
|
||||
'version': '15.0.1.0.0',
|
||||
'version': '15.0.1.0.1',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Warehouse',
|
||||
'license': 'OPL-1',
|
||||
|
||||
@@ -196,14 +196,6 @@ class ProviderGSO(models.Model):
|
||||
request_body['Shipment'].update(self._gso_make_shipper_address(from_, company))
|
||||
request_body['Shipment'].update(self._gso_make_ship_address(to))
|
||||
|
||||
# Automatic insurance at $100.0
|
||||
insurance_value = sudoself.get_insurance_value(picking=picking)
|
||||
if insurance_value:
|
||||
request_body['Shipment']['SignatureCode'] = 'SIG_REQD'
|
||||
if insurance_value > 100.0:
|
||||
# Documentation says to set DeclaredValue ONLY if over $100.00
|
||||
request_body['Shipment']['DeclaredValue'] = insurance_value
|
||||
|
||||
cost = 0.0
|
||||
labels = {
|
||||
'thermal': [],
|
||||
@@ -218,6 +210,20 @@ class ProviderGSO(models.Model):
|
||||
if picking_packages:
|
||||
# Every package will be a transaction
|
||||
for package in picking_packages:
|
||||
# Use Sale Order Number or fall back to Picking
|
||||
shipment_ref = (picking.sale_id.name if picking.sale_id else picking.name) + '-' + package.name
|
||||
insurance_value = sudoself.get_insurance_value(picking=picking, package=package)
|
||||
if insurance_value > 100.0:
|
||||
# Documentation says to set DeclaredValue ONLY if over $100.00
|
||||
request_body['Shipment']['DeclaredValue'] = insurance_value
|
||||
elif 'DeclaredValue' in request_body['Shipment']:
|
||||
del request_body['Shipment']['DeclaredValue']
|
||||
|
||||
if sudoself.get_signature_required(picking=picking, package=package):
|
||||
request_body['Shipment']['SignatureCode'] = 'SIG_REQD'
|
||||
else:
|
||||
request_body['Shipment']['SignatureCode'] = 'SIG_NOT_REQD'
|
||||
|
||||
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
|
||||
@@ -236,9 +242,10 @@ class ProviderGSO(models.Model):
|
||||
raise ValidationError(e)
|
||||
elif not package_carriers:
|
||||
# ship the whole picking
|
||||
shipment_ref = picking.sale_id.name if picking.sale_id else picking.name
|
||||
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
|
||||
request_body['Shipment']['ShipmentReference'] = shipment_ref
|
||||
request_body['Shipment']['TrackingNumber'] = self._gso_create_tracking_number(picking.name)
|
||||
try:
|
||||
response = service.post_shipment(request_body)
|
||||
@@ -393,7 +400,7 @@ class ProviderGSO(models.Model):
|
||||
elif not package:
|
||||
est_weight_value = self._gso_convert_weight(picking.shipping_weight)
|
||||
else:
|
||||
est_weight_value = package.shipping_weight or package.weight
|
||||
est_weight_value = self._gso_convert_weight(package.shipping_weight or package.weight)
|
||||
|
||||
request_body = {
|
||||
'AccountNumber': sudoself.gso_account_number,
|
||||
@@ -409,7 +416,7 @@ class ProviderGSO(models.Model):
|
||||
result = service.get_rates_and_transit_time(request_body)
|
||||
# _logger.warning('GSO result:\n%s' % result)
|
||||
except HTTPError as e:
|
||||
_logger.error(e)
|
||||
# _logger.error(e)
|
||||
return [{
|
||||
'success': False,
|
||||
'price': 0.0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
'name': 'Delivery Hibou',
|
||||
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
|
||||
'version': '15.0.1.0.0',
|
||||
'version': '15.0.1.1.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Stock',
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from odoo import fields, models, _
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
@@ -9,6 +10,9 @@ class DeliveryCarrier(models.Model):
|
||||
automatic_insurance_value = fields.Float(string='Automatic Insurance Value',
|
||||
help='Will be used during shipping to determine if the '
|
||||
'picking\'s value warrants insurance being added.')
|
||||
automatic_sig_req_value = fields.Float(string='Automatic Signature Required Value',
|
||||
help='Will be used during shipping to determine if the '
|
||||
'picking\'s value warrants signature required being added.')
|
||||
procurement_priority = fields.Selection(PROCUREMENT_PRIORITIES,
|
||||
string='Procurement Priority',
|
||||
help='Priority for this carrier. Will affect pickings '
|
||||
@@ -16,21 +20,42 @@ class DeliveryCarrier(models.Model):
|
||||
|
||||
# Utility
|
||||
|
||||
def get_insurance_value(self, order=None, picking=None):
|
||||
def get_insurance_value(self, order=None, picking=None, package=None):
|
||||
value = 0.0
|
||||
if order:
|
||||
if order.order_line:
|
||||
value = sum(order.order_line.filtered(lambda l: l.type != 'service').mapped('price_subtotal'))
|
||||
value = sum(order.order_line.filtered(lambda l: l.product_id.type != 'service').mapped('price_subtotal'))
|
||||
else:
|
||||
return value
|
||||
if picking:
|
||||
value = picking.declared_value()
|
||||
if picking.require_insurance == 'no':
|
||||
value = 0.0
|
||||
elif picking.require_insurance == 'auto' and self.automatic_insurance_value and self.automatic_insurance_value > value:
|
||||
value = picking.declared_value(package=package)
|
||||
if package and not package.require_insurance:
|
||||
value = 0.0
|
||||
else:
|
||||
if picking.require_insurance == 'no':
|
||||
value = 0.0
|
||||
elif picking.require_insurance == 'auto' and self.automatic_insurance_value and self.automatic_insurance_value > value:
|
||||
value = 0.0
|
||||
return value
|
||||
|
||||
def get_signature_required(self, order=None, picking=None, package=None):
|
||||
value = 0.0
|
||||
if order:
|
||||
if order.order_line:
|
||||
value = sum(order.order_line.filtered(lambda l: l.product_id.type != 'service').mapped('price_subtotal'))
|
||||
else:
|
||||
return False
|
||||
if picking:
|
||||
value = picking.declared_value(package=package)
|
||||
if package:
|
||||
return package.require_signature
|
||||
else:
|
||||
if picking.require_signature == 'no':
|
||||
return False
|
||||
elif picking.require_signature == 'yes':
|
||||
return True
|
||||
return self.automatic_sig_req_value and value >= self.automatic_sig_req_value
|
||||
|
||||
def get_third_party_account(self, order=None, picking=None):
|
||||
if order and order.shipping_account_id:
|
||||
return order.shipping_account_id
|
||||
@@ -202,9 +227,9 @@ class DeliveryCarrier(models.Model):
|
||||
|
||||
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.package_type_id.package_carrier_type in (False, '', 'none', carrier.delivery_type))
|
||||
carrier_packages = packages and packages.filtered(lambda p: not p.carrier_tracking_ref and
|
||||
(not p.carrier_id or p.carrier_id == carrier) and
|
||||
p.package_type_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):
|
||||
@@ -243,3 +268,70 @@ class DeliveryCarrier(models.Model):
|
||||
})
|
||||
|
||||
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings)
|
||||
|
||||
|
||||
class ChooseDeliveryPackage(models.TransientModel):
|
||||
_inherit = 'choose.delivery.package'
|
||||
|
||||
package_declared_value = fields.Float(string='Declared Value')
|
||||
package_require_insurance = fields.Boolean(string='Require Insurance')
|
||||
package_require_signature = fields.Boolean(string='Require Signature')
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
defaults = super().default_get(fields_list)
|
||||
if 'package_declared_value' in fields_list:
|
||||
picking = self.env['stock.picking'].browse(defaults.get('picking_id'))
|
||||
move_line_ids = picking.move_line_ids.filtered(lambda m:
|
||||
float_compare(m.qty_done, 0.0, precision_rounding=m.product_uom_id.rounding) > 0
|
||||
and not m.result_package_id
|
||||
)
|
||||
total_value = 0.0
|
||||
for ml in move_line_ids:
|
||||
qty = ml.product_uom_id._compute_quantity(ml.qty_done, ml.product_id.uom_id)
|
||||
total_value += qty * ml.product_id.standard_price
|
||||
defaults['package_declared_value'] = total_value
|
||||
return defaults
|
||||
|
||||
@api.onchange('package_declared_value')
|
||||
def _onchange_package_declared_value(self):
|
||||
picking = self.picking_id
|
||||
value = self.package_declared_value
|
||||
if picking.require_insurance == 'auto':
|
||||
self.package_require_insurance = value and picking.carrier_id.automatic_insurance_value and value >= picking.carrier_id.automatic_insurance_value
|
||||
else:
|
||||
self.package_require_insurance = picking.require_insurance == 'yes'
|
||||
if picking.require_signature == 'auto':
|
||||
self.package_require_signature = value and picking.carrier_id.automatic_sig_req_value and value >= picking.carrier_id.automatic_sig_req_value
|
||||
else:
|
||||
self.package_require_signature = picking.require_signature == 'yes'
|
||||
|
||||
def action_put_in_pack(self):
|
||||
# Copied because `delivery_package` is not retained by reference or returned...
|
||||
picking_move_lines = self.picking_id.move_line_ids
|
||||
if not self.picking_id.picking_type_id.show_reserved and not self.env.context.get('barcode_view'):
|
||||
picking_move_lines = self.picking_id.move_line_nosuggest_ids
|
||||
|
||||
move_line_ids = picking_move_lines.filtered(lambda ml:
|
||||
float_compare(ml.qty_done, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) > 0
|
||||
and not ml.result_package_id
|
||||
)
|
||||
if not move_line_ids:
|
||||
move_line_ids = picking_move_lines.filtered(lambda ml: float_compare(ml.product_uom_qty, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) > 0 and float_compare(
|
||||
ml.qty_done, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) == 0)
|
||||
|
||||
delivery_package = self.picking_id._put_in_pack(move_line_ids)
|
||||
# write shipping weight and package type on 'stock_quant_package' if needed
|
||||
if self.delivery_package_type_id:
|
||||
delivery_package.package_type_id = self.delivery_package_type_id
|
||||
if self.shipping_weight:
|
||||
delivery_package.shipping_weight = self.shipping_weight
|
||||
# Hibou : Fill additional fields.
|
||||
delivery_package.write({
|
||||
'declared_value': self.package_declared_value,
|
||||
'require_insurance': self.package_require_insurance,
|
||||
'require_signature': self.package_require_signature,
|
||||
})
|
||||
|
||||
@@ -7,6 +7,9 @@ class StockQuantPackage(models.Model):
|
||||
|
||||
carrier_id = fields.Many2one('delivery.carrier', string='Carrier')
|
||||
carrier_tracking_ref = fields.Char(string='Tracking Reference')
|
||||
require_insurance = fields.Boolean(string='Require Insurance')
|
||||
require_signature = fields.Boolean(string='Require Signature')
|
||||
declared_value = fields.Float(string='Declared Value')
|
||||
|
||||
def _get_active_picking(self):
|
||||
picking_id = self._context.get('active_id')
|
||||
@@ -34,7 +37,14 @@ 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.')
|
||||
require_signature = fields.Selection([
|
||||
('auto', 'Automatic'),
|
||||
('yes', 'Yes'),
|
||||
('no', 'No'),
|
||||
], string='Require Signature', default='auto',
|
||||
help='If your carrier supports it, auto should be calculated off of the "Automatic Signature Required Value" field.')
|
||||
package_carrier_tracking_ref = fields.Char(string='Package Tracking Numbers', compute='_compute_package_carrier_tracking_ref')
|
||||
commercial_partner_id = fields.Many2one('res.partner', related='partner_id.commercial_partner_id')
|
||||
|
||||
@api.depends('package_ids.carrier_tracking_ref')
|
||||
def _compute_package_carrier_tracking_ref(self):
|
||||
@@ -67,8 +77,10 @@ class StockPicking(models.Model):
|
||||
res = super(StockPicking, self).create(values)
|
||||
return res
|
||||
|
||||
def declared_value(self):
|
||||
def declared_value(self, package=None):
|
||||
self.ensure_one()
|
||||
if package:
|
||||
return package.declared_value
|
||||
cost = sum([(l.product_id.standard_price * l.qty_done) for l in self.move_line_ids] or [0.0])
|
||||
if not cost:
|
||||
# Assume Full Value
|
||||
@@ -112,6 +124,8 @@ class StockPicking(models.Model):
|
||||
tracking_numbers.append(tracking_number)
|
||||
# Try to add tracking to the individual packages.
|
||||
potential_tracking_numbers = tracking_number.split(',')
|
||||
if len(potential_tracking_numbers) == 1:
|
||||
potential_tracking_numbers = tracking_number.split('+') # UPS for example...
|
||||
if len(potential_tracking_numbers) >= len(carrier_packages):
|
||||
for t, p in zip(potential_tracking_numbers, carrier_packages):
|
||||
p.carrier_tracking_ref = t
|
||||
@@ -150,9 +164,10 @@ class StockPicking(models.Model):
|
||||
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)
|
||||
# Above cancel should also say which are cancelled in chatter.
|
||||
# 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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='integration_level']" position="after">
|
||||
<field name="automatic_insurance_value"/>
|
||||
<field name="automatic_sig_req_value"/>
|
||||
<field name="procurement_priority"/>
|
||||
</xpath>
|
||||
</field>
|
||||
@@ -20,6 +21,11 @@
|
||||
<xpath expr="//field[@name='delivery_package_type_id']" position="attributes">
|
||||
<attribute name="domain">[]</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='delivery_package_type_id']" position="after">
|
||||
<field name="package_declared_value" />
|
||||
<field name="package_require_insurance" />
|
||||
<field name="package_require_signature" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
<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>
|
||||
<field name="declared_value" />
|
||||
<field name="require_insurance" />
|
||||
<field name="require_signature" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -28,8 +31,12 @@
|
||||
<field name="priority" eval="200" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='carrier_id']" position="before">
|
||||
<field name="commercial_partner_id" invisible="1" />
|
||||
<field name="require_insurance" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="require_signature" 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', 'in', (False, partner_id, commercial_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)]}" />
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import wizard
|
||||
from . import models
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
'name': 'Sale Order Planner',
|
||||
'summary': 'Plans order dates and warehouses.',
|
||||
'version': '15.0.1.0.0',
|
||||
'version': '15.0.2.0.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Sale',
|
||||
'license': 'AGPL-3',
|
||||
'license': 'OPL-1',
|
||||
'complexity': 'expert',
|
||||
'images': [],
|
||||
'website': "https://hibou.io",
|
||||
@@ -37,6 +37,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
|
||||
'views/stock.xml',
|
||||
'views/delivery.xml',
|
||||
'views/product.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'auto_install': False,
|
||||
'installable': True,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import delivery
|
||||
from . import partner
|
||||
from . import planning
|
||||
@@ -5,3 +7,4 @@ from . import product
|
||||
from . import resource
|
||||
from . import sale
|
||||
from . import stock
|
||||
from . import res_config_settings
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
@@ -61,6 +63,9 @@ class DeliveryCarrier(models.Model):
|
||||
|
||||
def calculate_transit_days(self, date_planned, date_delivered):
|
||||
self.ensure_one()
|
||||
if not self.delivery_calendar_id:
|
||||
return 0
|
||||
|
||||
if isinstance(date_planned, str):
|
||||
date_planned = fields.Datetime.from_string(date_planned)
|
||||
if isinstance(date_delivered, str):
|
||||
|
||||
@@ -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
|
||||
|
||||
try:
|
||||
|
||||
@@ -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,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
|
||||
79
sale_planner/models/res_config_settings.py
Normal file
79
sale_planner/models/res_config_settings.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
def sale_planner_warehouse_ids(env, company):
|
||||
get_param = env['ir.config_parameter'].sudo().get_param
|
||||
warehouse_ids = get_param('sale.planner.warehouse_ids.%s' % (company.id, )) or []
|
||||
if warehouse_ids and isinstance(warehouse_ids, str):
|
||||
try:
|
||||
warehouse_ids = [int(i) for i in warehouse_ids.split(',')]
|
||||
except:
|
||||
warehouse_ids = []
|
||||
return warehouse_ids
|
||||
|
||||
|
||||
def sale_planner_carrier_ids(env, company):
|
||||
get_param = env['ir.config_parameter'].sudo().get_param
|
||||
carrier_ids = get_param('sale.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
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
sale_planner_warehouse_ids = fields.Many2many('stock.warehouse',
|
||||
string='Sale Order Planner Warehouses',
|
||||
compute='_compute_sale_planner_warehouse_ids',
|
||||
inverse='_inverse_sale_planner_warehouse_ids')
|
||||
sale_planner_carrier_ids = fields.Many2many('delivery.carrier',
|
||||
string='Sale Order Planner Carriers',
|
||||
compute='_compute_sale_planner_carrier_ids',
|
||||
inverse='_inverse_sale_planner_carrier_ids')
|
||||
|
||||
def _compute_sale_planner_warehouse_ids_ids(self):
|
||||
company = self.company_id or self.env.user.company_id
|
||||
return sale_planner_warehouse_ids(self.env, company)
|
||||
|
||||
def _compute_sale_planner_carrier_ids_ids(self):
|
||||
company = self.company_id or self.env.user.company_id
|
||||
return sale_planner_carrier_ids(self.env, company)
|
||||
|
||||
def _compute_sale_planner_warehouse_ids(self):
|
||||
for settings in self:
|
||||
warehouse_ids = settings._compute_sale_planner_warehouse_ids_ids()
|
||||
warehouses = self.env['stock.warehouse'].browse(warehouse_ids)
|
||||
settings.sale_planner_warehouse_ids = warehouses
|
||||
|
||||
def _compute_sale_planner_carrier_ids(self):
|
||||
for settings in self:
|
||||
carrier_ids = settings._compute_sale_planner_carrier_ids_ids()
|
||||
carriers = self.env['delivery.carrier'].browse(carrier_ids)
|
||||
settings.sale_planner_carrier_ids = carriers
|
||||
|
||||
def _inverse_sale_planner_warehouse_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:
|
||||
warehouse_ids = ','.join(str(i) for i in settings.sale_planner_warehouse_ids.ids)
|
||||
set_param('sale.planner.warehouse_ids.%s' % (company_id, ), warehouse_ids)
|
||||
|
||||
def _inverse_sale_planner_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.sale_planner_carrier_ids.ids)
|
||||
set_param('sale.planner.carrier_ids.%s' % (company_id, ), carrier_ids)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
res['sale_planner_warehouse_ids'] = [(6, 0, self._compute_sale_planner_warehouse_ids_ids())]
|
||||
res['sale_planner_carrier_ids'] = [(6, 0, self._compute_sale_planner_carrier_ids_ids())]
|
||||
return res
|
||||
@@ -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,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
@@ -7,3 +9,7 @@ class Warehouse(models.Model):
|
||||
shipping_calendar_id = fields.Many2one(
|
||||
'resource.calendar', 'Shipping Calendar',
|
||||
help="This calendar represents shipping availability from the warehouse.")
|
||||
sale_planner_carrier_ids = fields.Many2many('delivery.carrier',
|
||||
relation='sale_planner_carrier_wh_rel',
|
||||
string='Sale Order Planner Base Carriers',
|
||||
help='Overrides the global carriers.')
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import test_planner
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import common
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads as json_decode
|
||||
|
||||
39
sale_planner/views/res_config_settings_views.xml
Normal file
39
sale_planner/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?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="49"/>
|
||||
<field name="inherit_id" ref="delivery.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@data-key='sale_management']" position="inside">
|
||||
<h2>Sale Order Planner</h2>
|
||||
<div class="col-lg-6 col-12 o_setting_box" id="sale_planner_carriers">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="sale_planner_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="sale_planner_carrier_ids" class="oe_inline" options="{'no_create_edit': True, 'no_create': True}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box" id="sale_planner_warehouses">
|
||||
<div class="o_setting_left_pane"/>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="sale_planner_warehouse_ids" />
|
||||
<div class="text-muted">
|
||||
Warehouses you typically ship inventory out of that you want to
|
||||
include in the planning of sale orders.
|
||||
</div>
|
||||
<field name="sale_planner_warehouse_ids" class="oe_inline" options="{'no_create_edit': True, 'no_create': True}" />
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -7,6 +7,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="shipping_calendar_id" />
|
||||
<field name="sale_planner_carrier_ids" options="{'no_create_edit': True, 'no_create': True}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import order_planner
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from math import sin, cos, sqrt, atan2, radians
|
||||
from json import dumps, loads
|
||||
from copy import deepcopy
|
||||
@@ -10,11 +12,12 @@ _logger = getLogger(__name__)
|
||||
try:
|
||||
from uszipcode import SearchEngine
|
||||
except ImportError:
|
||||
_logger.warning('module "uszipcode" cannot be loaded, falling back to Google API')
|
||||
_logger.warn('module "uszipcode" cannot be loaded, falling back to Google API')
|
||||
SearchEngine = None
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from ..models.res_config_settings import sale_planner_warehouse_ids, sale_planner_carrier_ids
|
||||
|
||||
|
||||
class FakeCollection():
|
||||
@@ -26,7 +29,12 @@ class FakeCollection():
|
||||
yield v
|
||||
|
||||
def filtered(self, f):
|
||||
return filter(f, self.vals)
|
||||
return self.__class__([v for v in self.vals if f(v)])
|
||||
# return filter(f, self.vals)
|
||||
|
||||
def mapped(self, s):
|
||||
# note this only maps to one level and doesn't really support recordset
|
||||
return [v[s] for v in self.vals]
|
||||
|
||||
def sudo(self, *args, **kwargs):
|
||||
return self
|
||||
@@ -178,6 +186,12 @@ class FakeSaleOrder(FakeCollection):
|
||||
if item == '__last_update':
|
||||
return str(datetime.now())
|
||||
return getattr(self, item)
|
||||
|
||||
def _get_estimated_weight(self):
|
||||
weight = 0.0
|
||||
for order_line in self.order_line.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not l.is_delivery and not l.display_type):
|
||||
weight += order_line.product_qty * order_line.product_id.weight
|
||||
return weight
|
||||
|
||||
|
||||
def distance(lat_1, lon_1, lat_2, lon_2):
|
||||
@@ -308,15 +322,17 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
domain.append(('company_id', 'in', self.env.context['allowed_company_ids']))
|
||||
|
||||
if self.env.context.get('warehouse_domain'):
|
||||
if not domain:
|
||||
domain = []
|
||||
domain.extend(self.env.context.get('warehouse_domain'))
|
||||
if domain:
|
||||
return warehouse.search(domain)
|
||||
|
||||
irconfig_parameter = self.env['ir.config_parameter'].sudo()
|
||||
if irconfig_parameter.get_param('sale.order.planner.warehouse_domain'):
|
||||
domain.extend(safe_eval(irconfig_parameter.get_param('sale.order.planner.warehouse_domain')))
|
||||
# no domain, use global
|
||||
warehouse_ids = sale_planner_warehouse_ids(self.env, self.env.user.company_id)
|
||||
return warehouse.browse(warehouse_ids)
|
||||
|
||||
return warehouse.search(domain)
|
||||
|
||||
def get_shipping_carriers(self, carrier_id=None, domain=None):
|
||||
def get_shipping_carriers(self, carrier_id=None, domain=None, warehouse_id=None):
|
||||
Carrier = self.env['delivery.carrier'].sudo()
|
||||
if carrier_id:
|
||||
return Carrier.browse(carrier_id)
|
||||
@@ -324,18 +340,21 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
if domain:
|
||||
if not isinstance(domain, (list, tuple)):
|
||||
domain = safe_eval(domain)
|
||||
else:
|
||||
domain = []
|
||||
|
||||
if self.env.context.get('carrier_domain'):
|
||||
# potential bug here if this is textual
|
||||
if not domain:
|
||||
domain = []
|
||||
domain.extend(self.env.context.get('carrier_domain'))
|
||||
if domain:
|
||||
return Carrier.search(domain)
|
||||
|
||||
irconfig_parameter = self.env['ir.config_parameter'].sudo()
|
||||
if irconfig_parameter.get_param('sale.order.planner.carrier_domain'):
|
||||
domain.extend(safe_eval(irconfig_parameter.get_param('sale.order.planner.carrier_domain')))
|
||||
|
||||
return Carrier.search(domain)
|
||||
# no domain, use global
|
||||
if warehouse_id:
|
||||
warehouse = self.env['stock.warehouse'].sudo().browse(warehouse_id)
|
||||
if warehouse.sale_planner_carrier_ids:
|
||||
return warehouse.sale_planner_carrier_ids.sudo()
|
||||
carrier_ids = sale_planner_carrier_ids(self.env, self.env.user.company_id)
|
||||
return Carrier.browse(carrier_ids)
|
||||
|
||||
def _generate_base_option(self, order_fake, policy_group):
|
||||
flag_force_closest = False
|
||||
@@ -616,6 +635,8 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
return self._find_closest_warehouse(warehouses, partner.partner_latitude, partner.partner_longitude)
|
||||
|
||||
def _find_closest_warehouse(self, warehouses, latitude, longitude):
|
||||
if not warehouses:
|
||||
return warehouses
|
||||
distances = {distance(latitude, longitude, wh.partner_id.partner_latitude, wh.partner_id.partner_longitude): wh.id for wh in warehouses}
|
||||
wh_id = distances[min(distances)]
|
||||
return warehouses.filtered(lambda wh: wh.id == wh_id)
|
||||
@@ -662,19 +683,19 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
policy = line.product_id.product_tmpl_id.get_planning_policy()
|
||||
if policy and policy.carrier_filter_id:
|
||||
domain.extend(safe_eval(policy.carrier_filter_id.domain))
|
||||
carriers = self.get_shipping_carriers(base_option.get('carrier_id'), domain=domain)
|
||||
carriers = self.get_shipping_carriers(base_option.get('carrier_id'), domain=domain, warehouse_id=base_option.get('warehouse_id'))
|
||||
_logger.info('generate_shipping_options:: base_option: ' + str(base_option) + ' order_fake: ' + str(order_fake) + ' carriers: ' + str(carriers))
|
||||
|
||||
if not carriers:
|
||||
return base_option
|
||||
return [base_option]
|
||||
|
||||
if not base_option.get('sub_options'):
|
||||
options = []
|
||||
# this locic comes from "delivery.models.sale_order.SaleOrder"
|
||||
for carrier in carriers:
|
||||
option = self._generate_shipping_carrier_option(base_option, order_fake, carrier)
|
||||
if option:
|
||||
options.append(option)
|
||||
carrier_options = self._generate_shipping_carrier_option(base_option, order_fake, carrier)
|
||||
if carrier_options:
|
||||
options += carrier_options
|
||||
if options:
|
||||
return options
|
||||
return [base_option]
|
||||
@@ -686,26 +707,40 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
for carrier in carriers:
|
||||
new_base_option = deepcopy(base_option)
|
||||
has_error = False
|
||||
found_carrier_ids = set()
|
||||
for wh_id, wh_vals in base_option['sub_options'].items():
|
||||
if has_error:
|
||||
continue
|
||||
order_fake.warehouse_id = warehouses.filtered(lambda wh: wh.id == wh_id)
|
||||
order_fake.order_line = FakeCollection(filter(lambda line: line.product_id.id in wh_vals['product_ids'], original_order_fake_order_line))
|
||||
wh_option = self._generate_shipping_carrier_option(wh_vals, order_fake, carrier)
|
||||
if not wh_option:
|
||||
order_fake.order_line = FakeCollection(list(filter(lambda line: line.product_id.id in wh_vals['product_ids'], original_order_fake_order_line)))
|
||||
wh_carrier_options = self._generate_shipping_carrier_option(wh_vals, order_fake, carrier)
|
||||
if not wh_carrier_options:
|
||||
has_error = True
|
||||
else:
|
||||
new_base_option['sub_options'][wh_id] = wh_option
|
||||
for _option in wh_carrier_options:
|
||||
if _option.get('carrier_id'):
|
||||
found_carrier_ids.add(_option['carrier_id'])
|
||||
new_base_option['sub_options'][wh_id] = wh_carrier_options
|
||||
|
||||
if has_error:
|
||||
continue
|
||||
# now that we've collected, we can roll up some details.
|
||||
|
||||
new_base_option['carrier_id'] = carrier.id
|
||||
new_base_option['shipping_price'] = self._get_shipping_price_for_options(new_base_option['sub_options'])
|
||||
new_base_option['requested_date'] = self._get_max_requested_date(new_base_option['sub_options'])
|
||||
new_base_option['transit_days'] = self._get_max_transit_days(new_base_option['sub_options'])
|
||||
options.append(new_base_option)
|
||||
# now that we've collected details for this carrier, we likely have more than one carrier's rates
|
||||
_logger.info(' from ' + str(carrier) + ' found ' + str(found_carrier_ids))
|
||||
for carrier_id in found_carrier_ids:
|
||||
carrier_option = deepcopy(base_option)
|
||||
carrier_option['carrier_id'] = False
|
||||
for wh_id, wh_vals in base_option['sub_options'].items():
|
||||
for co in new_base_option['sub_options'].get(wh_id, []):
|
||||
if co.get('carrier_id') == carrier_id:
|
||||
# we have found the rate!
|
||||
carrier_option['carrier_id'] = carrier_id
|
||||
carrier_option['sub_options'][wh_id] = co
|
||||
if carrier_option['carrier_id']:
|
||||
carrier_option['shipping_price'] = self._get_shipping_price_for_options(carrier_option['sub_options'])
|
||||
carrier_option['requested_date'] = self._get_max_requested_date(carrier_option['sub_options'])
|
||||
carrier_option['transit_days'] = self._get_max_transit_days(carrier_option['sub_options'])
|
||||
options.append(carrier_option)
|
||||
|
||||
#restore values in case more processing occurs
|
||||
order_fake.warehouse_id = original_order_fake_warehouse_id
|
||||
@@ -734,6 +769,8 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
def _generate_shipping_carrier_option(self, base_option, order_fake, carrier):
|
||||
# some carriers look at the order carrier_id
|
||||
order_fake.carrier_id = carrier
|
||||
date_planned = base_option.get('date_planned')
|
||||
order_fake.date_planned = date_planned
|
||||
|
||||
# this logic comes from "delivery.models.sale_order.SaleOrder"
|
||||
try:
|
||||
@@ -741,7 +778,9 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
date_delivered = None
|
||||
transit_days = 0
|
||||
if carrier.delivery_type not in ['fixed', 'base_on_rule']:
|
||||
if hasattr(carrier, 'rate_shipment_date_planned'):
|
||||
if hasattr(carrier, 'rate_shipment_multi'):
|
||||
result = carrier.rate_shipment_multi(order=order_fake)
|
||||
elif hasattr(carrier, 'rate_shipment_date_planned'):
|
||||
# New API
|
||||
result = carrier.rate_shipment_date_planned(order_fake, base_option.get('date_planned'))
|
||||
if result:
|
||||
@@ -751,7 +790,8 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
elif hasattr(carrier, 'get_shipping_price_for_plan'):
|
||||
# Old API
|
||||
result = carrier.get_shipping_price_for_plan(order_fake, base_option.get('date_planned'))
|
||||
if result and isinstance(result, list):
|
||||
if result and isinstance(result, list) and not isinstance(result[0], dict):
|
||||
# this detects the above only if it isn't a list of dictionaries (aka multi-rating result)
|
||||
price_unit, transit_days, date_delivered = result[0]
|
||||
elif not result:
|
||||
rate = carrier.rate_shipment(order_fake)
|
||||
@@ -762,7 +802,7 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
if rate.get('date_delivered'):
|
||||
date_delivered = rate.get('date_delivered')
|
||||
else:
|
||||
_logger.warning('returning None because carrier: ' + str(carrier))
|
||||
_logger.warning('returning None because carrier: ' + str(carrier) + ' returned rate: ' + str(rate))
|
||||
return None
|
||||
else:
|
||||
carrier = carrier.available_carriers(order_fake.partner_shipping_id)
|
||||
@@ -773,13 +813,38 @@ class SaleOrderMakePlan(models.TransientModel):
|
||||
if order_fake.company_id.currency_id.id != order_fake.pricelist_id.currency_id.id:
|
||||
price_unit = order_fake.company_id.currency_id.with_context(date=order_fake.date_order).compute(price_unit, order_fake.pricelist_id.currency_id)
|
||||
|
||||
final_price = float(price_unit) * (1.0 + (float(carrier.margin) / 100.0))
|
||||
option = deepcopy(base_option)
|
||||
option['carrier_id'] = carrier.id
|
||||
option['shipping_price'] = final_price
|
||||
option['requested_date'] = date_delivered
|
||||
option['transit_days'] = transit_days
|
||||
return option
|
||||
if result and isinstance(result, list):
|
||||
res = []
|
||||
for rate in result:
|
||||
rate_carrier = rate.get('carrier')
|
||||
if not rate_carrier:
|
||||
continue
|
||||
price_unit = rate['price']
|
||||
date_delivered = rate.get('date_delivered')
|
||||
transit_days = rate.get('transit_days')
|
||||
|
||||
if date_planned and transit_days and not date_delivered:
|
||||
# compute from planned date anc current rate carrier
|
||||
date_delivered = rate_carrier.calculate_date_delivered(date_planned, transit_days)
|
||||
elif date_planned and date_delivered and not transit_days:
|
||||
transit_days = rate_carrier.calculate_transit_days(date_planned, date_delivered)
|
||||
|
||||
final_price = float(price_unit) * (1.0 + (float(rate_carrier.margin) / 100.0))
|
||||
option = deepcopy(base_option)
|
||||
option['carrier_id'] = rate_carrier.id
|
||||
option['shipping_price'] = final_price
|
||||
option['requested_date'] = fields.Datetime.to_string(date_delivered) if (date_delivered and isinstance(date_delivered, datetime)) else date_delivered
|
||||
option['transit_days'] = transit_days
|
||||
res.append(option)
|
||||
return res
|
||||
else:
|
||||
final_price = float(price_unit) * (1.0 + (float(carrier.margin) / 100.0))
|
||||
option = deepcopy(base_option)
|
||||
option['carrier_id'] = carrier.id
|
||||
option['shipping_price'] = final_price
|
||||
option['requested_date'] = fields.Datetime.to_string(date_delivered) if (date_delivered and isinstance(date_delivered, datetime)) else date_delivered
|
||||
option['transit_days'] = transit_days
|
||||
return [option]
|
||||
except Exception as e:
|
||||
_logger.info("Exception collecting carrier rates: " + str(e))
|
||||
# Want to see more?
|
||||
@@ -796,12 +861,14 @@ class SaleOrderPlanningOption(models.TransientModel):
|
||||
def datetime_converter(o):
|
||||
if isinstance(o, datetime):
|
||||
return str(o)
|
||||
|
||||
if 'sub_options' in values and not isinstance(values['sub_options'], str):
|
||||
for wh_id, option in values['sub_options'].items():
|
||||
if option.get('date_planned'):
|
||||
option['date_planned'] = str(option['date_planned'])
|
||||
values['sub_options'] = dumps(values['sub_options'], default=datetime_converter)
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for option_values in values:
|
||||
if 'sub_options' in option_values and not isinstance(option_values['sub_options'], str):
|
||||
for wh_id, option in option_values['sub_options'].items():
|
||||
if option.get('date_planned'):
|
||||
option['date_planned'] = str(option['date_planned'])
|
||||
option_values['sub_options'] = dumps(option_values['sub_options'], default=datetime_converter)
|
||||
return super(SaleOrderPlanningOption, self).create(values)
|
||||
|
||||
def _compute_sub_options_text(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
'name': 'Stock Delivery Planner',
|
||||
'summary': 'Get rates and choose carrier for delivery.',
|
||||
'version': '15.0.1.0.0',
|
||||
'version': '15.0.1.1.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Warehouse',
|
||||
'license': 'OPL-1',
|
||||
|
||||
@@ -47,3 +47,12 @@ class StockPicking(models.Model):
|
||||
domain.extend(tools.safe_eval(irconfig_parameter.get_param('sale.order.planner.carrier_domain')))
|
||||
|
||||
return Carrier.search(domain)
|
||||
|
||||
|
||||
class Warehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
delivery_planner_carrier_ids = fields.Many2many('delivery.carrier',
|
||||
relation='delivery_planner_carrier_wh_rel',
|
||||
string='Delivery Planner Base Carriers',
|
||||
help='Overrides the global carriers.')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?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>
|
||||
@@ -10,4 +11,16 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_warehouse_delivery_carriers" model="ir.ui.view">
|
||||
<field name="name">stock.warehouse.delivery.carriers</field>
|
||||
<field name="model">stock.warehouse</field>
|
||||
<field name="inherit_id" ref="stock.view_warehouse" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="delivery_planner_carrier_ids" options="{'no_create_edit': True, 'no_create': True}" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,32 +26,37 @@ class StockDeliveryPlanner(models.TransientModel):
|
||||
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.company.id, ))
|
||||
if carrier_ids:
|
||||
try:
|
||||
carrier_ids = [int(c) for c in carrier_ids.split(',')]
|
||||
base_carriers = base_carriers.browse(carrier_ids)
|
||||
except:
|
||||
pass
|
||||
base_carriers = planner.picking_id.picking_type_id.warehouse_id.delivery_planner_carrier_ids
|
||||
if not base_carriers:
|
||||
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
|
||||
base_carriers = base_carriers.sudo()
|
||||
|
||||
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'],
|
||||
})
|
||||
try:
|
||||
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.get('date_delivered', False),
|
||||
'transit_days': rate.get('transit_days', 0),
|
||||
})
|
||||
except (UserError, ValidationError) as e:
|
||||
_logger.warning('Exception during delivery planning. %s' % str(e))
|
||||
return planner
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<field name="plan_option_ids" nolabel="1">
|
||||
<tree decoration-info="selection == 'selected'"
|
||||
decoration-muted="selection == 'deselected'"
|
||||
decoration-bf="days_different == 0.0"
|
||||
default_order="package_id, price"
|
||||
create="false" edit="false" delete="false">
|
||||
<field name="package_id" />
|
||||
|
||||
Reference in New Issue
Block a user