From 453c3816cf2f4d621762250c7db1e673e43df296 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 29 Oct 2020 13:33:32 -0700 Subject: [PATCH 01/13] [MOV] delivery_fedex_hibou: from hibou-suite-enterprise --- delivery_fedex_hibou/__init__.py | 1 + delivery_fedex_hibou/__manifest__.py | 19 + delivery_fedex_hibou/models/__init__.py | 2 + delivery_fedex_hibou/models/delivery_fedex.py | 456 ++++++++++++++++++ delivery_fedex_hibou/models/fedex_request.py | 200 ++++++++ delivery_fedex_hibou/models/stock.py | 8 + delivery_fedex_hibou/views/stock_views.xml | 14 + 7 files changed, 700 insertions(+) create mode 100644 delivery_fedex_hibou/__init__.py create mode 100644 delivery_fedex_hibou/__manifest__.py create mode 100644 delivery_fedex_hibou/models/__init__.py create mode 100644 delivery_fedex_hibou/models/delivery_fedex.py create mode 100644 delivery_fedex_hibou/models/fedex_request.py create mode 100644 delivery_fedex_hibou/models/stock.py create mode 100644 delivery_fedex_hibou/views/stock_views.xml diff --git a/delivery_fedex_hibou/__init__.py b/delivery_fedex_hibou/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_fedex_hibou/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_fedex_hibou/__manifest__.py b/delivery_fedex_hibou/__manifest__.py new file mode 100644 index 00000000..103cd511 --- /dev/null +++ b/delivery_fedex_hibou/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': 'Hibou Fedex Shipping', + 'version': '12.0.1.0.0', + 'category': 'Stock', + 'author': "Hibou Corp.", + 'license': 'AGPL-3', + '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..cfdf1b44 --- /dev/null +++ b/delivery_fedex_hibou/models/__init__.py @@ -0,0 +1,2 @@ +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..538f9323 --- /dev/null +++ b/delivery_fedex_hibou/models/delivery_fedex.py @@ -0,0 +1,456 @@ +import logging +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'), + ]) + + 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 + 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 + + """ + 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_packaging_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]) 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_packaging_id.shipper_package_code, + self.fedex_weight_unit, + self.fedex_saturday_delivery, + ) + pkg = self.fedex_default_packaging_id + + srm.set_currency(_convert_curr_iso_fdx(order_currency.name)) + srm.set_shipper(shipper_company, shipper_warehouse) + srm.set_recipient(recipient, residential=residential) + + if max_weight and weight_value > max_weight: + total_package = int(weight_value / max_weight) + last_package_weight = weight_value % max_weight + + for sequence in range(1, total_package + 1): + srm.add_package( + max_weight, + package_code=pkg.shipper_package_code, + package_height=pkg.height, + package_width=pkg.width, + package_length=pkg.length, + sequence_number=sequence, + mode='rating', + ) + if last_package_weight: + total_package = total_package + 1 + srm.add_package( + last_package_weight, + package_code=pkg.shipper_package_code, + package_height=pkg.height, + package_width=pkg.width, + package_length=pkg.length, + sequence_number=total_package, + mode='rating', + ) + srm.set_master_package(weight_value, total_package) + else: + srm.add_package( + weight_value, + package_code=pkg.shipper_package_code, + package_height=pkg.height, + package_width=pkg.width, + package_length=pkg.length, + mode='rating', + ) + srm.set_master_package(weight_value, 1) + + # 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']): + 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 = 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} + + return {'success': True, + 'price': price, + 'error_message': False, + 'transit_days': request.get('transit_days', False), + 'date_delivered': request.get('date_delivered', False), + '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() + + 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() + order_name = superself.get_order_name(picking=picking) + attn = superself.get_attn(picking=picking) + insurance_value = superself.get_insurance_value(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) + + # Not the actual reference. Using `shipment_name` during `add_package` calls. + srm.transaction_detail(picking.id) + + package_type = picking.package_ids and picking.package_ids[0].packaging_id.shipper_package_code or self.fedex_default_packaging_id.shipper_package_code + 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))) + + # Commonly this needs to be modified, e.g. for doc tabs. Do not want to have to patch this entire method. + 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_unit or operation.product_id.list_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' + # DO NOT FORWARD PORT AFTER 12.0 + if getattr(operation.product_id, 'hs_code', False): + commodity_harmonized_code = operation.product_id.hs_code or '' + else: + commodity_harmonized_code = '' + 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.partner_id.country_id.code, acc_number) + + package_count = len(picking.package_ids) or 1 + + # For india picking courier is not accepted without this details in label. + po_number = 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.package_ids, start=1): + + package_weight = self._fedex_convert_weight(package.shipping_weight, self.fedex_weight_unit) + packaging = package.packaging_id + + # Hibou Delivery + # Add more details to package. + srm._add_package( + package_weight, + package_code=packaging.shipper_package_code, + package_height=packaging.height, + package_width=packaging.width, + package_length=packaging.length, + sequence_number=sequence, + po_number=po_number, + dept_number=dept_number, + ref=('%s-%d' % (order_name, sequence)), + insurance=insurance_value + ) + 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.package_ids[:1].packaging_id or picking.carrier_id.fedex_default_packaging_id + # Hibou Delivery + # Add more details to package. + srm._add_package( + net_weight, + package_code=packaging.shipper_package_code, + package_height=packaging.height, + package_width=packaging.width, + package_length=packaging.length, + po_number=po_number, + dept_number=dept_number, + ref=order_name, + insurance=insurance_value + ) + 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')) + + return res diff --git a/delivery_fedex_hibou/models/fedex_request.py b/delivery_fedex_hibou/models/fedex_request.py new file mode 100644 index 00000000..a355ec83 --- /dev/null +++ b/delivery_fedex_hibou/models/fedex_request.py @@ -0,0 +1,200 @@ +import suds +from odoo.addons.delivery_fedex.models import fedex_request +import logging +_logger = logging.getLogger(__name__) + +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, + '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.client.factory.create('Contact') + if recipient_partner.is_company: + Contact.PersonName = '' + Contact.CompanyName = sanitize_name(recipient_partner.name) + else: + Contact.PersonName = sanitize_name(recipient_partner.name) + Contact.CompanyName = sanitize_name(recipient_partner.parent_id.name or '') + + if attn: + Contact.PersonName = Contact.PersonName + ' ATTN: ' + str(attn) + + Contact.PhoneNumber = recipient_partner.phone or '' + + Address = self.client.factory.create('Address') + Address.StreetLines = [recipient_partner.street or '', recipient_partner.street2 or ''] + Address.City = 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.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', ref=False, insurance=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, ref=ref, insurance=insurance) + + 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, ref=False, insurance=False): + package = self.client.factory.create('RequestedPackageLineItem') + package_weight = self.client.factory.create('Weight') + package_weight.Value = weight_value + package_weight.Units = self.RequestedShipment.TotalWeight.Units + + if ref: + customer_ref = self.client.factory.create('CustomerReference') + customer_ref.CustomerReferenceType = 'CUSTOMER_REFERENCE' + customer_ref.Value = str(ref) + package.CustomerReferences.append(customer_ref) + + if insurance: + insured = self.client.factory.create('Money') + insured.Amount = insurance + # TODO at some point someone might need currency here + insured.Currency = 'USD' + package.InsuredValue = insured + + package.PhysicalPackaging = 'BOX' + if package_code == 'YOUR_PACKAGING': + 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.client.factory.create('CustomerReference') + po_reference.CustomerReferenceType = 'P_O_NUMBER' + po_reference.Value = po_number + package.CustomerReferences.append(po_reference) + if dept_number: + dept_reference = self.client.factory.create('CustomerReference') + dept_reference.CustomerReferenceType = 'DEPARTMENT_NUMBER' + dept_reference.Value = dept_number + package.CustomerReferences.append(dept_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.PaymentType = 'SENDER' if not third_party else 'THIRD_PARTY' + Payor = self.client.factory.create('Payor') + Payor.ResponsibleParty.AccountNumber = shipping_charges_payment_account + self.RequestedShipment.ShippingChargesPayment.Payor = Payor + + # Rating stuff + + def rate(self, date_planned=None): + """ + 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 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") + for rating in self.response.RateReplyDetails[0].RatedShipmentDetails: + formatted_response['price'][rating.ShipmentRateDetail.TotalNetFedExCharge.Currency] = rating.ShipmentRateDetail.TotalNetFedExCharge.Amount + if len(self.response.RateReplyDetails[0].RatedShipmentDetails) == 1: + if 'CurrencyExchangeRate' in self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail: + formatted_response['price'][self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.FromCurrency] = self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.TotalNetFedExCharge.Amount / self.response.RateReplyDetails[0].RatedShipmentDetails[0].ShipmentRateDetail.CurrencyExchangeRate.Rate + + # Hibou Delivery Planning + if hasattr(self.response.RateReplyDetails[0], 'DeliveryTimestamp') and self.response.RateReplyDetails[0].DeliveryTimestamp: + formatted_response['date_delivered'] = self.response.RateReplyDetails[0].DeliveryTimestamp + elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'CommitTimestamp'): + formatted_response['date_delivered'] = self.response.RateReplyDetails[0].CommitDetails[0].CommitTimestamp + formatted_response['transit_days'] = self._service_transit_days.get(self.response.RateReplyDetails[0].CommitDetails[0].ServiceType, 0) + elif hasattr(self.response.RateReplyDetails[0], 'CommitDetails') and hasattr(self.response.RateReplyDetails[0].CommitDetails[0], 'TransitTime'): + transit_days = self.response.RateReplyDetails[0].CommitDetails[0].TransitTime + transit_days = self._transit_days.get(transit_days, 0) + formatted_response['transit_days'] = transit_days + + else: + 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 suds.WebFault 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] + + 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..3dfb1a1f --- /dev/null +++ b/delivery_fedex_hibou/models/stock.py @@ -0,0 +1,8 @@ +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 From 6f93c9b4a424706a0f1343fcde6c327fe6b60966 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 29 Oct 2020 13:39:10 -0700 Subject: [PATCH 02/13] [MOV] delivery_gls_nl: from hibou-suite-enterprise:12.0 --- delivery_gls_nl/__init__.py | 1 + delivery_gls_nl/__manifest__.py | 26 ++ delivery_gls_nl/models/__init__.py | 1 + delivery_gls_nl/models/delivery_gls_nl.py | 294 ++++++++++++++++++ delivery_gls_nl/models/gls_nl_request.py | 36 +++ .../views/delivery_gls_nl_view.xml | 61 ++++ 6 files changed, 419 insertions(+) create mode 100644 delivery_gls_nl/__init__.py create mode 100644 delivery_gls_nl/__manifest__.py create mode 100644 delivery_gls_nl/models/__init__.py create mode 100644 delivery_gls_nl/models/delivery_gls_nl.py create mode 100644 delivery_gls_nl/models/gls_nl_request.py create mode 100644 delivery_gls_nl/views/delivery_gls_nl_view.xml diff --git a/delivery_gls_nl/__init__.py b/delivery_gls_nl/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_gls_nl/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_gls_nl/__manifest__.py b/delivery_gls_nl/__manifest__.py new file mode 100644 index 00000000..1b27d305 --- /dev/null +++ b/delivery_gls_nl/__manifest__.py @@ -0,0 +1,26 @@ +{ + 'name': 'GLS Netherlands Shipping', + 'summary': 'Create and print your shipping labels with GLS from the Netherlands.', + 'version': '12.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +GLS Netherlands Shipping +======================== + +Create and print your shipping labels with GLS from the Netherlands. + +""", + 'depends': [ + 'delivery_hibou', + ], + 'demo': [], + 'data': [ + 'views/delivery_gls_nl_view.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/delivery_gls_nl/models/__init__.py b/delivery_gls_nl/models/__init__.py new file mode 100644 index 00000000..d16621df --- /dev/null +++ b/delivery_gls_nl/models/__init__.py @@ -0,0 +1 @@ +from . import delivery_gls_nl diff --git a/delivery_gls_nl/models/delivery_gls_nl.py b/delivery_gls_nl/models/delivery_gls_nl.py new file mode 100644 index 00000000..46fcebff --- /dev/null +++ b/delivery_gls_nl/models/delivery_gls_nl.py @@ -0,0 +1,294 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from .gls_nl_request import GLSNLRequest +from requests import HTTPError +from base64 import decodebytes +from csv import reader as csv_reader + + +class ProductPackaging(models.Model): + _inherit = 'product.packaging' + + package_carrier_type = fields.Selection(selection_add=[('gls_nl', 'GLS Netherlands')]) + + +class ProviderGLSNL(models.Model): + _inherit = 'delivery.carrier' + + GLS_NL_SOFTWARE_NAME = 'Odoo' + GLS_NL_SOFTWARE_VER = '12.0' + GLS_NL_COUNTRY_NOT_FOUND = 'GLS_NL_COUNTRY_NOT_FOUND' + + delivery_type = fields.Selection(selection_add=[('gls_nl', 'GLS Netherlands')]) + + gls_nl_username = fields.Char(string='GLS NL Username', groups='base.group_system') + gls_nl_password = fields.Char(string='GLS NL Password', groups='base.group_system') + gls_nl_labeltype = fields.Selection([ + ('zpl', 'ZPL'), + ('pdf', 'PDF'), + ], string='GLS NL Label Type') + gls_nl_express = fields.Selection([ + ('t9', 'Delivery before 09:00 on weekdays'), + ('t12', 'Delivery before 12:00 on weekdays'), + ('t17', 'Delivery before 17:00 on weekdays'), + ('s9', 'Delivery before 09:00 on Saturday'), + ('s12', 'Delivery before 12:00 on Saturday'), + ('s17', 'Delivery before 17:00 on Saturday'), + ], string='GLS NL Express', help='Express service tier (leave blank for regular)') + gls_nl_rate_id = fields.Many2one('ir.attachment', string='GLS NL Rates') + + def button_gls_nl_test_rates(self): + self.ensure_one() + if not self.gls_nl_rate_id: + raise UserError(_('No GLS NL Rate file is attached.')) + rate_data = self._gls_nl_process_rates() + weight_col_count = len(rate_data['w']) + row_count = len(rate_data['r']) + country_col = rate_data['c'] + country_model = self.env['res.country'] + for row in rate_data['r']: + country = country_model.search([('code', '=', row[country_col])], limit=1) + if not country: + raise ValidationError(_('Could not locate country by code: "%s" for row: %s') % (row[country_col], row)) + for w, i in rate_data['w'].items(): + try: + cost = float(row[i]) + except ValueError: + raise ValidationError(_('Could not process cost for row: %s') % (row, )) + raise ValidationError(_('Looks good! %s weights, %s countries located.') % (weight_col_count, row_count)) + + def _gls_nl_process_rates(self): + """ + 'w' key will be weights to row index map + 'c' key will be the country code index + 'r' key will be rows from the original that can use indexes above + :return: + """ + datab = decodebytes(self.gls_nl_rate_id.datas) + csv_data = datab.decode() + csv_data = csv_data.replace('\r', '') + csv_lines = csv_data.splitlines() + header = [csv_lines[0]] + body = csv_lines[1:] + data = {'w': {}, 'r': []} + for row in csv_reader(header): + for i, col in enumerate(row): + if col == 'Country': + data['c'] = i + else: + try: + weight = float(col) + data['w'][weight] = i + except ValueError: + pass + if 'c' not in data: + raise ValidationError(_('Could not locate "Country" column.')) + if not data['w']: + raise ValidationError(_('Could not locate any weight columns.')) + for row in csv_reader(body): + data['r'].append(row) + return data + + def _gls_nl_rate(self, country_code, weight): + if weight < 0.0: + return 0.0 + rate_data = self._gls_nl_process_rates() + country_col = rate_data['c'] + rate = None + country_found = False + for row in rate_data['r']: + if row[country_col] == country_code: + country_found = True + for w, i in rate_data['w'].items(): + if weight <= w: + try: + rate = float(row[i]) + break + except ValueError: + pass + else: + # our w, i will be the last weight and rate. + try: + # Return Max rate + remaining weight rated + return float(row[i]) + self._gls_nl_rate(country_code, weight-w) + except ValueError: + pass + break + if rate is None and not country_found: + return self.GLS_NL_COUNTRY_NOT_FOUND + return rate + + def gls_nl_rate_shipment(self, order): + recipient = self.get_recipient(order=order) + rate = None + dest_country = recipient.country_id.code + est_weight_value = sum([(line.product_id.weight * line.product_uom_qty) for line in order.order_line]) or 0.0 + if dest_country: + rate = self._gls_nl_rate(dest_country, est_weight_value) + + # Handle errors and rate conversions. + error_message = None + if not dest_country or rate == self.GLS_NL_COUNTRY_NOT_FOUND: + error_message = _('Destination country not found: "%s"') % (dest_country, ) + if rate is None or error_message: + if not error_message: + error_message = _('Rate not found for weight: "%s"') % (est_weight_value, ) + return {'success': False, + 'price': 0.0, + 'error_message': error_message, + 'warning_message': False} + + euro_currency = self.env['res.currency'].search([('name', '=', 'EUR')], limit=1) + if euro_currency and order.currency_id and euro_currency != order.currency_id: + rate = euro_currency._convert(rate, + order.currency_id, + order.company_id, + order.date_order or fields.Date.today()) + + return {'success': True, + 'price': rate, + 'error_message': False, + 'warning_message': False} + + def _get_gls_nl_service(self): + return GLSNLRequest(self.prod_environment) + + def _gls_nl_make_address(self, partner): + # Addresses look like + # { + # 'name1': '', + # 'name2': '', + # 'name3': '', + # 'street': '', + # 'houseNo': '', + # 'houseNoExt': '', + # 'zipCode': '', + # 'city': '', + # 'countrycode': '', + # 'contact': '', + # 'phone': '', + # 'email': '', + # } + address = {} + pieces = partner.street.split(' ') + street = ' '.join(pieces[:-1]).strip(' ,') + house = pieces[-1] + address['name1'] = partner.name + address['street'] = street + address['houseNo'] = house + if partner.street2: + address['houseNoExt'] = partner.street2 + address['zipCode'] = partner.zip + address['city'] = partner.city + address['countrycode'] = partner.country_id.code + if partner.phone: + address['phone'] = partner.phone + if partner.email: + address['email'] = partner.email + return address + + def gls_nl_send_shipping(self, pickings): + res = [] + sudoself = self.sudo() + service = sudoself._get_gls_nl_service() + + for picking in pickings: + #company = self.get_shipper_company(picking=picking) # Requester not needed currently + from_ = self.get_shipper_warehouse(picking=picking) + to = self.get_recipient(picking=picking) + total_rate = 0.0 + + request_body = { + 'labelType': sudoself.gls_nl_labeltype, + 'username': sudoself.gls_nl_username, + 'password': sudoself.gls_nl_password, + 'shiptype': 'p', # note not shipType, 'f' for Freight + 'trackingLinkType': 's', + # 'customerNo': '', # needed if there are more 'customer numbers' attached to a single MyGLS API Account + 'reference': picking.name, + 'addresses': { + 'pickupAddress': self._gls_nl_make_address(from_), + 'deliveryAddress': self._gls_nl_make_address(to), + #'requesterAddress': {}, # Not needed currently + }, + 'units': [], + 'services': {}, + 'shippingDate': fields.Date.to_string(fields.Date.today()), + 'shippingSystemName': self.GLS_NL_SOFTWARE_NAME, + 'shippingSystemVersion': self.GLS_NL_SOFTWARE_VER, + } + + if sudoself.gls_nl_express: + request_body['services']['expressService'] = sudoself.gls_nl_express + + # Build out units + # Units look like: + # { + # 'unitId': 'A', + # 'unitType': '', # only for freight + # 'weight': 0.0, + # 'additionalInfo1': '', + # 'additionalInfo2': '', + # } + if picking.package_ids: + for package in picking.package_ids: + rate = self._gls_nl_rate(to.country_id.code, package.shipping_weight or 0.0) + if rate and rate != self.GLS_NL_COUNTRY_NOT_FOUND: + total_rate += rate + unit = { + 'unitId': package.name, + 'weight': package.shipping_weight, + } + request_body['units'].append(unit) + else: + rate = self._gls_nl_rate(to.country_id.code, picking.shipping_weight or 0.0) + if rate and rate != self.GLS_NL_COUNTRY_NOT_FOUND: + total_rate += rate + unit = { + 'unitId': picking.name, + 'weight': picking.shipping_weight, + } + request_body['units'].append(unit) + + try: + # Create label + label = service.create_label(request_body) + trackings = [] + uniq_nos = [] + attachments = [] + for i, unit in enumerate(label['units'], 1): + trackings.append(unit['unitNo']) + uniq_nos.append(unit['uniqueNo']) + attachments.append(('LabelGLSNL-%s-%s.%s' % (unit['unitNo'], i, sudoself.gls_nl_labeltype), unit['label'])) + + tracking = ', '.join(set(trackings)) + logmessage = _("Shipment created into GLS NL
" + "Tracking Number: %s
" + "UniqueNo: %s") % (tracking, ', '.join(set(uniq_nos))) + picking.message_post(body=logmessage, attachments=attachments) + shipping_data = {'exact_price': total_rate, 'tracking_number': tracking} + res.append(shipping_data) + except HTTPError as e: + raise ValidationError(e) + return res + + def gls_nl_get_tracking_link(self, pickings): + return 'https://gls-group.eu/EU/en/parcel-tracking?match=%s' % pickings.carrier_tracking_ref + + def gls_nl_cancel_shipment(self, picking): + sudoself = self.sudo() + service = sudoself._get_gls_nl_service() + try: + request_body = { + 'unitNo': picking.carrier_tracking_ref, + 'username': sudoself.gls_nl_username, + 'password': sudoself.gls_nl_password, + 'shiptype': 'p', + 'shippingSystemName': self.GLS_NL_SOFTWARE_NAME, + 'shippingSystemVersion': self.GLS_NL_SOFTWARE_VER, + } + service.delete_label(request_body) + picking.message_post(body=_('Shipment N° %s has been cancelled' % picking.carrier_tracking_ref)) + picking.write({'carrier_tracking_ref': '', 'carrier_price': 0.0}) + except HTTPError as e: + raise ValidationError(e) diff --git a/delivery_gls_nl/models/gls_nl_request.py b/delivery_gls_nl/models/gls_nl_request.py new file mode 100644 index 00000000..9011ed69 --- /dev/null +++ b/delivery_gls_nl/models/gls_nl_request.py @@ -0,0 +1,36 @@ +import requests +from json import dumps + + +class GLSNLRequest: + def __init__(self, production): + self.production = production + self.api_key = '234a6d4ad5fd4d039526a8a1074051ee' if production else 'f80d41c6f7d542878c9c0a4295de7a6a' + self.url = 'https://api.gls.nl/V1/api' if production else 'https://api.gls.nl/Test/V1/api' + self.headers = self._make_headers() + + def _make_headers(self): + return { + 'Content-Type': 'application/json', + 'Ocp-Apim-Subscription-Key': self.api_key, + } + + def post_request(self, endpoint, body): + if not self.production and body.get('username') == 'test': + # Override to test credentials + body['username'] = 'apitest1@gls-netherlands.com' + body['password'] = '9PMev9qM' + url = self.url + endpoint + result = requests.request('POST', url, headers=self.headers, data=dumps(body)) + if result.status_code != 200: + raise requests.HTTPError(result.text) + return result.json() + + def create_label(self, body): + return self.post_request('/Label/Create', body) + + def confirm_label(self, body): + return self.post_request('/Label/Confirm', body) + + def delete_label(self, body): + return self.post_request('/Label/Delete', body) diff --git a/delivery_gls_nl/views/delivery_gls_nl_view.xml b/delivery_gls_nl/views/delivery_gls_nl_view.xml new file mode 100644 index 00000000..8a928d17 --- /dev/null +++ b/delivery_gls_nl/views/delivery_gls_nl_view.xml @@ -0,0 +1,61 @@ + + + + + delivery.carrier.form.provider.gls_nl + delivery.carrier + + + + + + + + + + + + + + + + From f1b689e7fe6e66a2911623e75e405507aaf064c5 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 30 Oct 2020 09:15:22 -0700 Subject: [PATCH 07/13] [MIG] pos_elavon: to Odoo 12 --- pos_elavon/__manifest__.py | 2 +- pos_elavon/views/pos_config_setting_views.xml | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pos_elavon/__manifest__.py b/pos_elavon/__manifest__.py index 4ccb76ba..fc66d79e 100644 --- a/pos_elavon/__manifest__.py +++ b/pos_elavon/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Elavon Payment Services', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'category': 'Point of Sale', 'sequence': 6, 'summary': 'Credit card support for Point Of Sale', diff --git a/pos_elavon/views/pos_config_setting_views.xml b/pos_elavon/views/pos_config_setting_views.xml index 1fd2b0d9..27972ed0 100644 --- a/pos_elavon/views/pos_config_setting_views.xml +++ b/pos_elavon/views/pos_config_setting_views.xml @@ -6,12 +6,16 @@ pos.config -
- -
-
+
- +
From ace92d0511ef6ad59052d3a130b390c39545afd0 Mon Sep 17 00:00:00 2001 From: Connor Christian Date: Fri, 30 Oct 2020 14:37:14 -0400 Subject: [PATCH 08/13] [MIG] helpdesk_rma: for Odoo 12.0 Additionally hide button if there is no assigned customer --- helpdesk_rma/__manifest__.py | 2 +- helpdesk_rma/views/helpdesk_views.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/helpdesk_rma/__manifest__.py b/helpdesk_rma/__manifest__.py index 6a4386f0..03a4a9ce 100755 --- a/helpdesk_rma/__manifest__.py +++ b/helpdesk_rma/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Helpdesk RMA', 'summary': 'Adds RMA functionality to the Helpdesk App', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Helpdesk', 'license': 'AGPL-3', diff --git a/helpdesk_rma/views/helpdesk_views.xml b/helpdesk_rma/views/helpdesk_views.xml index 412fce27..66d1639e 100644 --- a/helpdesk_rma/views/helpdesk_views.xml +++ b/helpdesk_rma/views/helpdesk_views.xml @@ -10,6 +10,7 @@ From 64095adb0592fc126208d1091d4f5758984b4015 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 29 Oct 2020 11:25:45 -0700 Subject: [PATCH 09/13] [MOV] helpdesk_sales: from hibou-suite-enterprise:11.0 --- helpdesk_sales/__init__.py | 1 + helpdesk_sales/__manifest__.py | 22 ++++++++++++++++++++++ helpdesk_sales/models/__init__.py | 1 + helpdesk_sales/models/helpdesk.py | 15 +++++++++++++++ helpdesk_sales/views/helpdesk_views.xml | 20 ++++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 helpdesk_sales/__init__.py create mode 100755 helpdesk_sales/__manifest__.py create mode 100644 helpdesk_sales/models/__init__.py create mode 100644 helpdesk_sales/models/helpdesk.py create mode 100644 helpdesk_sales/views/helpdesk_views.xml diff --git a/helpdesk_sales/__init__.py b/helpdesk_sales/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/helpdesk_sales/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/helpdesk_sales/__manifest__.py b/helpdesk_sales/__manifest__.py new file mode 100755 index 00000000..008a5ac2 --- /dev/null +++ b/helpdesk_sales/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Helpdesk Sales', + 'summary': 'Adds smart button on Helpdesk Tickets to see and create Sale Orders', + 'version': '11.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Helpdesk', + 'license': 'AGPL-3', + 'images': [], + 'website': "https://hibou.io", + 'description': "Adds smart button on Helpdesk Tickets to see and create Sale Orders", + 'depends': [ + 'helpdesk', + 'sale', + 'sale_management', + ], + 'demo': [], + 'data': [ + 'views/helpdesk_views.xml', + ], + 'auto_install': False, + 'installable': True, + } diff --git a/helpdesk_sales/models/__init__.py b/helpdesk_sales/models/__init__.py new file mode 100644 index 00000000..a3d4b803 --- /dev/null +++ b/helpdesk_sales/models/__init__.py @@ -0,0 +1 @@ +from . import helpdesk diff --git a/helpdesk_sales/models/helpdesk.py b/helpdesk_sales/models/helpdesk.py new file mode 100644 index 00000000..26b149ec --- /dev/null +++ b/helpdesk_sales/models/helpdesk.py @@ -0,0 +1,15 @@ +from odoo import api, models, fields + + +class Ticket(models.Model): + _inherit = 'helpdesk.ticket' + + sale_order_count = fields.Integer(related='partner_id.sale_order_count', string='# of Sale Orders') + + def action_partner_sales(self): + self.ensure_one() + action = self.env.ref('sale.act_res_partner_2_sale_order').read()[0] + action['context'] = { + 'search_default_partner_id': self.partner_id.id, + } + return action diff --git a/helpdesk_sales/views/helpdesk_views.xml b/helpdesk_sales/views/helpdesk_views.xml new file mode 100644 index 00000000..4f12041b --- /dev/null +++ b/helpdesk_sales/views/helpdesk_views.xml @@ -0,0 +1,20 @@ + + + + + + helpdesk.ticket.form.inherit + helpdesk.ticket + + + + + + + + + From 0d8b542f2c961014ae07859afd83d0ece7039c0c Mon Sep 17 00:00:00 2001 From: Connor Christian Date: Fri, 30 Oct 2020 15:00:54 -0400 Subject: [PATCH 10/13] [MIG] helpdesk_sales: for Odoo 12.0 Additionally hide button if there is no assigned customer --- helpdesk_sales/__manifest__.py | 2 +- helpdesk_sales/views/helpdesk_views.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/helpdesk_sales/__manifest__.py b/helpdesk_sales/__manifest__.py index 008a5ac2..134a6457 100755 --- a/helpdesk_sales/__manifest__.py +++ b/helpdesk_sales/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Helpdesk Sales', 'summary': 'Adds smart button on Helpdesk Tickets to see and create Sale Orders', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Helpdesk', 'license': 'AGPL-3', diff --git a/helpdesk_sales/views/helpdesk_views.xml b/helpdesk_sales/views/helpdesk_views.xml index 4f12041b..7fd09982 100644 --- a/helpdesk_sales/views/helpdesk_views.xml +++ b/helpdesk_sales/views/helpdesk_views.xml @@ -10,6 +10,7 @@ From b88b08c38d0a9c2e1c1b6d8d11896f6a22618d14 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 12 May 2018 14:11:37 -0700 Subject: [PATCH 11/13] Initial commit `account_invoice_change` and `account_invoice_change_analytic` for 11.0 --- account_invoice_change/__init__.py | 1 + account_invoice_change/__manifest__.py | 30 ++++++++++ account_invoice_change/tests/__init__.py | 1 + .../tests/test_invoice_change.py | 60 +++++++++++++++++++ account_invoice_change/wizard/__init__.py | 1 + .../wizard/invoice_change.py | 56 +++++++++++++++++ .../wizard/invoice_change_views.xml | 48 +++++++++++++++ 7 files changed, 197 insertions(+) create mode 100644 account_invoice_change/__init__.py create mode 100644 account_invoice_change/__manifest__.py create mode 100644 account_invoice_change/tests/__init__.py create mode 100644 account_invoice_change/tests/test_invoice_change.py create mode 100644 account_invoice_change/wizard/__init__.py create mode 100644 account_invoice_change/wizard/invoice_change.py create mode 100644 account_invoice_change/wizard/invoice_change_views.xml diff --git a/account_invoice_change/__init__.py b/account_invoice_change/__init__.py new file mode 100644 index 00000000..40272379 --- /dev/null +++ b/account_invoice_change/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/account_invoice_change/__manifest__.py b/account_invoice_change/__manifest__.py new file mode 100644 index 00000000..7cb8459d --- /dev/null +++ b/account_invoice_change/__manifest__.py @@ -0,0 +1,30 @@ +{ + 'name': 'Account Invoice Change', + 'author': 'Hibou Corp. ', + 'version': '11.0.1.0.0', + 'category': 'Accounting', + 'sequence': 95, + 'summary': 'Technical foundation for changing invoices.', + 'description': """ +Technical foundation for changing invoices. + +Creates wizard and permissions for making invoice changes that can be +handled by other individual modules. + +This module implements, as examples, how to change the Salesperson and Date fields. + +Abstractly, individual 'changes' should come from specific 'fields' or capability +modules that handle the consequences of changing that field in whatever state the +the invoice is currently in. + + """, + 'website': 'https://hibou.io/', + 'depends': [ + 'account', + ], + 'data': [ + 'wizard/invoice_change_views.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/account_invoice_change/tests/__init__.py b/account_invoice_change/tests/__init__.py new file mode 100644 index 00000000..defd2696 --- /dev/null +++ b/account_invoice_change/tests/__init__.py @@ -0,0 +1 @@ +from . import test_invoice_change diff --git a/account_invoice_change/tests/test_invoice_change.py b/account_invoice_change/tests/test_invoice_change.py new file mode 100644 index 00000000..3809241c --- /dev/null +++ b/account_invoice_change/tests/test_invoice_change.py @@ -0,0 +1,60 @@ +from odoo.addons.account.tests.account_test_users import AccountTestUsers +from odoo import fields + +class TestInvoiceChange(AccountTestUsers): + + def test_invoice_change_basic(self): + self.account_invoice_obj = self.env['account.invoice'] + self.payment_term = self.env.ref('account.account_payment_term_advance') + self.journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0] + self.partner3 = self.env.ref('base.res_partner_3') + account_user_type = self.env.ref('account.data_account_type_receivable') + self.account_rec1_id = self.account_model.sudo(self.account_manager.id).create(dict( + code="cust_acc", + name="customer account", + user_type_id=account_user_type.id, + reconcile=True, + )) + invoice_line_data = [ + (0, 0, + { + 'product_id': self.env.ref('product.product_product_5').id, + 'quantity': 10.0, + 'account_id': self.env['account.account'].search( + [('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id, + 'name': 'product test 5', + 'price_unit': 100.00, + } + ) + ] + self.invoice_basic = self.account_invoice_obj.sudo(self.account_user.id).create(dict( + name="Test Customer Invoice", + reference_type="none", + payment_term_id=self.payment_term.id, + journal_id=self.journalrec.id, + partner_id=self.partner3.id, + account_id=self.account_rec1_id.id, + invoice_line_ids=invoice_line_data + )) + self.assertEqual(self.invoice_basic.state, 'draft') + self.invoice_basic.action_invoice_open() + self.assertEqual(self.invoice_basic.state, 'open') + self.assertEqual(self.invoice_basic.date, fields.Date.today()) + self.assertEqual(self.invoice_basic.user_id, self.account_user) + self.assertEqual(self.invoice_basic.move_id.date, fields.Date.today()) + self.assertEqual(self.invoice_basic.move_id.line_ids[0].date, fields.Date.today()) + + ctx = {'active_model': 'account.invoice', 'active_ids': [self.invoice_basic.id]} + change = self.env['account.invoice.change'].with_context(ctx).create({}) + self.assertEqual(change.date, self.invoice_basic.date) + self.assertEqual(change.user_id, self.invoice_basic.user_id) + + change_date = '2018-01-01' + change_user = self.env.user + change.write({'user_id': change_user.id, 'date': change_date}) + + change.affect_change() + self.assertEqual(self.invoice_basic.date, change_date) + self.assertEqual(self.invoice_basic.user_id, change_user) + self.assertEqual(self.invoice_basic.move_id.date, change_date) + self.assertEqual(self.invoice_basic.move_id.line_ids[0].date, change_date) diff --git a/account_invoice_change/wizard/__init__.py b/account_invoice_change/wizard/__init__.py new file mode 100644 index 00000000..8ec5d6da --- /dev/null +++ b/account_invoice_change/wizard/__init__.py @@ -0,0 +1 @@ +from . import invoice_change \ No newline at end of file diff --git a/account_invoice_change/wizard/invoice_change.py b/account_invoice_change/wizard/invoice_change.py new file mode 100644 index 00000000..def9583f --- /dev/null +++ b/account_invoice_change/wizard/invoice_change.py @@ -0,0 +1,56 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class InvoiceChangeWizard(models.TransientModel): + _name = 'account.invoice.change' + _description = 'Invoice Change' + + invoice_id = fields.Many2one('account.invoice', string='Invoice', readonly=True, required=True) + invoice_company_id = fields.Many2one('res.company', readonly=True, related='invoice_id.company_id') + user_id = fields.Many2one('res.users', string='Salesperson') + date = fields.Date(string='Accounting Date') + + @api.model + def default_get(self, fields): + rec = super(InvoiceChangeWizard, self).default_get(fields) + context = dict(self._context or {}) + active_model = context.get('active_model') + active_ids = context.get('active_ids') + + # Checks on context parameters + if not active_model or not active_ids: + raise UserError( + _("Programmation error: wizard action executed without active_model or active_ids in context.")) + if active_model != 'account.invoice': + raise UserError(_( + "Programmation error: the expected model for this action is 'account.invoice'. The provided one is '%d'.") % active_model) + + # Checks on received invoice records + invoice = self.env[active_model].browse(active_ids) + if len(invoice) != 1: + raise UserError(_("Invoice Change expects only one invoice.")) + rec.update({ + 'invoice_id': invoice.id, + 'user_id': invoice.user_id.id, + 'date': invoice.date, + }) + return rec + + def _new_invoice_vals(self): + vals = {} + if self.invoice_id.user_id != self.user_id: + vals['user_id'] = self.user_id.id + if self.invoice_id.date != self.date: + vals['date'] = self.date + return vals + + @api.multi + def affect_change(self): + self.ensure_one() + vals = self._new_invoice_vals() + if vals: + self.invoice_id.write(vals) + if 'date' in vals and self.invoice_id.move_id: + self.invoice_id.move_id.write({'date': vals['date']}) + return True diff --git a/account_invoice_change/wizard/invoice_change_views.xml b/account_invoice_change/wizard/invoice_change_views.xml new file mode 100644 index 00000000..5852570c --- /dev/null +++ b/account_invoice_change/wizard/invoice_change_views.xml @@ -0,0 +1,48 @@ + + + + Invoice Change + account.invoice.change + +
+ + + + + + + + + +
+
+ +
+
+ + + Invoice Change Wizard + ir.actions.act_window + account.invoice.change + form + form + new + + + + account.invoice.form.inherit + account.invoice + + + +