mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'imp/14.0/delivery_hibou__per_package_carrier' into '14.0'
imp/14.0/delivery_hibou__per_package_carrier into 14.0 See merge request hibou-io/hibou-odoo/suite!1047
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
'name': 'Hibou Fedex Shipping',
|
'name': 'Hibou Fedex Shipping',
|
||||||
'version': '14.0.1.0.0',
|
'version': '14.0.1.1.0',
|
||||||
'category': 'Stock',
|
'category': 'Stock',
|
||||||
'author': "Hibou Corp.",
|
'author': "Hibou Corp.",
|
||||||
'license': 'AGPL-3',
|
'license': 'OPL-1',
|
||||||
'website': 'https://hibou.io/',
|
'website': 'https://hibou.io/',
|
||||||
'depends': [
|
'depends': [
|
||||||
'delivery_fedex',
|
'delivery_fedex',
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import delivery_fedex
|
from . import delivery_fedex
|
||||||
from . import stock
|
from . import stock
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import pytz
|
import pytz
|
||||||
from odoo import fields, models, tools, _
|
from odoo import fields, models, tools, _
|
||||||
@@ -223,6 +225,15 @@ class DeliveryFedex(models.Model):
|
|||||||
srm = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
|
srm = FedexRequest(self.log_xml, request_type="shipping", prod_environment=self.prod_environment)
|
||||||
superself = self.sudo()
|
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_company = superself.get_shipper_company(picking=picking)
|
||||||
shipper_warehouse = superself.get_shipper_warehouse(picking=picking)
|
shipper_warehouse = superself.get_shipper_warehouse(picking=picking)
|
||||||
recipient = superself.get_recipient(picking=picking)
|
recipient = superself.get_recipient(picking=picking)
|
||||||
@@ -239,7 +250,7 @@ class DeliveryFedex(models.Model):
|
|||||||
|
|
||||||
srm.transaction_detail(picking.id)
|
srm.transaction_detail(picking.id)
|
||||||
|
|
||||||
package_type = picking.package_ids and picking.package_ids[0].packaging_id.shipper_package_code or self.fedex_default_packaging_id.shipper_package_code
|
package_type = picking_packages and picking_packages[0].packaging_id.shipper_package_code or self.fedex_default_packaging_id.shipper_package_code
|
||||||
srm.shipment_request(self.fedex_droppoff_type, self.fedex_service_type, package_type, self.fedex_weight_unit, self.fedex_saturday_delivery)
|
srm.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_currency(_convert_curr_iso_fdx(picking.company_id.currency_id.name))
|
||||||
srm.set_shipper(shipper_company, shipper_warehouse)
|
srm.set_shipper(shipper_company, shipper_warehouse)
|
||||||
@@ -278,7 +289,7 @@ class DeliveryFedex(models.Model):
|
|||||||
send_etd = superself.env['ir.config_parameter'].get_param("delivery_fedex.send_etd")
|
send_etd = superself.env['ir.config_parameter'].get_param("delivery_fedex.send_etd")
|
||||||
srm.commercial_invoice(self.fedex_document_stock_type, send_etd)
|
srm.commercial_invoice(self.fedex_document_stock_type, send_etd)
|
||||||
|
|
||||||
package_count = len(picking.package_ids) or 1
|
package_count = len(picking_packages) or 1
|
||||||
|
|
||||||
# For india picking courier is not accepted without this details in label.
|
# For india picking courier is not accepted without this details in label.
|
||||||
po_number = order.display_name or False
|
po_number = order.display_name or False
|
||||||
@@ -307,16 +318,17 @@ class DeliveryFedex(models.Model):
|
|||||||
package_labels = []
|
package_labels = []
|
||||||
carrier_tracking_ref = ""
|
carrier_tracking_ref = ""
|
||||||
|
|
||||||
for sequence, package in enumerate(picking.package_ids, start=1):
|
for sequence, package in enumerate(picking_packages, start=1):
|
||||||
|
|
||||||
package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit)
|
package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit)
|
||||||
packaging = package.packaging_id
|
packaging = package.packaging_id
|
||||||
|
packaging_code = packaging.shipper_package_code if (packaging.package_carrier_type == 'fedex' and packaging.shipper_package_code) else self.fedex_default_packaging_id.shipper_package_code
|
||||||
|
|
||||||
# Hibou Delivery
|
# Hibou Delivery
|
||||||
# Add more details to package.
|
# Add more details to package.
|
||||||
srm._add_package(
|
srm._add_package(
|
||||||
package_weight,
|
package_weight,
|
||||||
package_code=packaging.shipper_package_code,
|
package_code=packaging_code,
|
||||||
package_height=packaging.height,
|
package_height=packaging.height,
|
||||||
package_width=packaging.width,
|
package_width=packaging.width,
|
||||||
package_length=packaging.packaging_length,
|
package_length=packaging.packaging_length,
|
||||||
@@ -394,10 +406,11 @@ class DeliveryFedex(models.Model):
|
|||||||
# One package #
|
# One package #
|
||||||
###############
|
###############
|
||||||
elif package_count == 1:
|
elif package_count == 1:
|
||||||
packaging = picking.package_ids[:1].packaging_id or picking.carrier_id.fedex_default_packaging_id
|
packaging = picking_packages[:1].packaging_id or self.fedex_default_packaging_id
|
||||||
|
packaging_code = packaging.shipper_package_code if packaging.package_carrier_type == 'fedex' else self.fedex_default_packaging_id.shipper_package_code
|
||||||
srm._add_package(
|
srm._add_package(
|
||||||
net_weight,
|
net_weight,
|
||||||
package_code=packaging.shipper_package_code,
|
package_code=packaging_code,
|
||||||
package_height=packaging.height,
|
package_height=packaging.height,
|
||||||
package_width=packaging.width,
|
package_width=packaging.width,
|
||||||
package_length=packaging.packaging_length,
|
package_length=packaging.packaging_length,
|
||||||
@@ -457,21 +470,33 @@ class DeliveryFedex(models.Model):
|
|||||||
picking.message_post(body='Fedex Documents', attachments=fedex_documents)
|
picking.message_post(body='Fedex Documents', attachments=fedex_documents)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def fedex_rate_shipment_multi(self, order=None, picking=None):
|
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:
|
if order:
|
||||||
max_weight = self._fedex_convert_weight(self.fedex_default_packaging_id.max_weight, self.fedex_weight_unit)
|
max_weight = self._fedex_convert_weight(self.fedex_default_packaging_id.max_weight, self.fedex_weight_unit)
|
||||||
is_india = order.partner_shipping_id.country_id.code == 'IN' and order.company_id.partner_id.country_id.code == 'IN'
|
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
|
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)
|
weight_value = self._fedex_convert_weight(est_weight_value, self.fedex_weight_unit)
|
||||||
order_currency = order.currency_id
|
order_currency = order.currency_id
|
||||||
else:
|
elif not package:
|
||||||
# max_weight = self._fedex_convert_weight(self.fedex_default_packaging_id.max_weight, self.fedex_weight_unit)
|
|
||||||
is_india = picking.partner_id.country_id.code == 'IN' and picking.company_id.partner_id.country_id.code == 'IN'
|
is_india = picking.partner_id.country_id.code == 'IN' and picking.company_id.partner_id.country_id.code == 'IN'
|
||||||
# TODO must be per-package eventually
|
|
||||||
# theoretically just sum of all packages weights, but the rating itself will also need to change...
|
|
||||||
est_weight_value = sum([(line.product_id.weight * (line.qty_done or line.product_uom_qty)) for line in picking.move_line_ids]) or 0.0
|
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)
|
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
|
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
|
price = 0.0
|
||||||
|
|
||||||
@@ -558,24 +583,24 @@ class DeliveryFedex(models.Model):
|
|||||||
)
|
)
|
||||||
srm.set_master_package(weight_value, 1)
|
srm.set_master_package(weight_value, 1)
|
||||||
else:
|
else:
|
||||||
if picking.package_ids:
|
if package:
|
||||||
for sequence, package in enumerate(picking.package_ids, start=1):
|
package_weight = self._fedex_convert_weight(package.shipping_weight or package.weight, self.fedex_weight_unit)
|
||||||
package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit)
|
packaging = package.packaging_id
|
||||||
packaging = package.packaging_id
|
package_code = package.packaging_id.shipper_package_code if packaging.package_carrier_type == 'fedex' else self.fedex_default_packaging_id.shipper_package_code
|
||||||
|
|
||||||
srm.add_package(
|
srm.add_package(
|
||||||
package_weight,
|
package_weight,
|
||||||
mode='rating',
|
mode='rating',
|
||||||
package_code=packaging.shipper_package_code,
|
package_code=package_code,
|
||||||
package_height=packaging.height,
|
package_height=packaging.height,
|
||||||
package_width=packaging.width,
|
package_width=packaging.width,
|
||||||
package_length=packaging.packaging_length,
|
package_length=packaging.packaging_length,
|
||||||
sequence_number=sequence,
|
sequence_number=1,
|
||||||
# po_number=po_number,
|
# po_number=po_number,
|
||||||
# dept_number=dept_number,
|
# dept_number=dept_number,
|
||||||
reference=('%s-%d' % (order_name, sequence)),
|
reference=('%s-%d' % (order_name, 1)),
|
||||||
insurance=insurance_value
|
insurance=insurance_value
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# deliver all together...
|
# deliver all together...
|
||||||
package_weight = self._fedex_convert_weight(picking.shipping_weight or picking.weight, self.fedex_weight_unit)
|
package_weight = self._fedex_convert_weight(picking.shipping_weight or picking.weight, self.fedex_weight_unit)
|
||||||
@@ -662,6 +687,7 @@ class DeliveryFedex(models.Model):
|
|||||||
tz = pytz.timezone(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)
|
date_delivered = tz.localize(date_delivered).astimezone(pytz.utc).replace(tzinfo=None)
|
||||||
result.append({'carrier': carrier,
|
result.append({'carrier': carrier,
|
||||||
|
'package': package or self.env['stock.quant.package'].browse(),
|
||||||
'success': True,
|
'success': True,
|
||||||
'price': price,
|
'price': price,
|
||||||
'error_message': False,
|
'error_message': False,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from zeep.exceptions import Fault
|
from zeep.exceptions import Fault
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
'name': 'Golden State Overnight (gso.com) Shipping',
|
'name': 'Golden State Overnight (gso.com) Shipping',
|
||||||
'summary': 'Send your shippings through gso.com and track them online.',
|
'summary': 'Send your shippings through gso.com and track them online.',
|
||||||
'version': '14.0.1.0.0',
|
'version': '14.0.1.1.0',
|
||||||
'author': "Hibou Corp.",
|
'author': "Hibou Corp.",
|
||||||
'category': 'Warehouse',
|
'category': 'Warehouse',
|
||||||
'license': 'AGPL-3',
|
'license': 'OPL-1',
|
||||||
'images': [],
|
'images': [],
|
||||||
'website': "https://hibou.io",
|
'website': "https://hibou.io",
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import delivery_gso
|
from . import delivery_gso
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
@@ -198,9 +200,15 @@ class ProviderGSO(models.Model):
|
|||||||
'thermal': [],
|
'thermal': [],
|
||||||
'paper': [],
|
'paper': [],
|
||||||
}
|
}
|
||||||
if picking.package_ids:
|
picking_packages = picking.package_ids
|
||||||
|
package_carriers = picking_packages.mapped('carrier_id')
|
||||||
|
if package_carriers:
|
||||||
|
# only ship ours
|
||||||
|
picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
|
||||||
|
|
||||||
|
if picking_packages:
|
||||||
# Every package will be a transaction
|
# Every package will be a transaction
|
||||||
for package in picking.package_ids:
|
for package in picking_packages:
|
||||||
request_body['Shipment']['Weight'] = self._gso_convert_weight(package.shipping_weight)
|
request_body['Shipment']['Weight'] = self._gso_convert_weight(package.shipping_weight)
|
||||||
request_body['Shipment'].update(self._gso_get_package_dimensions(package))
|
request_body['Shipment'].update(self._gso_get_package_dimensions(package))
|
||||||
request_body['Shipment']['ShipmentReference'] = package.name
|
request_body['Shipment']['ShipmentReference'] = package.name
|
||||||
@@ -217,7 +225,8 @@ class ProviderGSO(models.Model):
|
|||||||
cost += response['ShipmentCharges']['TotalCharge']
|
cost += response['ShipmentCharges']['TotalCharge']
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
raise ValidationError(e)
|
raise ValidationError(e)
|
||||||
else:
|
elif not package_carriers:
|
||||||
|
# ship the whole picking
|
||||||
request_body['Shipment']['Weight'] = self._gso_convert_weight(picking.shipping_weight)
|
request_body['Shipment']['Weight'] = self._gso_convert_weight(picking.shipping_weight)
|
||||||
request_body['Shipment'].update(self._gso_get_package_dimensions())
|
request_body['Shipment'].update(self._gso_get_package_dimensions())
|
||||||
request_body['Shipment']['ShipmentReference'] = picking.name
|
request_body['Shipment']['ShipmentReference'] = picking.name
|
||||||
@@ -234,6 +243,8 @@ class ProviderGSO(models.Model):
|
|||||||
cost += response['ShipmentCharges']['TotalCharge']
|
cost += response['ShipmentCharges']['TotalCharge']
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
raise ValidationError(e)
|
raise ValidationError(e)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
# Handle results
|
# Handle results
|
||||||
trackings = [l[0] for l in labels['thermal']] + [l[0] for l in labels['paper']]
|
trackings = [l[0] for l in labels['thermal']] + [l[0] for l in labels['paper']]
|
||||||
@@ -331,18 +342,32 @@ class ProviderGSO(models.Model):
|
|||||||
res.append('https://www.gso.com/Tracking')
|
res.append('https://www.gso.com/Tracking')
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def gso_rate_shipment_multi(self, order=None, picking=None):
|
def gso_rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||||
|
if not packages:
|
||||||
|
return self._gso_rate_shipment_multi_package(order=order, picking=picking)
|
||||||
|
else:
|
||||||
|
rates = []
|
||||||
|
for package in packages:
|
||||||
|
rates += self._gso_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||||
|
return rates
|
||||||
|
|
||||||
|
def _gso_rate_shipment_multi_package(self, order=None, picking=None, package=None):
|
||||||
sudoself = self.sudo()
|
sudoself = self.sudo()
|
||||||
service = sudoself._get_gso_service()
|
try:
|
||||||
|
service = sudoself._get_gso_service()
|
||||||
|
except HTTPError as e:
|
||||||
|
_logger.error(e)
|
||||||
|
return [{
|
||||||
|
'success': False,
|
||||||
|
'price': 0.0,
|
||||||
|
'error_message': _('GSO web service returned an error. ' + str(e)),
|
||||||
|
'warning_message': False,
|
||||||
|
}]
|
||||||
|
|
||||||
from_ = sudoself.get_shipper_warehouse(order=order, picking=picking)
|
from_ = sudoself.get_shipper_warehouse(order=order, picking=picking)
|
||||||
to = sudoself.get_recipient(order=order, picking=picking)
|
to = sudoself.get_recipient(order=order, picking=picking)
|
||||||
address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R'
|
address_type = 'B' if bool(to.is_company or to.parent_id.is_company) else 'R'
|
||||||
|
package_dimensions = self._gso_get_package_dimensions(package=package)
|
||||||
if order:
|
|
||||||
est_weight_value = self._gso_convert_weight(
|
|
||||||
sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0)
|
|
||||||
else:
|
|
||||||
est_weight_value = self._gso_convert_weight(picking.shipping_weight)
|
|
||||||
|
|
||||||
date_planned = fields.Datetime.now()
|
date_planned = fields.Datetime.now()
|
||||||
if self.env.context.get('date_planned'):
|
if self.env.context.get('date_planned'):
|
||||||
@@ -353,10 +378,13 @@ class ProviderGSO(models.Model):
|
|||||||
ship_date_gso = ship_date_utc.astimezone(pytz.timezone(GSO_TZ))
|
ship_date_gso = ship_date_utc.astimezone(pytz.timezone(GSO_TZ))
|
||||||
ship_date_gso = fields.Datetime.to_string(ship_date_gso)
|
ship_date_gso = fields.Datetime.to_string(ship_date_gso)
|
||||||
|
|
||||||
if picking and picking.package_ids:
|
if order:
|
||||||
package_dimensions = self._gso_get_package_dimensions(package=picking.package_ids[0])
|
est_weight_value = self._gso_convert_weight(
|
||||||
|
sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0)
|
||||||
|
elif not package:
|
||||||
|
est_weight_value = self._gso_convert_weight(picking.shipping_weight)
|
||||||
else:
|
else:
|
||||||
package_dimensions = self._gso_get_package_dimensions()
|
est_weight_value = package.shipping_weight or package.weight
|
||||||
|
|
||||||
request_body = {
|
request_body = {
|
||||||
'AccountNumber': sudoself.gso_account_number,
|
'AccountNumber': sudoself.gso_account_number,
|
||||||
@@ -395,6 +423,7 @@ class ProviderGSO(models.Model):
|
|||||||
if carrier:
|
if carrier:
|
||||||
rates.append({
|
rates.append({
|
||||||
'carrier': carrier,
|
'carrier': carrier,
|
||||||
|
'package': package or self.env['stock.quant.package'].browse(),
|
||||||
'success': True,
|
'success': True,
|
||||||
'price': price,
|
'price': price,
|
||||||
'error_message': False,
|
'error_message': False,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
'name': 'Delivery Hibou',
|
'name': 'Delivery Hibou',
|
||||||
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
|
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
|
||||||
'version': '14.0.1.0.0',
|
'version': '14.0.1.1.0',
|
||||||
'author': "Hibou Corp.",
|
'author': "Hibou Corp.",
|
||||||
'category': 'Stock',
|
'category': 'Stock',
|
||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from odoo import fields, models
|
from odoo import fields, models
|
||||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
class DeliveryCarrier(models.Model):
|
class DeliveryCarrier(models.Model):
|
||||||
@@ -161,11 +162,12 @@ class DeliveryCarrier(models.Model):
|
|||||||
# -------------------------- #
|
# -------------------------- #
|
||||||
# API for external providers #
|
# API for external providers #
|
||||||
# -------------------------- #
|
# -------------------------- #
|
||||||
def rate_shipment_multi(self, order=None, picking=None):
|
def rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||||
''' Compute the price of the order shipment
|
''' Compute the price of the order shipment
|
||||||
|
|
||||||
:param order: record of sale.order or None
|
:param order: record of sale.order or None
|
||||||
:param picking: record of stock.picking or None
|
:param picking: record of stock.picking or None
|
||||||
|
:param packages: recordset of stock.quant.package or None (requires picking also set)
|
||||||
:return list: dict: {
|
:return list: dict: {
|
||||||
'carrier': delivery.carrier(),
|
'carrier': delivery.carrier(),
|
||||||
'success': boolean,
|
'success': boolean,
|
||||||
@@ -176,6 +178,7 @@ class DeliveryCarrier(models.Model):
|
|||||||
'date_delivered': a datetime for when the shipment is supposed to arrive,
|
'date_delivered': a datetime for when the shipment is supposed to arrive,
|
||||||
'transit_days': a Float for how many days it takes in transit,
|
'transit_days': a Float for how many days it takes in transit,
|
||||||
'service_code': a string that represents the service level/agreement,
|
'service_code': a string that represents the service level/agreement,
|
||||||
|
'package': stock.quant.package(),
|
||||||
}
|
}
|
||||||
|
|
||||||
e.g. self == delivery.carrier(5, 6)
|
e.g. self == delivery.carrier(5, 6)
|
||||||
@@ -190,12 +193,53 @@ class DeliveryCarrier(models.Model):
|
|||||||
|
|
||||||
if picking:
|
if picking:
|
||||||
self = self.with_context(date_planned=fields.Datetime.now())
|
self = self.with_context(date_planned=fields.Datetime.now())
|
||||||
|
if not packages:
|
||||||
|
packages = picking.package_ids
|
||||||
else:
|
else:
|
||||||
|
if packages:
|
||||||
|
raise UserError('Cannot rate package without picking.')
|
||||||
self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now()))
|
self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now()))
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
for carrier in self:
|
for carrier in self:
|
||||||
|
carrier_packages = packages.filtered(lambda p: not p.carrier_tracking_ref and
|
||||||
|
(not p.carrier_id or p.carrier_id == carrier) and
|
||||||
|
p.packaging_id.package_carrier_type in (False, '', 'none', carrier.delivery_type))
|
||||||
|
if packages and not carrier_packages:
|
||||||
|
continue
|
||||||
if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type):
|
if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type):
|
||||||
carrier_rates = getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, picking=picking)
|
try:
|
||||||
res += carrier_rates
|
res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order,
|
||||||
|
picking=picking,
|
||||||
|
packages=carrier_packages)
|
||||||
|
except TypeError:
|
||||||
|
# TODO remove catch if after Odoo 14
|
||||||
|
# This is intended to find ones that don't support packages= kwarg
|
||||||
|
res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order,
|
||||||
|
picking=picking)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def cancel_shipment(self, pickings, packages=None):
|
||||||
|
''' Cancel a shipment
|
||||||
|
|
||||||
|
:param pickings: A recordset of pickings
|
||||||
|
:param packages: Optional recordset of packages (should be for this carrier)
|
||||||
|
'''
|
||||||
|
self.ensure_one()
|
||||||
|
if hasattr(self, '%s_cancel_shipment' % self.delivery_type):
|
||||||
|
# No good way to tell if this method takes the kwarg for packages
|
||||||
|
if packages:
|
||||||
|
try:
|
||||||
|
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings, packages=packages)
|
||||||
|
except TypeError:
|
||||||
|
# we won't be able to cancel the packages properly
|
||||||
|
# here we will TRY to make a good call here where we put the package references into the picking
|
||||||
|
# and let the original mechanisms try to work here
|
||||||
|
tracking_ref = ','.join(packages.mapped('carrier_tracking_ref'))
|
||||||
|
pickings.write({
|
||||||
|
'carrier_id': self.id,
|
||||||
|
'carrier_tracking_ref': tracking_ref,
|
||||||
|
})
|
||||||
|
|
||||||
|
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings)
|
||||||
|
|||||||
@@ -1,4 +1,27 @@
|
|||||||
from odoo import api, fields, models
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class StockQuantPackage(models.Model):
|
||||||
|
_inherit = 'stock.quant.package'
|
||||||
|
|
||||||
|
carrier_id = fields.Many2one('delivery.carrier', string='Carrier')
|
||||||
|
carrier_tracking_ref = fields.Char(string='Tracking Reference')
|
||||||
|
|
||||||
|
def _get_active_picking(self):
|
||||||
|
picking_id = self._context.get('active_id')
|
||||||
|
picking_model = self._context.get('active_model')
|
||||||
|
if not picking_id or picking_model != 'stock.picking':
|
||||||
|
raise UserError('Cannot cancel package other than through shipment/picking.')
|
||||||
|
return self.env['stock.picking'].browse(picking_id)
|
||||||
|
|
||||||
|
def send_to_shipper(self):
|
||||||
|
picking = self._get_active_picking()
|
||||||
|
picking.send_to_shipper(packages=self)
|
||||||
|
|
||||||
|
def cancel_shipment(self):
|
||||||
|
picking = self._get_active_picking()
|
||||||
|
picking.cancel_shipment(packages=self)
|
||||||
|
|
||||||
|
|
||||||
class StockPicking(models.Model):
|
class StockPicking(models.Model):
|
||||||
@@ -11,6 +34,16 @@ class StockPicking(models.Model):
|
|||||||
('no', 'No'),
|
('no', 'No'),
|
||||||
], string='Require Insurance', default='auto',
|
], string='Require Insurance', default='auto',
|
||||||
help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.')
|
help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.')
|
||||||
|
package_carrier_tracking_ref = fields.Char(string='Package Tracking Numbers', compute='_compute_package_carrier_tracking_ref')
|
||||||
|
|
||||||
|
@api.depends('package_ids.carrier_tracking_ref')
|
||||||
|
def _compute_package_carrier_tracking_ref(self):
|
||||||
|
for picking in self:
|
||||||
|
package_refs = picking.package_ids.filtered('carrier_tracking_ref').mapped('carrier_tracking_ref')
|
||||||
|
if package_refs:
|
||||||
|
picking.package_carrier_tracking_ref = ','.join(package_refs)
|
||||||
|
else:
|
||||||
|
picking.package_carrier_tracking_ref = False
|
||||||
|
|
||||||
@api.onchange('carrier_id')
|
@api.onchange('carrier_id')
|
||||||
def _onchange_carrier_id_for_priority(self):
|
def _onchange_carrier_id_for_priority(self):
|
||||||
@@ -41,3 +74,86 @@ class StockPicking(models.Model):
|
|||||||
# Assume Full Value
|
# Assume Full Value
|
||||||
cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0])
|
cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0])
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
|
def clear_carrier_tracking_ref(self):
|
||||||
|
self.write({'carrier_tracking_ref': False})
|
||||||
|
|
||||||
|
def reset_carrier_tracking_ref(self):
|
||||||
|
for picking in self:
|
||||||
|
picking.carrier_tracking_ref = picking.package_carrier_tracking_ref
|
||||||
|
|
||||||
|
# Override to send to specific packaging carriers
|
||||||
|
def send_to_shipper(self, packages=None):
|
||||||
|
self.ensure_one()
|
||||||
|
if not packages:
|
||||||
|
packages = self.package_ids
|
||||||
|
package_carriers = packages.mapped('carrier_id')
|
||||||
|
if not package_carriers:
|
||||||
|
# Original behavior
|
||||||
|
return super().send_to_shipper()
|
||||||
|
|
||||||
|
tracking_numbers = []
|
||||||
|
carrier_prices = []
|
||||||
|
order_currency = self.sale_id.currency_id or self.company_id.currency_id
|
||||||
|
for carrier in package_carriers:
|
||||||
|
self.carrier_id = carrier
|
||||||
|
carrier_packages = packages.filtered(lambda p: p.carrier_id == carrier)
|
||||||
|
res = carrier.send_shipping(self)
|
||||||
|
if res:
|
||||||
|
res = res[0]
|
||||||
|
if carrier.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= carrier.amount:
|
||||||
|
res['exact_price'] = 0.0
|
||||||
|
carrier_price = res['exact_price'] * (1.0 + (self.carrier_id.margin / 100.0))
|
||||||
|
carrier_prices.append(carrier_price)
|
||||||
|
tracking_number = ''
|
||||||
|
if res['tracking_number']:
|
||||||
|
tracking_number = res['tracking_number']
|
||||||
|
tracking_numbers.append(tracking_number)
|
||||||
|
# Try to add tracking to the individual packages.
|
||||||
|
potential_tracking_numbers = tracking_number.split(',')
|
||||||
|
if len(potential_tracking_numbers) >= len(carrier_packages):
|
||||||
|
for t, p in zip(potential_tracking_numbers, carrier_packages):
|
||||||
|
p.carrier_tracking_ref = t
|
||||||
|
else:
|
||||||
|
carrier_packages.write({'carrier_tracking_ref': tracking_number})
|
||||||
|
msg = _(
|
||||||
|
"Shipment sent to carrier %(carrier_name)s for shipping with tracking number %(ref)s<br/>Cost: %(price).2f %(currency)s",
|
||||||
|
carrier_name=carrier.name,
|
||||||
|
ref=tracking_number,
|
||||||
|
price=carrier_price,
|
||||||
|
currency=order_currency.name
|
||||||
|
)
|
||||||
|
self.message_post(body=msg)
|
||||||
|
|
||||||
|
self.carrier_price = sum(carrier_prices or [0.0])
|
||||||
|
self.carrier_tracking_ref = ','.join(tracking_numbers or [''])
|
||||||
|
self._add_delivery_cost_to_so()
|
||||||
|
|
||||||
|
# Override to provide per-package versions...
|
||||||
|
def cancel_shipment(self, packages=None):
|
||||||
|
pickings_with_package_tracking = self.filtered(lambda p: p.package_carrier_tracking_ref)
|
||||||
|
for picking in pickings_with_package_tracking:
|
||||||
|
if packages:
|
||||||
|
current_packages = packages
|
||||||
|
else:
|
||||||
|
current_packages = picking.package_ids
|
||||||
|
# Packages without a carrier can just be cleared
|
||||||
|
packages_without_carrier = current_packages.filtered(lambda p: not p.carrier_id and p.carrier_tracking_ref)
|
||||||
|
packages_without_carrier.write({
|
||||||
|
'carrier_tracking_ref': False,
|
||||||
|
})
|
||||||
|
# Packages with carrier can use the carrier method
|
||||||
|
packages_with_carrier = current_packages.filtered(lambda p: p.carrier_id and p.carrier_tracking_ref)
|
||||||
|
carriers = packages_with_carrier.mapped('carrier_id')
|
||||||
|
for carrier in carriers:
|
||||||
|
carrier_packages = packages_with_carrier.filtered(lambda p: p.carrier_id == carrier)
|
||||||
|
carrier.cancel_shipment(self, packages=carrier_packages)
|
||||||
|
package_refs = ','.join(carrier_packages.mapped('carrier_tracking_ref'))
|
||||||
|
msg = "Shipment %s cancelled" % package_refs
|
||||||
|
picking.message_post(body=msg)
|
||||||
|
carrier_packages.write({'carrier_tracking_ref': False})
|
||||||
|
|
||||||
|
pickings_without_package_tracking = self - pickings_with_package_tracking
|
||||||
|
if pickings_without_package_tracking:
|
||||||
|
# use original on these
|
||||||
|
super(StockPicking, pickings_without_package_tracking).cancel_shipment()
|
||||||
|
|||||||
@@ -11,4 +11,16 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="choose_delivery_package_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">hibou.choose.delivery.package.form</field>
|
||||||
|
<field name="model">choose.delivery.package</field>
|
||||||
|
<field name="inherit_id" ref="delivery.choose_delivery_package_view_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='delivery_packaging_id']" position="attributes">
|
||||||
|
<attribute name="domain">[('package_carrier_type', '!=', False)]</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
@@ -1,14 +1,46 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
|
<record id="hibou_view_quant_package_form" model="ir.ui.view">
|
||||||
|
<field name="name">hibou.stock.quant.package.form</field>
|
||||||
|
<field name="model">stock.quant.package</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_quant_package_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='location_id']" position="after">
|
||||||
|
<label for="carrier_id"/>
|
||||||
|
<div name="carrier">
|
||||||
|
<field name="carrier_id" class="oe_inline"/>
|
||||||
|
<button type="object" class="fa fa-arrow-right oe_link" name="send_to_shipper" string="Ship" attrs="{'invisible':['|',('carrier_tracking_ref','!=',False),('carrier_id','=', False)]}"/>
|
||||||
|
</div>
|
||||||
|
<label for="carrier_tracking_ref"/>
|
||||||
|
<div name="tracking">
|
||||||
|
<field name="carrier_tracking_ref" class="oe_inline" />
|
||||||
|
<button type="object" class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|',('carrier_tracking_ref','=',False),('carrier_id','=', False)]}"/>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
|
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
|
||||||
<field name="name">hibou.delivery.stock.picking_withcarrier.form.view</field>
|
<field name="name">hibou.delivery.stock.picking_withcarrier.form.view</field>
|
||||||
<field name="model">stock.picking</field>
|
<field name="model">stock.picking</field>
|
||||||
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form" />
|
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form" />
|
||||||
|
<field name="priority" eval="200" />
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//field[@name='carrier_id']" position="before">
|
<xpath expr="//field[@name='carrier_id']" position="before">
|
||||||
<field name="require_insurance" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
<field name="require_insurance" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||||
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||||
|
<field name="package_carrier_tracking_ref" attrs="{'invisible': [('package_carrier_tracking_ref', '=', False)]}" />
|
||||||
|
<button name="clear_carrier_tracking_ref" type="object" string="Clear Tracking" attrs="{'invisible': [('carrier_tracking_ref', '!=', False)]}" />
|
||||||
|
<button name="reset_carrier_tracking_ref" type="object" string="Reset Tracking" attrs="{'invisible': [('package_carrier_tracking_ref', '!=', False)]}" />
|
||||||
|
<field name="package_ids" attrs="{'invisible': [('package_carrier_tracking_ref', '=', False)]}" context="{'active_id': id, 'active_model': 'stock.picking'}" nolabel="1" colspan="2">
|
||||||
|
<tree>
|
||||||
|
<field name="name" />
|
||||||
|
<field name="carrier_id" />
|
||||||
|
<field name="carrier_tracking_ref" />
|
||||||
|
<button type="object" name="cancel_shipment" string="Cancel" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
'name': 'Stamps.com (USPS) Shipping',
|
'name': 'Stamps.com (USPS) Shipping',
|
||||||
'summary': 'Send your shippings through Stamps.com and track them online.',
|
'summary': 'Send your shippings through Stamps.com and track them online.',
|
||||||
'version': '14.0.1.0.0',
|
'version': '14.0.1.1.0',
|
||||||
'author': "Hibou Corp.",
|
'author': "Hibou Corp.",
|
||||||
'category': 'Warehouse',
|
'category': 'Warehouse',
|
||||||
'license': 'OPL-1',
|
'license': 'OPL-1',
|
||||||
|
|||||||
@@ -147,11 +147,13 @@ class ProviderStamps(models.Model):
|
|||||||
ret_val.ContentType = self._stamps_content_type()
|
ret_val.ContentType = self._stamps_content_type()
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
def _get_stamps_shipping_multi(self, service, date_planned, order=False, picking=False):
|
def _get_stamps_shipping_multi(self, service, date_planned, order=False, picking=False, package=False):
|
||||||
if order:
|
if order:
|
||||||
weight = sum([(line.product_id.weight * line.product_qty) for line in order.order_line]) or 0.0
|
weight = sum([(line.product_id.weight * line.product_qty) for line in order.order_line]) or 0.0
|
||||||
else:
|
elif not package:
|
||||||
weight = picking.shipping_weight
|
weight = picking.shipping_weight
|
||||||
|
else:
|
||||||
|
weight = package.shipping_weight or package.weight
|
||||||
weight = self._stamps_convert_weight(weight)
|
weight = self._stamps_convert_weight(weight)
|
||||||
|
|
||||||
shipper = self.get_shipper_warehouse(order=order, picking=picking)
|
shipper = self.get_shipper_warehouse(order=order, picking=picking)
|
||||||
@@ -164,7 +166,7 @@ class ProviderStamps(models.Model):
|
|||||||
ret_val.ShipDate = date_planned.strftime('%Y-%m-%d') if date_planned else date.today().isoformat()
|
ret_val.ShipDate = date_planned.strftime('%Y-%m-%d') if date_planned else date.today().isoformat()
|
||||||
ret_val.From = self._stamps_address(service, shipper)
|
ret_val.From = self._stamps_address(service, shipper)
|
||||||
ret_val.To = self._stamps_address(service, recipient)
|
ret_val.To = self._stamps_address(service, recipient)
|
||||||
ret_val.PackageType = self._stamps_package_type()
|
ret_val.PackageType = self._stamps_package_type(package=package)
|
||||||
ret_val.WeightLb = weight
|
ret_val.WeightLb = weight
|
||||||
ret_val.ContentType = 'Merchandise'
|
ret_val.ContentType = 'Merchandise'
|
||||||
return ret_val
|
return ret_val
|
||||||
@@ -206,7 +208,13 @@ class ProviderStamps(models.Model):
|
|||||||
if not all((from_partner.zip, to_partner.zip)):
|
if not all((from_partner.zip, to_partner.zip)):
|
||||||
raise ValidationError('Stamps needs ZIP/PostalCode. From: ' + str(from_partner.zip) + ' To: ' + str(to_partner.zip))
|
raise ValidationError('Stamps needs ZIP/PostalCode. From: ' + str(from_partner.zip) + ' To: ' + str(to_partner.zip))
|
||||||
|
|
||||||
for package in picking.package_ids:
|
picking_packages = picking.package_ids
|
||||||
|
package_carriers = picking_packages.mapped('carrier_id')
|
||||||
|
if package_carriers:
|
||||||
|
# only ship ours
|
||||||
|
picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
|
||||||
|
|
||||||
|
for package in picking_packages:
|
||||||
weight = self._stamps_convert_weight(package.shipping_weight)
|
weight = self._stamps_convert_weight(package.shipping_weight)
|
||||||
l, w, h = self._stamps_package_dimensions(package=package)
|
l, w, h = self._stamps_package_dimensions(package=package)
|
||||||
|
|
||||||
@@ -223,7 +231,7 @@ class ProviderStamps(models.Model):
|
|||||||
ret_val.WeightLb = weight
|
ret_val.WeightLb = weight
|
||||||
ret_val.ContentType = self._stamps_content_type(package=package)
|
ret_val.ContentType = self._stamps_content_type(package=package)
|
||||||
ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
|
ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb) + self._stamps_hash_partner(to_partner), ret_val))
|
||||||
if not ret:
|
if not ret and not package_carriers:
|
||||||
weight = self._stamps_convert_weight(picking.shipping_weight)
|
weight = self._stamps_convert_weight(picking.shipping_weight)
|
||||||
l, w, h = self._stamps_package_dimensions()
|
l, w, h = self._stamps_package_dimensions()
|
||||||
|
|
||||||
@@ -312,6 +320,8 @@ class ProviderStamps(models.Model):
|
|||||||
package_labels = []
|
package_labels = []
|
||||||
|
|
||||||
shippings = self._stamps_get_shippings_for_picking(service, picking)
|
shippings = self._stamps_get_shippings_for_picking(service, picking)
|
||||||
|
if not shippings:
|
||||||
|
continue
|
||||||
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
|
company, from_partner, to_partner = self._stamps_get_addresses_for_picking(picking)
|
||||||
|
|
||||||
customs = None
|
customs = None
|
||||||
@@ -453,14 +463,23 @@ class ProviderStamps(models.Model):
|
|||||||
except WebFault as e:
|
except WebFault as e:
|
||||||
raise ValidationError(e)
|
raise ValidationError(e)
|
||||||
|
|
||||||
def stamps_rate_shipment_multi(self, order=None, picking=None):
|
def stamps_rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||||
|
if not packages:
|
||||||
|
return self._stamps_rate_shipment_multi_package(order=order, picking=picking)
|
||||||
|
else:
|
||||||
|
rates = []
|
||||||
|
for package in packages:
|
||||||
|
rates += self._stamps_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||||
|
return rates
|
||||||
|
|
||||||
|
def _stamps_rate_shipment_multi_package(self, order=None, picking=None, package=None):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
date_planned = fields.Datetime.now()
|
date_planned = fields.Datetime.now()
|
||||||
if self.env.context.get('date_planned'):
|
if self.env.context.get('date_planned'):
|
||||||
date_planned = self.env.context.get('date_planned')
|
date_planned = self.env.context.get('date_planned')
|
||||||
res = []
|
res = []
|
||||||
service = self._get_stamps_service()
|
service = self._get_stamps_service()
|
||||||
shipping = self._get_stamps_shipping_multi(service, date_planned, order=order, picking=picking)
|
shipping = self._get_stamps_shipping_multi(service, date_planned, order=order, picking=picking, package=package)
|
||||||
rates = service.get_rates(shipping)
|
rates = service.get_rates(shipping)
|
||||||
for rate in rates:
|
for rate in rates:
|
||||||
price = float(rate.Amount)
|
price = float(rate.Amount)
|
||||||
@@ -486,6 +505,7 @@ class ProviderStamps(models.Model):
|
|||||||
if carrier:
|
if carrier:
|
||||||
res.append({
|
res.append({
|
||||||
'carrier': carrier,
|
'carrier': carrier,
|
||||||
|
'package': package or self.env['stock.quant.package'].browse(),
|
||||||
'success': True,
|
'success': True,
|
||||||
'price': price,
|
'price': price,
|
||||||
'error_message': False,
|
'error_message': False,
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
'name': 'Hibou UPS Shipping',
|
'name': 'Hibou UPS Shipping',
|
||||||
'version': '14.0.1.0.0',
|
'version': '14.0.1.1.0',
|
||||||
'category': 'Stock',
|
'category': 'Stock',
|
||||||
'author': "Hibou Corp.",
|
'author': "Hibou Corp.",
|
||||||
'license': 'AGPL-3',
|
'license': 'OPL-1',
|
||||||
'website': 'https://hibou.io/',
|
'website': 'https://hibou.io/',
|
||||||
'depends': [
|
'depends': [
|
||||||
'delivery_ups',
|
'delivery_ups',
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import delivery_ups
|
from . import delivery_ups
|
||||||
from . import ups_request_patch
|
from . import ups_request_patch
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
from odoo.addons.delivery_ups.models.ups_request import UPSRequest, Package
|
from odoo.addons.delivery_ups.models.ups_request import UPSRequest, Package
|
||||||
@@ -223,7 +225,17 @@ class ProviderUPS(models.Model):
|
|||||||
self.ups_get_return_label(picking)
|
self.ups_get_return_label(picking)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def ups_rate_shipment_multi(self, order=None, picking=None):
|
def ups_rate_shipment_multi(self, order=None, picking=None, packages=None):
|
||||||
|
if not packages:
|
||||||
|
return self._ups_rate_shipment_multi_package(order=order, picking=picking)
|
||||||
|
else:
|
||||||
|
rates = []
|
||||||
|
for package in packages:
|
||||||
|
rates += self._ups_rate_shipment_multi_package(order=order, picking=picking, package=package)
|
||||||
|
return rates
|
||||||
|
|
||||||
|
def _ups_rate_shipment_multi_package(self, order=None, picking=None, package=None):
|
||||||
|
# TODO package here is ignored, it should not be (UPS is not multi-rating capable until we can get rates for a single package)
|
||||||
superself = self.sudo()
|
superself = self.sudo()
|
||||||
srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
|
srm = UPSRequest(self.log_xml, superself.ups_username, superself.ups_passwd, superself.ups_shipper_number, superself.ups_access_number, self.prod_environment)
|
||||||
ResCurrency = self.env['res.currency']
|
ResCurrency = self.env['res.currency']
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||||
|
|
||||||
from zeep.exceptions import Fault
|
from zeep.exceptions import Fault
|
||||||
from odoo.addons.delivery_ups.models.ups_request import UPSRequest
|
from odoo.addons.delivery_ups.models.ups_request import UPSRequest
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
'name': 'Stock Delivery Planner',
|
'name': 'Stock Delivery Planner',
|
||||||
'summary': 'Get rates and choose carrier for delivery.',
|
'summary': 'Get rates and choose carrier for delivery.',
|
||||||
'version': '14.0.1.0.0',
|
'version': '14.0.1.1.0',
|
||||||
'author': "Hibou Corp.",
|
'author': "Hibou Corp.",
|
||||||
'category': 'Warehouse',
|
'category': 'Warehouse',
|
||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<field name="inherit_id" ref="stock.view_picking_form"/>
|
<field name="inherit_id" ref="stock.view_picking_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//header" position="inside">
|
<xpath expr="//header" position="inside">
|
||||||
<button name="action_plan_delivery" type="object" string="Plan Shipment" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"/>
|
<button name="action_plan_delivery" type="object" string="Plan Shipment" class="oe_highlight" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from odoo import api, fields, models
|
from odoo import api, fields, models, _
|
||||||
from odoo.tools import safe_eval
|
from odoo.tools import safe_eval
|
||||||
import logging
|
import logging
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -10,6 +10,16 @@ class StockDeliveryPlanner(models.TransientModel):
|
|||||||
|
|
||||||
picking_id = fields.Many2one('stock.picking', 'Transfer')
|
picking_id = fields.Many2one('stock.picking', 'Transfer')
|
||||||
plan_option_ids = fields.One2many('stock.delivery.planner.option', 'plan_id', 'Options')
|
plan_option_ids = fields.One2many('stock.delivery.planner.option', 'plan_id', 'Options')
|
||||||
|
packages_planned = fields.Boolean(compute='_compute_packages_planned')
|
||||||
|
|
||||||
|
@api.depends('plan_option_ids.selection')
|
||||||
|
def _compute_packages_planned(self):
|
||||||
|
for wiz in self:
|
||||||
|
packages = wiz.picking_id.package_ids
|
||||||
|
if not packages:
|
||||||
|
wiz.packages_planned = False
|
||||||
|
selected_options = wiz.plan_option_ids.filtered(lambda p: p.selection == 'selected')
|
||||||
|
wiz.packages_planned = len(selected_options) == len(packages)
|
||||||
|
|
||||||
def create(self, values):
|
def create(self, values):
|
||||||
planner = super(StockDeliveryPlanner, self).create(values)
|
planner = super(StockDeliveryPlanner, self).create(values)
|
||||||
@@ -25,9 +35,12 @@ class StockDeliveryPlanner(models.TransientModel):
|
|||||||
_logger.warning(rate.get('error_message'))
|
_logger.warning(rate.get('error_message'))
|
||||||
for rate in filter(lambda r: r.get('success'), rates):
|
for rate in filter(lambda r: r.get('success'), rates):
|
||||||
rate = self.calculate_delivery_window(rate)
|
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({
|
planner.plan_option_ids |= planner.plan_option_ids.create({
|
||||||
'plan_id': self.id,
|
'plan_id': self.id,
|
||||||
'carrier_id': rate['carrier'].id,
|
'carrier_id': rate['carrier'].id,
|
||||||
|
'package_id': package.id,
|
||||||
'price': rate['price'],
|
'price': rate['price'],
|
||||||
'date_planned': rate['date_planned'],
|
'date_planned': rate['date_planned'],
|
||||||
'requested_date': rate['date_delivered'],
|
'requested_date': rate['date_delivered'],
|
||||||
@@ -48,6 +61,12 @@ class StockDeliveryPlanner(models.TransientModel):
|
|||||||
rate['date_delivered'] = carrier.calculate_date_delivered(date_planned, rate.get('transit_days'))
|
rate['date_delivered'] = carrier.calculate_date_delivered(date_planned, rate.get('transit_days'))
|
||||||
return rate
|
return rate
|
||||||
|
|
||||||
|
def action_plan(self):
|
||||||
|
self.ensure_one()
|
||||||
|
selected_package_options = self.plan_option_ids.filtered(lambda o: o.package_id and o.selection == 'selected')
|
||||||
|
selected_package_options._plan()
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
|
||||||
|
|
||||||
class StockDeliveryOption(models.TransientModel):
|
class StockDeliveryOption(models.TransientModel):
|
||||||
_name = 'stock.delivery.planner.option'
|
_name = 'stock.delivery.planner.option'
|
||||||
@@ -55,17 +74,45 @@ class StockDeliveryOption(models.TransientModel):
|
|||||||
|
|
||||||
plan_id = fields.Many2one('stock.delivery.planner', 'Plan', ondelete='cascade')
|
plan_id = fields.Many2one('stock.delivery.planner', 'Plan', ondelete='cascade')
|
||||||
carrier_id = fields.Many2one('delivery.carrier', 'Delivery Method')
|
carrier_id = fields.Many2one('delivery.carrier', 'Delivery Method')
|
||||||
|
package_id = fields.Many2one('stock.quant.package', 'Package')
|
||||||
price = fields.Float('Shipping Price')
|
price = fields.Float('Shipping Price')
|
||||||
date_planned = fields.Datetime('Planned Date')
|
date_planned = fields.Datetime('Planned Date')
|
||||||
requested_date = fields.Datetime('Expected Delivery Date')
|
requested_date = fields.Datetime('Expected Delivery Date')
|
||||||
transit_days = fields.Integer('Transit Days')
|
transit_days = fields.Integer('Transit Days')
|
||||||
sale_requested_date = fields.Datetime('Sale Order Delivery Date', related='plan_id.picking_id.sale_id.requested_date')
|
sale_requested_date = fields.Datetime('Sale Order Delivery Date', related='plan_id.picking_id.sale_id.requested_date')
|
||||||
days_different = fields.Float('Days Different', compute='_compute_days_different') # use carrier calendar
|
days_different = fields.Float('Days Different', compute='_compute_days_different') # use carrier calendar
|
||||||
|
selection = fields.Selection([
|
||||||
|
('', 'None'),
|
||||||
|
('selected', 'Selected'),
|
||||||
|
('deselected', 'De-selected')
|
||||||
|
])
|
||||||
|
|
||||||
|
def _plan(self):
|
||||||
|
# this is intended to be used during selecting a whole plan
|
||||||
|
for option in self:
|
||||||
|
option.package_id.write({
|
||||||
|
'carrier_id': option.carrier_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
def select_plan(self):
|
def select_plan(self):
|
||||||
for option in self.filtered('carrier_id'):
|
self.ensure_one()
|
||||||
option.plan_id.picking_id.carrier_id = option.carrier_id
|
self.selection = 'selected'
|
||||||
return
|
if self.package_id:
|
||||||
|
# need to deselect other options for this package
|
||||||
|
deselected = self.plan_id.plan_option_ids.filtered(lambda o: o.package_id == self.package_id and o != self)
|
||||||
|
deselected.write({'selection': 'deselected'})
|
||||||
|
return {
|
||||||
|
'name': _('Delivery Rate Planner'),
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_model': 'stock.delivery.planner',
|
||||||
|
'res_id': self.plan_id.id,
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Select plan for whole shipment
|
||||||
|
self.plan_id.picking_id.carrier_id = self.carrier_id
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
|
||||||
@api.depends('requested_date', 'sale_requested_date', 'carrier_id')
|
@api.depends('requested_date', 'sale_requested_date', 'carrier_id')
|
||||||
def _compute_days_different(self):
|
def _compute_days_different(self):
|
||||||
|
|||||||
@@ -6,22 +6,34 @@
|
|||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<field name="plan_option_ids">
|
<field name="packages_planned" invisible="1" />
|
||||||
<tree decoration-success="days_different < 0.0" decoration-danger="days_different > 0.0"
|
<group>
|
||||||
create="false" edit="false" delete="false">
|
<field name="plan_option_ids" nolabel="1" >
|
||||||
<field name="carrier_id"/>
|
<tree decoration-success="days_different < 0.0 and not selection"
|
||||||
<field name="date_planned"/>
|
decoration-danger="days_different > 0.0 and not selection"
|
||||||
<field name="requested_date"/>
|
decoration-info="selection == 'selected'"
|
||||||
<field name="transit_days"/>
|
decoration-muted="selection == 'deselected'"
|
||||||
<field name="sale_requested_date"/>
|
create="false" edit="false" delete="false">
|
||||||
<field name="days_different"/>
|
<field name="package_id"/>
|
||||||
<field name="price"/>
|
<field name="carrier_id"/>
|
||||||
<button class="eo_highlight"
|
<field name="date_planned" invisible="1"/>
|
||||||
name="select_plan"
|
<field name="requested_date"/>
|
||||||
string="Select"
|
<field name="transit_days"/>
|
||||||
type="object" />
|
<field name="sale_requested_date"/>
|
||||||
</tree>
|
<field name="days_different"/>
|
||||||
</field>
|
<field name="price"/>
|
||||||
|
<field name="selection" invisible="1" />
|
||||||
|
<button class="eo_highlight"
|
||||||
|
name="select_plan"
|
||||||
|
string="Select"
|
||||||
|
type="object" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button type="object" name="action_plan" string="Plan" class="btn-primary" attrs="{'invisible': [('packages_planned', '=', False)]}"/>
|
||||||
|
<button string="Discard" special="cancel" class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Reference in New Issue
Block a user