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 <ankush@frappe.io>
diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js
new file mode 100644
index 0000000..428ec51
--- /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 f6ff83a..82d0030 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 @@
];
}
},
+
+ 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 06e6f51..1130284 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 @@
};
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/__init__.py
diff --git a/erpnext/bulk_transaction/doctype/__init__.py b/erpnext/bulk_transaction/doctype/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/__init__.py
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log/__init__.py
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 0000000..a739cc3
--- /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 0000000..da42cf1
--- /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 0000000..de7cde5
--- /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 0000000..a78e697
--- /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 0000000..e69de29
--- /dev/null
+++ b/erpnext/bulk_transaction/doctype/bulk_transaction_log_detail/__init__.py
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 0000000..8262caa
--- /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 0000000..67795b9
--- /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 8413eb6..d7907e4 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 @@
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 d65ab94..171de78 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
@@ -143,6 +143,26 @@
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, {
"Supplier Quotation": {
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
index 5ab6c98..73685ca 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 @@
} 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 0e29038..d99f23e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -341,7 +341,8 @@
"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 c5705c1..8c79ee5 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -21,4 +21,5 @@
Loan Management
Payroll
Telephony
+Bulk Transaction
E-commerce
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 569910d..91a752c 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 0000000..101f50c
--- /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 5259bdc..b3a68b3 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -22,5 +22,6 @@
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 b631685..4c8f9c4 100644
--- a/erpnext/selling/doctype/quotation/quotation_list.js
+++ b/erpnext/selling/doctype/quotation/quotation_list.js
@@ -12,6 +12,14 @@
};
};
}
+
+ 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 26d96d5..4691190 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 @@
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 @@
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 c3247fb..2a4d639 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -608,7 +608,18 @@
"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 0402898..9e6f3bc 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 @@
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 @@
};
};
- 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 77711de..4029f0c 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 @@
} 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 0000000..d78689e
--- /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 0000000..64e2ff4
--- /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 <b><a href="/app/bulk-transaction-log">Bulk Transaction Log</a></b>""").format(
+ to_doctype
+ ),
+ title="Partially successful",
+ indicator="orange",
+ )
+
+ if len(failed_history) == len(deserialized_data):
+ frappe.msgprint(
+ _("""Creation of {0} failed.
+ Check <b><a href="/app/bulk-transaction-log">Bulk Transaction Log</a></b>""").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