diff --git a/delivery_fedex_hibou/__init__.py b/delivery_fedex_hibou/__init__.py new file mode 100644 index 00000000..09434554 --- /dev/null +++ b/delivery_fedex_hibou/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models diff --git a/delivery_fedex_hibou/__manifest__.py b/delivery_fedex_hibou/__manifest__.py new file mode 100644 index 00000000..67f04fdf --- /dev/null +++ b/delivery_fedex_hibou/__manifest__.py @@ -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, + } diff --git a/delivery_fedex_hibou/models/__init__.py b/delivery_fedex_hibou/models/__init__.py new file mode 100644 index 00000000..bad254c1 --- /dev/null +++ b/delivery_fedex_hibou/models/__init__.py @@ -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 diff --git a/delivery_fedex_hibou/models/delivery_fedex.py b/delivery_fedex_hibou/models/delivery_fedex.py new file mode 100644 index 00000000..62fd44ea --- /dev/null +++ b/delivery_fedex_hibou/models/delivery_fedex.py @@ -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
" + "Tracking Numbers: %s
" + "Packages: %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
Tracking Number : %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']) diff --git a/delivery_fedex_hibou/models/fedex_request.py b/delivery_fedex_hibou/models/fedex_request.py new file mode 100644 index 00000000..483c26a9 --- /dev/null +++ b/delivery_fedex_hibou/models/fedex_request.py @@ -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 diff --git a/delivery_fedex_hibou/models/stock.py b/delivery_fedex_hibou/models/stock.py new file mode 100644 index 00000000..58726f6f --- /dev/null +++ b/delivery_fedex_hibou/models/stock.py @@ -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') diff --git a/delivery_fedex_hibou/views/stock_views.xml b/delivery_fedex_hibou/views/stock_views.xml new file mode 100644 index 00000000..8df21336 --- /dev/null +++ b/delivery_fedex_hibou/views/stock_views.xml @@ -0,0 +1,14 @@ + + + + stock.warehouse + stock.warehouse + + + + + + + + + \ No newline at end of file diff --git a/delivery_gso/__manifest__.py b/delivery_gso/__manifest__.py index e1923d56..26968b3a 100644 --- a/delivery_gso/__manifest__.py +++ b/delivery_gso/__manifest__.py @@ -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', diff --git a/delivery_gso/models/delivery_gso.py b/delivery_gso/models/delivery_gso.py index a242ff85..cc3a6965 100644 --- a/delivery_gso/models/delivery_gso.py +++ b/delivery_gso/models/delivery_gso.py @@ -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, diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index 62d37108..96441511 100644 --- a/delivery_hibou/__manifest__.py +++ b/delivery_hibou/__manifest__.py @@ -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', diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 85f1f931..c604c02a 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -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, + }) diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index 9f732275..77ae28b1 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -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 diff --git a/delivery_hibou/views/delivery_views.xml b/delivery_hibou/views/delivery_views.xml index 681578aa..08fe2010 100644 --- a/delivery_hibou/views/delivery_views.xml +++ b/delivery_hibou/views/delivery_views.xml @@ -7,6 +7,7 @@ + @@ -20,6 +21,11 @@ [] + + + + + diff --git a/delivery_hibou/views/stock_views.xml b/delivery_hibou/views/stock_views.xml index 6bb47f34..0989f519 100644 --- a/delivery_hibou/views/stock_views.xml +++ b/delivery_hibou/views/stock_views.xml @@ -17,6 +17,9 @@