diff --git a/shipstation_integration/patches.txt b/shipstation_integration/patches.txt index 2885d01..36956a4 100644 --- a/shipstation_integration/patches.txt +++ b/shipstation_integration/patches.txt @@ -1,2 +1,3 @@ shipstation_integration.patches.set_enable_checks_in_shipstation_store -shipstation_integration.patches.update_shipstation_warehouses \ No newline at end of file +shipstation_integration.patches.update_shipstation_warehouses +shipstation_integration.patches.delete_delivery_note_shipment_custom_fields \ No newline at end of file diff --git a/shipstation_integration/patches/delete_delivery_note_shipment_custom_fields.py b/shipstation_integration/patches/delete_delivery_note_shipment_custom_fields.py new file mode 100644 index 0000000..df0cb97 --- /dev/null +++ b/shipstation_integration/patches/delete_delivery_note_shipment_custom_fields.py @@ -0,0 +1,14 @@ +import frappe + + +def execute(): + delivery_note_custom_fields = [ + 'Delivery Note-shipment_details', + 'Delivery Note-sb_shipment', + 'Delivery Note-carrier', + 'Delivery Note-tracking_number', + 'Delivery Note-cb_shipment', + 'Delivery Note-carrier_service' + ] + for custom_field in delivery_note_custom_fields: + frappe.delete_doc_if_exists("Custom Field", custom_field) diff --git a/shipstation_integration/setup.py b/shipstation_integration/setup.py index 75e3117..6556708 100644 --- a/shipstation_integration/setup.py +++ b/shipstation_integration/setup.py @@ -1,6 +1,7 @@ import frappe from frappe import _ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.custom.doctype.property_setter.property_setter import make_property_setter def get_setup_stages(args=None): @@ -244,46 +245,46 @@ def setup_custom_fields(args=None): label="Shipstation Shipment ID", insert_after="shipstation_order_id", translatable=False, - ), - dict( - fieldtype="Section Break", - fieldname="sb_shipment", - collapsible=True, - label="Shipment Details", - insert_after="has_pii", - ), - dict( - fieldtype="Data", - fieldname="carrier", - read_only=True, - label="Carrier", - insert_after="sb_shipment", - translatable=False, - ), - dict( - fieldtype="Data", - fieldname="tracking_number", - read_only=True, - label="Tracking Number", - insert_after="carrier", - translatable=False, - ), - dict( - fieldtype="Column Break", - fieldname="cb_shipment", - insert_after="tracking_number", - ), - dict( - fieldtype="Data", - fieldname="carrier_service", - read_only=True, - label="Carrier Service", - insert_after="cb_shipment", - translatable=False, - ), + ) ] ) + shipment_fields = [ + dict( + fieldtype="Data", + fieldname="shipstation_store_name", + read_only=True, + label="Shipstation Store", + insert_after="shipment_amount", + translatable=False, + ), + dict( + fieldtype="Data", + fieldname="shipstation_order_id", + read_only=True, + label="Shipstation Order ID", + insert_after="shipstation_store_name", + in_standard_filter=True, + translatable=False, + ), + dict( + fieldtype="Data", + fieldname="marketplace", + read_only=True, + label="Marketplace", + insert_after="awb_number", + translatable=False, + ), + dict( + fieldtype="Data", + fieldname="marketplace_order_id", + read_only=True, + label="Marketplace Order ID", + insert_after="marketplace", + translatable=False, + ) + ] + custom_fields = { "Item": item_fields, "Warehouse": warehouse_fields, @@ -293,7 +294,53 @@ def setup_custom_fields(args=None): "Sales Invoice Item": common_custom_sales_item_fields, "Delivery Note": delivery_note_fields, "Delivery Note Item": common_custom_sales_item_fields, + "Shipment": shipment_fields, } print("Creating custom fields for Shipstation") create_custom_fields(custom_fields) + + property_setters = [ + dict( + doctype="Shipment Parcel", + fieldname="length", + property="label", + property_type="Text", + value="Length (Inch)", + ), + dict( + doctype="Shipment Parcel", + fieldname="width", + property="label", + property_type="Text", + value="Width (Inch)", + ), + dict( + doctype="Shipment Parcel", + fieldname="height", + property="label", + property_type="Text", + value="Height (Inch)", + ), + dict( + doctype="Shipment Parcel", + fieldname="weight", + property="label", + property_type="Text", + value="Weight (Ounce)", + ) + ] + + print("Creating property setters for Shipstation") + for property_setter in property_setters: + if not frappe.db.exists( + "Property Setter", + dict( + doc_type=property_setter.get("doctype"), + field_name=property_setter.get("fieldname"), + property=property_setter.get("property"), + property_type=property_setter.get("property_type"), + value=property_setter.get("value"), + ), + ): + make_property_setter(**property_setter) diff --git a/shipstation_integration/shipments.py b/shipstation_integration/shipments.py index d18a002..b5dc4bf 100644 --- a/shipstation_integration/shipments.py +++ b/shipstation_integration/shipments.py @@ -7,18 +7,24 @@ from frappe.utils import getdate from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment if TYPE_CHECKING: from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice from erpnext.stock.doctype.delivery_note.delivery_note import DeliveryNote + from erpnext.stock.doctype.shipment.shipment import Shipment from shipstation.models import ShipStationOrder - from shipstation_integration.shipstation_integration.doctype.shipstation_store.shipstation_store import ShipstationStore - from shipstation_integration.shipstation_integration.doctype.shipstation_settings.shipstation_settings import ShipstationSettings + from shipstation_integration.shipstation_integration.doctype.shipstation_store.shipstation_store import ( + ShipstationStore, + ) + from shipstation_integration.shipstation_integration.doctype.shipstation_settings.shipstation_settings import ( + ShipstationSettings, + ) def list_shipments( settings: "ShipstationSettings" = None, - last_shipment_datetime: "datetime.datetime" = None + last_shipment_datetime: "datetime.datetime" = None, ): """ Fetch Shipstation shipments and create Sales Invoice and Delivery Note documents. @@ -40,7 +46,9 @@ def list_shipments( settings = [settings] for sss in settings: - sss_doc: "ShipstationSettings" = frappe.get_doc("Shipstation Settings", sss.name) + sss_doc: "ShipstationSettings" = frappe.get_doc( + "Shipstation Settings", sss.name + ) if not sss_doc.enabled: continue @@ -49,7 +57,9 @@ def list_shipments( if not last_shipment_datetime: # Get data for the last day, Shipstation API behaves oddly when it's a shorter period - last_shipment_datetime = datetime.datetime.utcnow() - datetime.timedelta(hours=24) + last_shipment_datetime = datetime.datetime.utcnow() - datetime.timedelta( + hours=24 + ) store: "ShipstationStore" for store in sss_doc.shipstation_stores: @@ -57,15 +67,18 @@ def list_shipments( continue parameters = { - 'store_id': store.store_id, - 'create_date_start': last_shipment_datetime, - 'create_date_end': datetime.datetime.utcnow() + "store_id": store.store_id, + "create_date_start": last_shipment_datetime, + "create_date_end": datetime.datetime.utcnow(), + "include_shipment_items": True } try: shipments = client.list_shipments(parameters=parameters) except HTTPError as e: - frappe.log_error(title="Error while fetching Shipstation shipment", message=e) + frappe.log_error( + title="Error while fetching Shipstation shipment", message=e + ) continue shipment: Optional["ShipStationOrder"] @@ -75,18 +88,25 @@ def list_shipments( continue # if a date filter is set in Shipstation Settings, don't create orders before that date - if sss_doc.since_date and getdate(shipment.create_date) < sss_doc.since_date: + if ( + sss_doc.since_date + and getdate(shipment.create_date) < sss_doc.since_date + ): continue - if frappe.db.exists("Delivery Note", - {"docstatus": 1, "shipstation_order_id": shipment.order_id}): + if frappe.db.exists( + "Delivery Note", + {"docstatus": 1, "shipstation_order_id": shipment.order_id}, + ): if shipment.voided: cancel_voided_shipments(shipment) else: create_erpnext_shipment(shipment, store) -def create_erpnext_shipment(shipment: "ShipStationOrder", store: "ShipstationStore") -> Union[str, None]: +def create_erpnext_shipment( + shipment: "ShipStationOrder", store: "ShipstationStore" +) -> Union[str, None]: """ Create a Delivery Note using shipment data from Shipstation @@ -106,23 +126,30 @@ def create_erpnext_shipment(shipment: "ShipStationOrder", store: "ShipstationSto return delivery_note = create_delivery_note(shipment, sales_invoice) + shipment = create_shipment(shipment, delivery_note, store) return delivery_note.name -def cancel_voided_shipments(shipment): - existing_dn = frappe.db.get_value("Delivery Note", - {"docstatus": 1, "shipstation_shipment_id": shipment.shipment_id}) +def cancel_voided_shipments(shipment: "ShipStationOrder"): + existing_dn = frappe.db.get_value( + "Delivery Note", + {"docstatus": 1, "shipstation_shipment_id": shipment.shipment_id}, + ) if existing_dn: frappe.get_doc("Delivery Note", existing_dn).cancel() - existing_si = frappe.db.get_value("Sales Invoice", - {"docstatus": 1, "shipstation_shipment_id": shipment.shipment_id}) + existing_si = frappe.db.get_value( + "Sales Invoice", + {"docstatus": 1, "shipstation_shipment_id": shipment.shipment_id}, + ) if existing_si: frappe.get_doc("Sales Invoice", existing_si).cancel() -def create_sales_invoice(shipment, store): - so_name = frappe.get_value('Sales Order', {'shipstation_order_id': shipment.order_id}) +def create_sales_invoice(shipment: "ShipStationOrder", store: "ShipstationStore"): + so_name = frappe.get_value( + "Sales Order", {"shipstation_order_id": shipment.order_id} + ) if not so_name: return @@ -131,25 +158,25 @@ def create_sales_invoice(shipment, store): si.cost_center = store.cost_center if shipment.shipment_cost: - si.append('taxes', { - 'charge_type': 'Actual', - 'account_head': store.shipping_expense_account, - 'description': 'Shipstation Shipping Cost', - 'tax_amount': -shipment.shipment_cost, - 'cost_center': store.cost_center - }) + si.append( + "taxes", + { + "charge_type": "Actual", + "account_head": store.shipping_expense_account, + "description": "Shipstation Shipping Cost", + "tax_amount": -shipment.shipment_cost, + "cost_center": store.cost_center, + }, + ) si.save() si.submit() return si -def create_delivery_note(shipment, sales_invoice): +def create_delivery_note(shipment: "ShipStationOrder", sales_invoice: "SalesInvoice"): dn: "DeliveryNote" = make_delivery_note(sales_invoice.name) dn.shipstation_shipment_id = shipment.shipment_id - dn.carrier = shipment.carrier_code.upper() - dn.carrier_service = shipment.service_code.upper() - dn.tracking_number = shipment.tracking_number for row in dn.items: row.allow_zero_valuation_rate = 1 # if row.rate < 0.001 else 0 @@ -158,3 +185,56 @@ def create_delivery_note(shipment, sales_invoice): dn.submit() frappe.db.commit() return dn + + +def create_shipment( + shipment: "ShipStationOrder", + delivery_note: "DeliveryNote", + store: "ShipstationStore", +): + shipment_doc: "Shipment" = make_shipment(delivery_note.name) + shipment_doc.update( + { + "shipment_id": shipment.shipment_id, + "pickup_date": shipment.create_date, + "carrier": shipment.carrier_code, + "carrier_service": shipment.service_code, + "awb_number": shipment.tracking_number, + "service_provider": "Shipstation", + "incoterm": "DAP (Delivered At Place)", + "shipstation_store_name": store.store_name, + "shipstation_order_id": shipment.order_id, + "marketplace": store.marketplace_name, + "marketplace_order_id": shipment.order_number, + } + ) + + if shipment.shipment_items: + description = "" + for count, shipment_item in enumerate(shipment.shipment_items, 1): + stock_uom = frappe.db.get_value('Item', {'item_name': shipment_item.name}, 'stock_uom') + description += f"{count}. {shipment_item.name} - {shipment_item.quantity} {stock_uom}\n" + + shipment_doc.update({ + "description_of_content": description + }) + + if shipment.dimensions: + shipment_doc.append( + "shipment_parcel", + { + "length": shipment.dimensions.length, + "width": shipment.dimensions.width, + "height": shipment.dimensions.height, + "weight": shipment.weight.value or 0.01, + }, + ) + + shipment_doc.flags.ignore_mandatory = True + shipment_doc.run_method("set_missing_values") + + shipment_doc.save() + shipment_doc.submit() + frappe.db.commit() + + return shipment