diff --git a/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/__init__.py b/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/pick_list_sales_order_details.json b/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/pick_list_sales_order_details.json new file mode 100644 index 00000000..9e715785 --- /dev/null +++ b/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/pick_list_sales_order_details.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "creation": "2023-04-19 11:57:15.149202", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_order", + "sales_invoice", + "posting_date", + "pick_status", + "invoice_url", + "invoice_pdf" + ], + "fields": [ + { + "fieldname": "sales_order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Order", + "options": "Sales Order" + }, + { + "fieldname": "sales_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Invoice", + "options": "Sales Invoice" + }, + { + "fetch_from": "sales_invoice.posting_date", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "fieldname": "pick_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Pick Status", + "options": "\nPartially Picked\nFully Picked" + }, + { + "fieldname": "invoice_url", + "fieldtype": "Data", + "hidden": 1, + "label": "Invoice URL" + }, + { + "fieldname": "invoice_pdf", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Invoice Pdf" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-04-20 10:58:07.144994", + "modified_by": "Administrator", + "module": "Ecommerce Integrations", + "name": "Pick List Sales Order Details", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/pick_list_sales_order_details.py b/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/pick_list_sales_order_details.py new file mode 100644 index 00000000..8fdb9ce6 --- /dev/null +++ b/ecommerce_integrations/ecommerce_integrations/doctype/pick_list_sales_order_details/pick_list_sales_order_details.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PickListSalesOrderDetails(Document): + pass diff --git a/ecommerce_integrations/hooks.py b/ecommerce_integrations/hooks.py index ccc2bcc0..5d2d97d1 100644 --- a/ecommerce_integrations/hooks.py +++ b/ecommerce_integrations/hooks.py @@ -37,6 +37,7 @@ "Sales Invoice": "public/js/unicommerce/sales_invoice.js", "Item": "public/js/unicommerce/item.js", "Stock Entry": "public/js/unicommerce/stock_entry.js", + "Pick List": "public/js/unicommerce/pick_list.js", } # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} @@ -114,6 +115,11 @@ "on_cancel": "ecommerce_integrations.unicommerce.grn.prevent_grn_cancel", }, "Item Price": {"on_change": "ecommerce_integrations.utils.price_list.discard_item_prices"}, + "Pick List": {"validate": "ecommerce_integrations.unicommerce.pick_list.validate"}, + "Sales Invoice": { + "on_submit": "ecommerce_integrations.unicommerce.invoice.on_submit", + "on_cancel": "ecommerce_integrations.unicommerce.invoice.on_cancel", + }, } # Scheduled Tasks diff --git a/ecommerce_integrations/public/js/unicommerce/pick_list.js b/ecommerce_integrations/public/js/unicommerce/pick_list.js new file mode 100644 index 00000000..679bace9 --- /dev/null +++ b/ecommerce_integrations/public/js/unicommerce/pick_list.js @@ -0,0 +1,47 @@ +frappe.ui.form.on('Pick List', { + refresh(frm){ + if (frm.doc.order_details){ + frm.add_custom_button(__('Generate Invoice'), () => frm.trigger('generate_invoice')) + } + }, + generate_invoice(frm){ + let selected_so = [] + var tbl = frm.doc.order_details || []; + for(var i = 0; i < tbl.length; i++) { + selected_so.push(tbl[i].sales_order) + } + let sales_orders = []; + let so_item_list = []; + const warehouse_allocation = {}; + selected_so.forEach(function(so) { + const item_details = frm.doc.locations.map((item) => { + if (item.sales_order == so && item.picked_qty > 0){ + so_item_list.push({so_item:item.sales_order_item, + qty:item.qty + }); + return { + sales_order_row: item.sales_order_item, + item_code: item.item_code, + warehouse: item.warehouse, + shelf:item.shelf + } + } + else{ + return {} + } + }); + sales_orders.push(so); + warehouse_allocation[so] = item_details.filter(value => Object.keys(value).length !== 0); + }); + frappe.call({ + method: 'ecommerce_integrations.unicommerce.invoice.generate_unicommerce_invoices', + args: { + 'sales_orders': sales_orders, + 'warehouse_allocation': warehouse_allocation + }, + freeze: true, + freeze_message: "Requesting Invoice generation. Once synced, invoice will appear in linked documents.", + }); + + }, +}) diff --git a/ecommerce_integrations/unicommerce/constants.py b/ecommerce_integrations/unicommerce/constants.py index 261caf8e..9bc2ba8c 100644 --- a/ecommerce_integrations/unicommerce/constants.py +++ b/ecommerce_integrations/unicommerce/constants.py @@ -33,7 +33,7 @@ SHIPPING_PACKAGE_STATUS_FIELD = "unicommerce_shipping_package_status" IS_COD_CHECKBOX = "unicommerce_is_cod" SHIPPING_METHOD_FIELD = "unicommerce_shipping_method" - +PICKLIST_ORDER_DETAILS_FIELD = "order_details" GRN_STOCK_ENTRY_TYPE = "GRN on Unicommerce" diff --git a/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/__init__.py b/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/pick_list_sales_order_details.json b/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/pick_list_sales_order_details.json new file mode 100644 index 00000000..7d1012a4 --- /dev/null +++ b/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/pick_list_sales_order_details.json @@ -0,0 +1,70 @@ +{ + "actions": [], + "creation": "2023-04-19 11:57:15.149202", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "sales_order", + "sales_invoice", + "posting_date", + "pick_status", + "invoice_url", + "invoice_pdf" + ], + "fields": [ + { + "fieldname": "sales_order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Order", + "options": "Sales Order" + }, + { + "fieldname": "sales_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Sales Invoice", + "options": "Sales Invoice" + }, + { + "fetch_from": "sales_invoice.posting_date", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "fieldname": "pick_status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Pick Status", + "options": "\nPartially Picked\nFully Picked" + }, + { + "fieldname": "invoice_url", + "fieldtype": "Data", + "hidden": 1, + "label": "Invoice URL" + }, + { + "fieldname": "invoice_pdf", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Invoice Pdf" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-05-02 19:52:50.157639", + "modified_by": "Administrator", + "module": "unicommerce", + "name": "Pick List Sales Order Details", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/pick_list_sales_order_details.py b/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/pick_list_sales_order_details.py new file mode 100644 index 00000000..8fdb9ce6 --- /dev/null +++ b/ecommerce_integrations/unicommerce/doctype/pick_list_sales_order_details/pick_list_sales_order_details.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class PickListSalesOrderDetails(Document): + pass diff --git a/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py b/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py index bc2d44cb..00694dd4 100644 --- a/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py +++ b/ecommerce_integrations/unicommerce/doctype/unicommerce_settings/unicommerce_settings.py @@ -34,6 +34,7 @@ ORDER_ITEM_CODE_FIELD, ORDER_STATUS_FIELD, PACKAGE_TYPE_FIELD, + PICKLIST_ORDER_DETAILS_FIELD, PRODUCT_CATEGORY_FIELD, RETURN_CODE_FIELD, SHIPPING_METHOD_FIELD, @@ -427,6 +428,14 @@ def setup_custom_fields(update=True): read_only=1, ), ], + "Pick List": [ + dict( + fieldname=PICKLIST_ORDER_DETAILS_FIELD, + label="Order Details", + fieldtype="Table", + options="Pick List Sales Order Details", + ), + ], } # create sections first for proper ordering diff --git a/ecommerce_integrations/unicommerce/doctype/unicommerce_warehouses/unicommerce_warehouses.json b/ecommerce_integrations/unicommerce/doctype/unicommerce_warehouses/unicommerce_warehouses.json index 13676893..540538f4 100644 --- a/ecommerce_integrations/unicommerce/doctype/unicommerce_warehouses/unicommerce_warehouses.json +++ b/ecommerce_integrations/unicommerce/doctype/unicommerce_warehouses/unicommerce_warehouses.json @@ -62,7 +62,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-19 16:39:01.415522", + "modified": "2023-05-12 14:06:13.182121", "modified_by": "Administrator", "module": "unicommerce", "name": "Unicommerce Warehouses", @@ -70,5 +70,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/ecommerce_integrations/unicommerce/invoice.py b/ecommerce_integrations/unicommerce/invoice.py index ea7da578..d9d3bbed 100644 --- a/ecommerce_integrations/unicommerce/invoice.py +++ b/ecommerce_integrations/unicommerce/invoice.py @@ -58,41 +58,41 @@ def generate_unicommerce_invoices( 1. Get shipping package details using get_sale_order 2. Ask for invoice generation - - marketplace - create_invoice_and_label_by_shipping_code - - self-shipped - create_invoice_and_assign_shipper + - marketplace - create_invoice_and_label_by_shipping_code + - self-shipped - create_invoice_and_assign_shipper 3. Sync invoice. args: - sales_orders: list of sales order codes to invoice. - warehouse_allocation: If warehouse is changed while shipping / non-group warehouse is to be assigned then this parameter is required. - - Example of warehouse_allocation: - - { - "SO0042": [ - { - "item_code": "SKU", - # "qty": 1, always assumed to be 1 for Unicommerce orders. - "warehouse": "Stores - WP", - "sales_order_row": "5hh123k1", `name` of SO child table row - }, - { - "item_code": "SKU2", - # "qty": 1, - "warehouse": "Stores - WP", - "sales_order_row": "5hh123k1", `name` of SO child table row - }, - ], - "SO0101": [ - { - "item_code": "SKU3", - # "qty": 1 - "warehouse": "Stores - WP", - "sales_order_row": "5hh123k1", `name` of SO child table row - }, - ] - } + sales_orders: list of sales order codes to invoice. + warehouse_allocation: If warehouse is changed while shipping / non-group warehouse is to be assigned then this parameter is required. + + Example of warehouse_allocation: + + { + "SO0042": [ + { + "item_code": "SKU", + # "qty": 1, always assumed to be 1 for Unicommerce orders. + "warehouse": "Stores - WP", + "sales_order_row": "5hh123k1", `name` of SO child table row + }, + { + "item_code": "SKU2", + # "qty": 1, + "warehouse": "Stores - WP", + "sales_order_row": "5hh123k1", `name` of SO child table row + }, + ], + "SO0101": [ + { + "item_code": "SKU3", + # "qty": 1 + "warehouse": "Stores - WP", + "sales_order_row": "5hh123k1", `name` of SO child table row + }, + ] + } """ if isinstance(sales_orders, str): @@ -282,7 +282,7 @@ def _fetch_and_sync_invoice( """Use the invoice generation response to fetch actual invoice and sync them to ERPNext. args: - invoice_response: response returned by either of two invoice generation methods + invoice_response: response returned by either of two invoice generation methods """ so_data = client.get_sales_order(unicommerce_so_code) @@ -570,3 +570,52 @@ def update_cancellation_status(so_data, so) -> bool: from ecommerce_integrations.unicommerce.cancellation_and_returns import update_erpnext_order_items update_erpnext_order_items(so_data, so) + + +def on_submit(self, method=None): + sales_order = self.get("items")[0].sales_order + unicommerce_order_code = frappe.db.get_value("Sales Order", sales_order, "unicommerce_order_code") + if unicommerce_order_code: + attached_docs = frappe.get_all( + "File", + fields=["file_name"], + filters={"attached_to_name": self.name, "file_name": ("like", "unicommerce%")}, + order_by="file_name", + ) + url = frappe.get_all( + "File", + fields=["file_url"], + filters={"attached_to_name": self.name, "file_name": ("like", "unicommerce%")}, + order_by="file_name", + ) + pi_so = frappe.get_all( + "Pick List Sales Order Details", + fields=["name", "parent"], + filters=[{"sales_order": sales_order, "docstatus": 0}], + ) + for pl in pi_so: + if not pl.parent or not frappe.db.exists("Pick List", pl.parent): + continue + if attached_docs: + frappe.db.set_value( + "Pick List Sales Order Details", + pl.name, + { + "sales_invoice": self.name, + "invoice_url": attached_docs[0].file_name, + "invoice_pdf": url[0].file_url, + }, + ) + else: + frappe.db.set_value("Pick List Sales Order Details", pl.name, {"sales_invoice": self.name}) + + +def on_cancel(self, method=None): + results = frappe.db.get_all( + "Pick List Sales Order Details", filters={"sales_invoice": self.name, "docstatus": 1} + ) + if results: + # self.flags.ignore_links = True + ignored_doctypes = list(self.get("ignore_linked_doctypes", [])) + ignored_doctypes.append("Pick List") + self.ignore_linked_doctypes = ignored_doctypes diff --git a/ecommerce_integrations/unicommerce/pick_list.py b/ecommerce_integrations/unicommerce/pick_list.py new file mode 100644 index 00000000..d445079e --- /dev/null +++ b/ecommerce_integrations/unicommerce/pick_list.py @@ -0,0 +1,55 @@ +import json + +import frappe +from frappe import _ + + +def validate(self, method=None): + sales_order = self.get("locations")[0].sales_order + unicommerce_order_code = frappe.db.get_value("Sales Order", sales_order, "unicommerce_order_code") + if unicommerce_order_code: + if self.get("locations"): + for pl in self.get("locations"): + if pl.picked_qty and float(pl.picked_qty) > 0: + if pl.picked_qty > pl.qty: + pl.picked_qty = pl.qty + + frappe.throw(_("Row {0} Picked Qty cannot be more than Sales Order Qty").format(pl.idx)) + if pl.picked_qty == 0 and pl.docstatus == 1: + frappe.throw( + _("You have not picked {0} in row {1} . Pick the item to proceed!").format( + pl.item_code, pl.idx + ) + ) + item_so_list = [d.sales_order for d in self.get("locations")] + unique_so_list = [] + for i in item_so_list: + if i not in unique_so_list: + unique_so_list.append(i) + so_list = [d.sales_order for d in self.get("order_details")] + for so in unique_so_list: + if so not in so_list: + pl_so_child = self.append("order_details", {}) + pl_so_child.sales_order = so + total_item_count = 0 + fully_picked_item_count = 0 + partial_picked_item_count = 0 + for item in self.get("locations"): + if item.sales_order == so: + total_item_count = total_item_count + 1 + if item.picked_qty == item.qty: + fully_picked_item_count = fully_picked_item_count + 1 + elif int(item.picked_qty) > 0: + partial_picked_item_count = partial_picked_item_count + 1 + if fully_picked_item_count == total_item_count: + for x in self.get("order_details"): + if x.sales_order == so: + x.pick_status = "Fully Picked" + elif fully_picked_item_count == 0 and partial_picked_item_count == 0: + for x in self.get("order_details"): + if x.sales_order == so: + x.pick_status = "" + elif int(partial_picked_item_count) > 0: + for x in self.get("order_details"): + if x.sales_order == so: + x.pick_status = "Partially Picked"