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>
This commit is contained in:
parent
2280ae5554
commit
a3e69cf75d
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
@ -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");
|
||||||
|
});
|
||||||
|
});
|
@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = {
|
|||||||
};
|
};
|
||||||
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
0
erpnext/bulk_transaction/__init__.py
Normal file
0
erpnext/bulk_transaction/__init__.py
Normal file
0
erpnext/bulk_transaction/doctype/__init__.py
Normal file
0
erpnext/bulk_transaction/doctype/__init__.py
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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,
|
||||||
|
)
|
@ -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
|
@ -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
|
||||||
|
}
|
@ -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
|
@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = {
|
|||||||
listview.call_for_selected_items(method, { "status": "Closed" });
|
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.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");
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None):
|
|||||||
|
|
||||||
return doclist
|
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()
|
@frappe.whitelist()
|
||||||
def make_quotation(source_name, target_doc=None):
|
def make_quotation(source_name, target_doc=None):
|
||||||
doclist = get_mapped_doc("Supplier Quotation", source_name, {
|
doclist = get_mapped_doc("Supplier Quotation", source_name, {
|
||||||
|
@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = {
|
|||||||
} else if(doc.status==="Expired") {
|
} else if(doc.status==="Expired") {
|
||||||
return [__("Expired"), "gray", "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");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -341,7 +341,8 @@ scheduler_events = {
|
|||||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
|
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
|
||||||
],
|
],
|
||||||
"hourly_long": [
|
"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": [
|
"daily": [
|
||||||
"erpnext.stock.reorder_item.reorder_item",
|
"erpnext.stock.reorder_item.reorder_item",
|
||||||
|
@ -21,4 +21,5 @@ Communication
|
|||||||
Loan Management
|
Loan Management
|
||||||
Payroll
|
Payroll
|
||||||
Telephony
|
Telephony
|
||||||
|
Bulk Transaction
|
||||||
E-commerce
|
E-commerce
|
||||||
|
@ -39,7 +39,8 @@
|
|||||||
"public/js/utils/dimension_tree_filter.js",
|
"public/js/utils/dimension_tree_filter.js",
|
||||||
"public/js/telephony.js",
|
"public/js/telephony.js",
|
||||||
"public/js/templates/call_link.html",
|
"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": [
|
"js/item-dashboard.min.js": [
|
||||||
"stock/dashboard/item_dashboard.html",
|
"stock/dashboard/item_dashboard.html",
|
||||||
|
30
erpnext/public/js/bulk_transaction_processing.js
Normal file
30
erpnext/public/js/bulk_transaction_processing.js
Normal file
@ -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"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -22,5 +22,6 @@ import "./call_popup/call_popup";
|
|||||||
import "./utils/dimension_tree_filter";
|
import "./utils/dimension_tree_filter";
|
||||||
import "./telephony";
|
import "./telephony";
|
||||||
import "./templates/call_link.html";
|
import "./templates/call_link.html";
|
||||||
|
import "./bulk_transaction_processing";
|
||||||
|
|
||||||
// import { sum } from 'frappe/public/utils/util.js'
|
// import { sum } from 'frappe/public/utils/util.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) {
|
get_indicator: function(doc) {
|
||||||
|
@ -16,7 +16,7 @@ frappe.listview_settings['Sales Order'] = {
|
|||||||
return [__("Overdue"), "red",
|
return [__("Overdue"), "red",
|
||||||
"per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
|
"per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"];
|
||||||
} else if (flt(doc.grand_total) === 0) {
|
} else if (flt(doc.grand_total) === 0) {
|
||||||
// not delivered (zero-amount order)
|
// not delivered (zeroount order)
|
||||||
return [__("To Deliver"), "orange",
|
return [__("To Deliver"), "orange",
|
||||||
"per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
"per_delivered,<,100|grand_total,=,0|status,!=,Closed"];
|
||||||
} else if (flt(doc.per_billed, 6) < 100) {
|
} 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.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");
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -608,7 +608,18 @@ def make_packing_slip(source_name, target_doc=None):
|
|||||||
"validation": {
|
"validation": {
|
||||||
"docstatus": ["=", 0]
|
"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)
|
}, target_doc)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
@ -14,7 +14,7 @@ frappe.listview_settings['Delivery Note'] = {
|
|||||||
return [__("Completed"), "green", "per_billed,=,100"];
|
return [__("Completed"), "green", "per_billed,=,100"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onload: function (doclist) {
|
onload: function (listview) {
|
||||||
const action = () => {
|
const action = () => {
|
||||||
const selected_docs = doclist.get_checked_items();
|
const selected_docs = doclist.get_checked_items();
|
||||||
const docnames = doclist.get_checked_items(true);
|
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");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -13,5 +13,13 @@ frappe.listview_settings['Purchase Receipt'] = {
|
|||||||
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
||||||
return [__("Completed"), "green", "per_billed,=,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");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
21
erpnext/tests/ui_test_bulk_transaction_processing.py
Normal file
21
erpnext/tests/ui_test_bulk_transaction_processing.py
Normal file
@ -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()
|
201
erpnext/utilities/bulk_transaction.py
Normal file
201
erpnext/utilities/bulk_transaction.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user