From a3e69cf75d27198132d05c7c10475a0297b1e190 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com> Date: Tue, 8 Feb 2022 01:00:37 +0530 Subject: [PATCH] feat: Bulk Transaction Processing (#28580) * feat: Bulk Transaction Processing * fix: add flags to ignore validations and exception handling correction * fix: remove duplicate code, added logger functionality and improved notifications * fix: linting and sider issues * test: added tests * fix: linter issues * fix: failing test case * fix: sider issues and test cases * refactor: mapping function calls to create order/invoice * fix: added more test cases to increase coverage * fix: test cases * fix: sider issue * fix: rename doctype, improve formatting and minor refactor * fix: update doctype name in hooks and sider issues * fix: entry log test case * fix: typos, translations and company name in tests * fix: linter issues and translations * fix: linter issue * fix: split into separate function for marking failed transaction * fix: typos, retry failed transaction logic and make log read only * fix: hide retry button when no failed transactions and remove test cases not rrelevant * fix: sider issues and indentation to tabs Co-authored-by: Ankush Menat --- .../test_bulk_transaction_processing.js | 44 ++++ .../purchase_invoice/purchase_invoice_list.js | 10 + .../sales_invoice/sales_invoice_list.js | 12 +- erpnext/bulk_transaction/__init__.py | 0 erpnext/bulk_transaction/doctype/__init__.py | 0 .../doctype/bulk_transaction_log/__init__.py | 0 .../bulk_transaction_log.js | 34 +++ .../bulk_transaction_log.json | 51 +++++ .../bulk_transaction_log.py | 66 ++++++ .../test_bulk_transaction_log.py | 81 +++++++ .../bulk_transaction_log_detail/__init__.py | 0 .../bulk_transaction_log_detail.json | 86 ++++++++ .../bulk_transaction_log_detail.py | 9 + .../purchase_order/purchase_order_list.js | 16 +- .../supplier_quotation/supplier_quotation.py | 20 ++ .../supplier_quotation_list.js | 10 + erpnext/hooks.py | 3 +- erpnext/modules.txt | 1 + erpnext/public/build.json | 3 +- .../public/js/bulk_transaction_processing.js | 30 +++ erpnext/public/js/erpnext.bundle.js | 1 + .../doctype/quotation/quotation_list.js | 8 + .../doctype/sales_order/sales_order_list.js | 14 +- .../doctype/delivery_note/delivery_note.py | 11 + .../delivery_note/delivery_note_list.js | 14 +- .../purchase_receipt/purchase_receipt_list.js | 8 + .../ui_test_bulk_transaction_processing.py | 21 ++ erpnext/utilities/bulk_transaction.py | 201 ++++++++++++++++++ 28 files changed, 747 insertions(+), 7 deletions(-) create mode 100644 cypress/integration/test_bulk_transaction_processing.js create mode 100644 erpnext/bulk_transaction/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json create mode 100644 erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py create mode 100644 erpnext/public/js/bulk_transaction_processing.js create mode 100644 erpnext/tests/ui_test_bulk_transaction_processing.py create mode 100644 erpnext/utilities/bulk_transaction.py diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js new file mode 100644 index 00000000000..428ec5100b5 --- /dev/null +++ b/cypress/integration/test_bulk_transaction_processing.js @@ -0,0 +1,44 @@ +describe("Bulk Transaction Processing", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + it("Creates To Sales Order", () => { + cy.visit("/app/sales-order"); + cy.url().should("include", "/sales-order"); + cy.window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records", + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + timeout: 60000, + }) + .then((res) => { + expect(res.status).eq(200); + }); + }); + cy.wait(5000); + cy.get( + ".list-row-head > .list-header-subject > .list-row-col > .list-check-all" + ).check({ force: true }); + cy.wait(3000); + cy.get(".actions-btn-group > .btn-primary").click({ force: true }); + cy.wait(3000); + cy.get(".dropdown-menu-right > .user-action > .dropdown-item") + .contains("Sales Invoice") + .click({ force: true }); + cy.wait(3000); + cy.get(".modal-content > .modal-footer > .standard-actions") + .contains("Yes") + .click({ force: true }); + cy.contains("Creation of Sales Invoice successful"); + }); +}); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index f6ff83add8c..82d00308db4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js @@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = { ]; } }, + + onload: function(listview) { + listview.page.add_action_item(__("Purchase Receipt"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt"); + }); + + listview.page.add_action_item(__("Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment"); + }); + } }; diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 06e6f511839..1130284ecc5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = { }; return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status]; }, - right_column: "grand_total" + right_column: "grand_total", + + onload: function(listview) { + listview.page.add_action_item(__("Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note"); + }); + + listview.page.add_action_item(__("Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment"); + }); + } }; diff --git a/erpnext/bulk_transaction/__init__.py b/erpnext/bulk_transaction/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/bulk_transaction/doctype/__init__.py b/erpnext/bulk_transaction/doctype/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js new file mode 100644 index 00000000000..a739cc37306 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.js @@ -0,0 +1,34 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Bulk Transaction Log', { + + before_load: function(frm) { + query(frm); + }, + + refresh: function(frm) { + frm.disable_save(); + frm.add_custom_button(__('Retry Failed Transactions'), ()=>{ + frappe.confirm(__("Retry Failing Transactions ?"), ()=>{ + query(frm); + } + ); + }); + } +}); + +function query(frm) { + frappe.call({ + method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", + args: { + log_date: frm.doc.log_date + } + }).then((r) => { + if (r.message) { + frm.remove_custom_button("Retry Failed Transactions"); + } else { + frappe.show_alert(__("Retrying Failed Transactions"), 5); + } + }); +} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json new file mode 100644 index 00000000000..da42cf1bd4b --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-30 13:41:16.343827", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "log_date", + "logger_data" + ], + "fields": [ + { + "fieldname": "log_date", + "fieldtype": "Date", + "label": "Log Date", + "read_only": 1 + }, + { + "fieldname": "logger_data", + "fieldtype": "Table", + "label": "Logger Data", + "options": "Bulk Transaction Log Detail" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-02-03 17:23:02.935325", + "modified_by": "Administrator", + "module": "Bulk Transaction", + "name": "Bulk Transaction Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py new file mode 100644 index 00000000000..de7cde5a6d3 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/bulk_transaction_log.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from datetime import date + +import frappe +from frappe.model.document import Document + +from erpnext.utilities.bulk_transaction import task, update_logger + + +class BulkTransactionLog(Document): + pass + + +@frappe.whitelist() +def retry_failing_transaction(log_date=None): + btp = frappe.qb.DocType("Bulk Transaction Log Detail") + data = ( + frappe.qb.from_(btp) + .select(btp.transaction_name, btp.from_doctype, btp.to_doctype) + .distinct() + .where(btp.retried != 1) + .where(btp.transaction_status == "Failed") + .where(btp.date == log_date) + ).run(as_dict=True) + + if data: + if not log_date: + log_date = str(date.today()) + if len(data) > 10: + frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date) + else: + job(data, log_date) + else: + return "No Failed Records" + +def job(data, log_date): + for d in data: + failed = [] + try: + frappe.db.savepoint("before_creation_of_record") + task(d.transaction_name, d.from_doctype, d.to_doctype) + except Exception as e: + frappe.db.rollback(save_point="before_creation_of_record") + failed.append(e) + update_logger( + d.transaction_name, + e, + d.from_doctype, + d.to_doctype, + status="Failed", + log_date=log_date, + restarted=1 + ) + + if not failed: + update_logger( + d.transaction_name, + None, + d.from_doctype, + d.to_doctype, + status="Success", + log_date=log_date, + restarted=1, + ) diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py new file mode 100644 index 00000000000..a78e697b6f9 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/test_bulk_transaction_log.py @@ -0,0 +1,81 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest +from datetime import date + +import frappe + +from erpnext.utilities.bulk_transaction import transaction_processing + + +class TestBulkTransactionLog(unittest.TestCase): + + def setUp(self): + create_company() + create_customer() + create_item() + + def test_for_single_record(self): + so_name = create_so() + transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") + data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"]) + if not data: + self.fail("No Sales Invoice Created !") + + def test_entry_in_log(self): + so_name = create_so() + transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice") + doc = frappe.get_doc("Bulk Transaction Log", str(date.today())) + for d in doc.get("logger_data"): + if d.transaction_name == so_name: + self.assertEqual(d.transaction_name, so_name) + self.assertEqual(d.transaction_status, "Success") + self.assertEqual(d.from_doctype, "Sales Order") + self.assertEqual(d.to_doctype, "Sales Invoice") + self.assertEqual(d.retried, 0) + + + +def create_company(): + if not frappe.db.exists('Company', '_Test Company'): + frappe.get_doc({ + 'doctype': 'Company', + 'company_name': '_Test Company', + 'country': 'India', + 'default_currency': 'INR' + }).insert() + +def create_customer(): + if not frappe.db.exists('Customer', 'Bulk Customer'): + frappe.get_doc({ + 'doctype': 'Customer', + 'customer_name': 'Bulk Customer' + }).insert() + +def create_item(): + if not frappe.db.exists("Item", "MK"): + frappe.get_doc({ + "doctype": "Item", + "item_code": "MK", + "item_name": "Milk", + "description": "Milk", + "item_group": "Products" + }).insert() + +def create_so(intent=None): + so = frappe.new_doc("Sales Order") + so.customer = "Bulk Customer" + so.company = "_Test Company" + so.transaction_date = date.today() + + so.set_warehouse = "Finished Goods - _TC" + so.append("items", { + "item_code": "MK", + "delivery_date": date.today(), + "qty": 10, + "rate": 80, + }) + so.insert() + so.submit() + return so.name \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json new file mode 100644 index 00000000000..8262caa0209 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.json @@ -0,0 +1,86 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-11-30 13:38:30.926047", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "transaction_name", + "date", + "time", + "transaction_status", + "error_description", + "from_doctype", + "to_doctype", + "retried" + ], + "fields": [ + { + "fieldname": "transaction_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Name", + "options": "from_doctype" + }, + { + "fieldname": "transaction_status", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Status", + "read_only": 1 + }, + { + "fieldname": "error_description", + "fieldtype": "Long Text", + "label": "Error Description", + "read_only": 1 + }, + { + "fieldname": "from_doctype", + "fieldtype": "Link", + "label": "From Doctype", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "to_doctype", + "fieldtype": "Link", + "label": "To Doctype", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "read_only": 1 + }, + { + "fieldname": "time", + "fieldtype": "Time", + "label": "Time", + "read_only": 1 + }, + { + "fieldname": "retried", + "fieldtype": "Int", + "label": "Retried", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-02-03 19:57:31.650359", + "modified_by": "Administrator", + "module": "Bulk Transaction", + "name": "Bulk Transaction Log Detail", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py new file mode 100644 index 00000000000..67795b9d490 --- /dev/null +++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/bulk_transaction_log_detail.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BulkTransactionLogDetail(Document): + pass diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index 8413eb65c3f..d7907e4274b 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = { listview.call_for_selected_items(method, { "status": "Closed" }); }); - listview.page.add_menu_item(__("Re-open"), function () { + listview.page.add_menu_item(__("Reopen"), function () { listview.call_for_selected_items(method, { "status": "Submitted" }); }); + + + listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice"); + }); + + listview.page.add_action_item(__("Purchase Receipt"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt"); + }); + + listview.page.add_action_item(__("Advance Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment"); + }); + } }; diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index d65ab94a6d3..171de7882dc 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_purchase_invoice(source_name, target_doc=None): + doc = get_mapped_doc("Supplier Quotation", source_name, { + "Supplier Quotation": { + "doctype": "Purchase Invoice", + "validation": { + "docstatus": ["=", 1], + } + }, + "Supplier Quotation Item": { + "doctype": "Purchase Invoice Item" + }, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges" + } + }, target_doc) + + return doc + + @frappe.whitelist() def make_quotation(source_name, target_doc=None): doclist = get_mapped_doc("Supplier Quotation", source_name, { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js index 5ab6c980d00..73685caa0b4 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js @@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = { } else if(doc.status==="Expired") { return [__("Expired"), "gray", "status,=,Expired"]; } + }, + + onload: function(listview) { + listview.page.add_action_item(__("Purchase Order"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order"); + }); + + listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice"); + }); } }; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 0e290384b4c..d99f23ed64e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -341,7 +341,8 @@ scheduler_events = { "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" ], "hourly_long": [ - "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" + "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", + "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/modules.txt b/erpnext/modules.txt index c5705c17636..8c79ee5c9a8 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -21,4 +21,5 @@ Communication Loan Management Payroll Telephony +Bulk Transaction E-commerce diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 569910dd9df..91a752c291d 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -39,7 +39,8 @@ "public/js/utils/dimension_tree_filter.js", "public/js/telephony.js", "public/js/templates/call_link.html", - "public/js/templates/node_card.html" + "public/js/templates/node_card.html", + "public/js/bulk_transaction_processing.js" ], "js/item-dashboard.min.js": [ "stock/dashboard/item_dashboard.html", diff --git a/erpnext/public/js/bulk_transaction_processing.js b/erpnext/public/js/bulk_transaction_processing.js new file mode 100644 index 00000000000..101f50c64aa --- /dev/null +++ b/erpnext/public/js/bulk_transaction_processing.js @@ -0,0 +1,30 @@ +frappe.provide("erpnext.bulk_transaction_processing"); + +$.extend(erpnext.bulk_transaction_processing, { + create: function(listview, from_doctype, to_doctype) { + let checked_items = listview.get_checked_items(); + const doc_name = []; + checked_items.forEach((Item)=> { + if (Item.docstatus == 0) { + doc_name.push(Item.name); + } + }); + + let count_of_rows = checked_items.length; + frappe.confirm(__("Create {0} {1} ?", [count_of_rows, to_doctype]), ()=>{ + if (doc_name.length == 0) { + frappe.call({ + method: "erpnext.utilities.bulk_transaction.transaction_processing", + args: {data: checked_items, from_doctype: from_doctype, to_doctype: to_doctype} + }).then(()=> { + + }); + if (count_of_rows > 10) { + frappe.show_alert("Starting a background job to create {0} {1}", [count_of_rows, to_doctype]); + } + } else { + frappe.msgprint(__("Selected document must be in submitted state")); + } + }); + } +}); \ No newline at end of file diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 5259bdcc765..b3a68b38629 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -22,5 +22,6 @@ import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; import "./telephony"; import "./templates/call_link.html"; +import "./bulk_transaction_processing"; // import { sum } from 'frappe/public/utils/util.js' diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index b631685bd19..4c8f9c4f84c 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -12,6 +12,14 @@ frappe.listview_settings['Quotation'] = { }; }; } + + listview.page.add_action_item(__("Sales Order"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Order"); + }); + + listview.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Quotation", "Sales Invoice"); + }); }, get_indicator: function(doc) { diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 26d96d59f29..4691190d2a5 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -16,7 +16,7 @@ frappe.listview_settings['Sales Order'] = { return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; } else if (flt(doc.grand_total) === 0) { - // not delivered (zero-amount order) + // not delivered (zeroount order) return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; } else if (flt(doc.per_billed, 6) < 100) { @@ -48,5 +48,17 @@ frappe.listview_settings['Sales Order'] = { listview.call_for_selected_items(method, {"status": "Submitted"}); }); + listview.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Sales Invoice"); + }); + + listview.page.add_action_item(__("Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Delivery Note"); + }); + + listview.page.add_action_item(__("Advance Payment"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment"); + }); + } }; diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c3247fbe3e8..2a4d63954a7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -608,7 +608,18 @@ def make_packing_slip(source_name, target_doc=None): "validation": { "docstatus": ["=", 0] } + }, + + "Delivery Note Item": { + "doctype": "Packing Slip Item", + "field_map": { + "item_code": "item_code", + "item_name": "item_name", + "description": "description", + "qty": "qty", + } } + }, target_doc) return doclist diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js index 04028980473..9e6f3bc9321 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js @@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = { return [__("Completed"), "green", "per_billed,=,100"]; } }, - onload: function (doclist) { + onload: function (listview) { const action = () => { const selected_docs = doclist.get_checked_items(); const docnames = doclist.get_checked_items(true); @@ -54,6 +54,16 @@ frappe.listview_settings['Delivery Note'] = { }; }; - doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); + // doclist.page.add_actions_menu_item(__('Create Delivery Trip'), action, false); + + listview.page.add_action_item(__('Create Delivery Trip'), action); + + listview.page.add_action_item(__("Sales Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Sales Invoice"); + }); + + listview.page.add_action_item(__("Packaging Slip From Delivery Note"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Delivery Note", "Packing Slip"); + }); } }; diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index 77711de93f7..4029f0c127b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -13,5 +13,13 @@ frappe.listview_settings['Purchase Receipt'] = { } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } + }, + + onload: function(listview) { + + listview.page.add_action_item(__("Purchase Invoice"), ()=>{ + erpnext.bulk_transaction_processing.create(listview, "Purchase Receipt", "Purchase Invoice"); + }); } + }; diff --git a/erpnext/tests/ui_test_bulk_transaction_processing.py b/erpnext/tests/ui_test_bulk_transaction_processing.py new file mode 100644 index 00000000000..d78689eb5b3 --- /dev/null +++ b/erpnext/tests/ui_test_bulk_transaction_processing.py @@ -0,0 +1,21 @@ +import frappe + +from erpnext.bulk_transaction.doctype.bulk_transaction_logger.test_bulk_transaction_logger import ( + create_company, + create_customer, + create_item, + create_so, +) + + +@frappe.whitelist() +def create_records(): + create_company() + create_customer() + create_item() + + gd = frappe.get_doc("Global Defaults") + gd.set("default_company", "Test Bulk") + gd.save() + frappe.clear_cache() + create_so() \ No newline at end of file diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py new file mode 100644 index 00000000000..64e2ff42184 --- /dev/null +++ b/erpnext/utilities/bulk_transaction.py @@ -0,0 +1,201 @@ +import json +from datetime import date, datetime + +import frappe +from frappe import _ + + +@frappe.whitelist() +def transaction_processing(data, from_doctype, to_doctype): + if isinstance(data, str): + deserialized_data = json.loads(data) + + else: + deserialized_data = data + + length_of_data = len(deserialized_data) + + if length_of_data > 10: + frappe.msgprint( + _("Started a background job to create {1} {0}").format(to_doctype, length_of_data) + ) + frappe.enqueue( + job, + deserialized_data=deserialized_data, + from_doctype=from_doctype, + to_doctype=to_doctype, + ) + else: + job(deserialized_data, from_doctype, to_doctype) + + +def job(deserialized_data, from_doctype, to_doctype): + failed_history = [] + i = 0 + for d in deserialized_data: + failed = [] + + try: + i += 1 + doc_name = d.get("name") + frappe.db.savepoint("before_creation_state") + task(doc_name, from_doctype, to_doctype) + + except Exception as e: + frappe.db.rollback(save_point="before_creation_state") + failed_history.append(e) + failed.append(e) + update_logger(doc_name, e, from_doctype, to_doctype, status="Failed", log_date=str(date.today())) + if not failed: + update_logger(doc_name, None, from_doctype, to_doctype, status="Success", log_date=str(date.today())) + + show_job_status(failed_history, deserialized_data, to_doctype) + + +def task(doc_name, from_doctype, to_doctype): + from erpnext.accounts.doctype.payment_entry import payment_entry + from erpnext.accounts.doctype.purchase_invoice import purchase_invoice + from erpnext.accounts.doctype.sales_invoice import sales_invoice + from erpnext.buying.doctype.purchase_order import purchase_order + from erpnext.buying.doctype.supplier_quotation import supplier_quotation + from erpnext.selling.doctype.quotation import quotation + from erpnext.selling.doctype.sales_order import sales_order + from erpnext.stock.doctype.delivery_note import delivery_note + from erpnext.stock.doctype.purchase_receipt import purchase_receipt + + mapper = { + "Sales Order": { + "Sales Invoice": sales_order.make_sales_invoice, + "Delivery Note": sales_order.make_delivery_note, + "Advance Payment": payment_entry.get_payment_entry, + }, + "Sales Invoice": { + "Delivery Note": sales_invoice.make_delivery_note, + "Payment": payment_entry.get_payment_entry, + }, + "Delivery Note": { + "Sales Invoice": delivery_note.make_sales_invoice, + "Packing Slip": delivery_note.make_packing_slip, + }, + "Quotation": { + "Sales Order": quotation.make_sales_order, + "Sales Invoice": quotation.make_sales_invoice, + }, + "Supplier Quotation": { + "Purchase Order": supplier_quotation.make_purchase_order, + "Purchase Invoice": supplier_quotation.make_purchase_invoice, + "Advance Payment": payment_entry.get_payment_entry, + }, + "Purchase Order": { + "Purchase Invoice": purchase_order.make_purchase_invoice, + "Purchase Receipt": purchase_order.make_purchase_receipt, + }, + "Purhcase Invoice": { + "Purchase Receipt": purchase_invoice.make_purchase_receipt, + "Payment": payment_entry.get_payment_entry, + }, + "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, + } + if to_doctype in ['Advance Payment', 'Payment']: + obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) + else: + obj = mapper[from_doctype][to_doctype](doc_name) + + obj.flags.ignore_validate = True + obj.insert(ignore_mandatory=True) + + +def check_logger_doc_exists(log_date): + return frappe.db.exists("Bulk Transaction Log", log_date) + + +def get_logger_doc(log_date): + return frappe.get_doc("Bulk Transaction Log", log_date) + + +def create_logger_doc(): + log_doc = frappe.new_doc("Bulk Transaction Log") + log_doc.set_new_name(set_name=str(date.today())) + log_doc.log_date = date.today() + + return log_doc + + +def append_data_to_logger(log_doc, doc_name, error, from_doctype, to_doctype, status, restarted): + row = log_doc.append("logger_data", {}) + row.transaction_name = doc_name + row.date = date.today() + now = datetime.now() + row.time = now.strftime("%H:%M:%S") + row.transaction_status = status + row.error_description = str(error) + row.from_doctype = from_doctype + row.to_doctype = to_doctype + row.retried = restarted + + +def update_logger(doc_name, e, from_doctype, to_doctype, status, log_date=None, restarted=0): + if not check_logger_doc_exists(log_date): + log_doc = create_logger_doc() + append_data_to_logger(log_doc, doc_name, e, from_doctype, to_doctype, status, restarted) + log_doc.insert() + else: + log_doc = get_logger_doc(log_date) + if record_exists(log_doc, doc_name, status): + append_data_to_logger( + log_doc, doc_name, e, from_doctype, to_doctype, status, restarted + ) + log_doc.save() + + +def show_job_status(failed_history, deserialized_data, to_doctype): + if not failed_history: + frappe.msgprint( + _("Creation of {0} successful").format(to_doctype), + title="Successful", + indicator="green", + ) + + if len(failed_history) != 0 and len(failed_history) < len(deserialized_data): + frappe.msgprint( + _("""Creation of {0} partially successful. + Check Bulk Transaction Log""").format( + to_doctype + ), + title="Partially successful", + indicator="orange", + ) + + if len(failed_history) == len(deserialized_data): + frappe.msgprint( + _("""Creation of {0} failed. + Check Bulk Transaction Log""").format( + to_doctype + ), + title="Failed", + indicator="red", + ) + + +def record_exists(log_doc, doc_name, status): + + record = mark_retrired_transaction(log_doc, doc_name) + + if record and status == "Failed": + return False + elif record and status == "Success": + return True + else: + return True + + +def mark_retrired_transaction(log_doc, doc_name): + record = 0 + for d in log_doc.get("logger_data"): + if d.transaction_name == doc_name and d.transaction_status == "Failed": + d.retried = 1 + record = record + 1 + + log_doc.save() + + return record \ No newline at end of file