diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js new file mode 100644 index 0000000000..428ec5100b --- /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.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 76d9cc7b49..2c3156175f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -178,8 +178,8 @@ class PurchaseInvoice(BuyingController): if self.supplier and account.account_type != "Payable": frappe.throw( - _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account") + _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.") + .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account") ) self.party_account_currency = account.account_currency diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js index f6ff83add8..82d00308db 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.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index bc443581e4..754ca81424 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -572,7 +572,10 @@ class SalesInvoice(SellingController): frappe.throw(msg, title=_("Invalid Account")) if self.customer and account.account_type != "Receivable": - msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " " + msg = _("Please ensure {} account {} is a Receivable account.").format( + frappe.bold("Debit To"), + frappe.bold(self.debit_to) + ) + " " msg += _("Change the account type to Receivable or select a different account.") frappe.throw(msg, title=_("Invalid Account")) @@ -1249,14 +1252,14 @@ class SalesInvoice(SellingController): def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): - if d.dn_detail: + if d.so_detail: + updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) + elif d.dn_detail: billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` where dn_detail=%s and docstatus=1""", d.dn_detail) billed_amt = billed_amt and billed_amt[0][0] or 0 frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) updated_delivery_notes.append(d.delivery_note) - elif d.so_detail: - updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index 06e6f51183..1130284ecc 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 0000000000..e69de29bb2 diff --git a/erpnext/bulk_transaction/doctype/__init__.py b/erpnext/bulk_transaction/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..e69de29bb2 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 0000000000..a739cc3730 --- /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 0000000000..da42cf1bd4 --- /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 0000000000..de7cde5a6d --- /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 0000000000..a78e697b6f --- /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 0000000000..e69de29bb2 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 0000000000..8262caa020 --- /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 0000000000..67795b9d49 --- /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 8413eb65c3..d7907e4274 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 d65ab94a6d..171de7882d 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 5ab6c980d0..73685caa0b 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/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8d17683953..c8e5eddfea 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -3,6 +3,7 @@ import json from collections import defaultdict +from typing import List, Tuple import frappe from frappe import _ @@ -181,33 +182,28 @@ class StockController(AccountsController): return details - def get_items_and_warehouses(self): - items, warehouses = [], [] + def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]: + """Get list of items and warehouses affected by a transaction""" - if hasattr(self, "items"): - item_doclist = self.get("items") - elif self.doctype == "Stock Reconciliation": - item_doclist = [] - data = json.loads(self.reconciliation_json) - for row in data[data.index(self.head_row)+1:]: - d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row)) - item_doclist.append(d) + if not (hasattr(self, "items") or hasattr(self, "packed_items")): + return [], [] - if item_doclist: - for d in item_doclist: - if d.item_code and d.item_code not in items: - items.append(d.item_code) + item_rows = (self.get("items") or []) + (self.get("packed_items") or []) - if d.get("warehouse") and d.warehouse not in warehouses: - warehouses.append(d.warehouse) + items = {d.item_code for d in item_rows if d.item_code} - if self.doctype == "Stock Entry": - if d.get("s_warehouse") and d.s_warehouse not in warehouses: - warehouses.append(d.s_warehouse) - if d.get("t_warehouse") and d.t_warehouse not in warehouses: - warehouses.append(d.t_warehouse) + warehouses = set() + for d in item_rows: + if d.get("warehouse"): + warehouses.add(d.warehouse) - return items, warehouses + if self.doctype == "Stock Entry": + if d.get("s_warehouse"): + warehouses.add(d.s_warehouse) + if d.get("t_warehouse"): + warehouses.add(d.t_warehouse) + + return list(items), list(warehouses) def get_stock_ledger_details(self): stock_ledger = {} @@ -219,7 +215,7 @@ class StockController(AccountsController): from `tabStock Ledger Entry` where - voucher_type=%s and voucher_no=%s + voucher_type=%s and voucher_no=%s and is_cancelled = 0 """, (self.doctype, self.name), as_dict=True) for sle in stock_ledger_entries: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 0e290384b4..d99f23ed64 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 c5705c1763..8c79ee5c9a 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 569910dd9d..91a752c291 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 0000000000..101f50c64a --- /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/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3791741663..ab3e802051 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2288,7 +2288,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => this.frm.doc.ignore_pricing_rule=1, () => me.ignore_pricing_rule(), () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() + () => me.apply_pricing_rule(), + () => this.frm.save() ]); } else { frappe.run_serially([ diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 5259bdcc76..b3a68b3862 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 b631685bd1..4c8f9c4f84 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 26d96d59f2..4691190d2a 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 d1e22440b9..2a4d63954a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -339,17 +339,35 @@ class DeliveryNote(SellingController): frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) def update_billed_amount_based_on_so(so_detail, update_modified=True): + from frappe.query_builder.functions import Sum + # Billed against Sales Order directly - billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail) + si = frappe.qb.DocType("Sales Invoice").as_("si") + si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") + sum_amount = Sum(si_item.amount).as_("amount") + + billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where( + (si_item.parent == si.name) & + (si_item.so_detail == so_detail) & + ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & + (si_item.docstatus == 1) & + (si.update_stock == 0) + ).run() billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row - dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent - from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn - where dn.name=dn_item.parent and dn_item.so_detail=%s - and dn.docstatus=1 and dn.is_return = 0 - order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1) + + dn = frappe.qb.DocType("Delivery Note").as_("dn") + dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") + + dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where( + (dn.name == dn_item.parent) & + (dn_item.so_detail == so_detail) & + (dn.docstatus == 1) & + (dn.is_return == 0) + ).orderby( + dn.posting_date, dn.posting_time, dn.name + ).run(as_dict=True) updated_dn = [] for dnd in dn_details: @@ -367,7 +385,11 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): # Distribute billed amount directly against SO between DNs based on FIFO if billed_against_so and billed_amt_agianst_dn < dnd.amount: - pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn + if dnd.returned_qty: + pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty + else: + pending_to_bill = flt(dnd.amount) + pending_to_bill -= billed_amt_agianst_dn if pending_to_bill <= billed_against_so: billed_amt_agianst_dn += pending_to_bill billed_against_so -= pending_to_bill @@ -586,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 0402898047..9e6f3bc932 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/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 5cbaa1ea66..2521ac9fe7 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,10 +1,14 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from frappe.utils import add_to_date, nowdate + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.tests.utils import ERPNextTestCase, change_settings @@ -12,31 +16,30 @@ class TestPackedItem(ERPNextTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: - make_item("_Test Product Bundle X", {"is_stock_item": 0}) - make_item("_Test Bundle Item 1", {"is_stock_item": 1}) - make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + super().setUpClass() + cls.bundle = "_Test Product Bundle X" + cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] + make_item(cls.bundle, {"is_stock_item": 0}) + for item in cls.bundle_items: + make_item(item, {"is_stock_item": 1}) + make_item("_Test Normal Stock Item", {"is_stock_item": 1}) - make_product_bundle( - "_Test Product Bundle X", - ["_Test Bundle Item 1", "_Test Bundle Item 2"], - qty=2 - ) + make_product_bundle(cls.bundle, cls.bundle_items, qty=2) def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, + so = make_sales_order(item_code = self.bundle, qty=1, do_not_submit=True) self.assertEqual(so.items[0].qty, 1) self.assertEqual(len(so.packed_items), 2) - self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1") + self.assertEqual(so.packed_items[0].item_code, self.bundle_items[0]) self.assertEqual(so.packed_items[0].qty, 2) def test_updating_bundle_item(self): "Test impact on packed items if bundle item row is updated." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=1, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) so.items[0].qty = 2 # change qty so.save() @@ -55,7 +58,7 @@ class TestPackedItem(ERPNextTestCase): so_items = [] for qty in [2, 4, 6, 8]: so_items.append({ - "item_code": "_Test Product Bundle X", + "item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC" @@ -66,7 +69,7 @@ class TestPackedItem(ERPNextTestCase): # check alternate rows for qty self.assertEqual(len(so.packed_items), 8) - self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2") + self.assertEqual(so.packed_items[1].item_code, self.bundle_items[1]) self.assertEqual(so.packed_items[1].qty, 4) self.assertEqual(so.packed_items[3].qty, 8) self.assertEqual(so.packed_items[5].qty, 12) @@ -94,8 +97,7 @@ class TestPackedItem(ERPNextTestCase): @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) def test_bundle_item_cumulative_price(self): "Test if Bundle Item rate is cumulative from packed items." - so = make_sales_order(item_code = "_Test Product Bundle X", qty=2, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=2, do_not_submit=True) so.packed_items[0].rate = 150 so.packed_items[1].rate = 200 @@ -109,7 +111,7 @@ class TestPackedItem(ERPNextTestCase): so_items = [] for qty in [2, 4]: so_items.append({ - "item_code": "_Test Product Bundle X", + "item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC" @@ -124,4 +126,33 @@ class TestPackedItem(ERPNextTestCase): self.assertEqual(len(dn.packed_items), 4) self.assertEqual(dn.packed_items[2].qty, 6) - self.assertEqual(dn.packed_items[3].qty, 6) \ No newline at end of file + self.assertEqual(dn.packed_items[3].qty, 6) + + def test_reposting_packed_items(self): + warehouse = "Stores - TCP1" + company = "_Test Company with perpetual inventory" + + today = nowdate() + yesterday = add_to_date(today, days=-1, as_string=True) + + for item in self.bundle_items: + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today) + + so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse) + + dn = make_delivery_note(so.name) + dn.save() + dn.submit() + + gles = get_gl_entries(dn.doctype, dn.name) + credit_before_repost = sum(gle.credit for gle in gles) + + # backdated stock entry + for item in self.bundle_items: + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) + + # assert correct reposting + gles = get_gl_entries(dn.doctype, dn.name) + credit_after_reposting = sum(gle.credit for gle in gles) + self.assertNotEqual(credit_before_repost, credit_after_reposting) + self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index ffdf8c420c..33e40c85f1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -288,9 +288,6 @@ class PurchaseReceipt(BuyingController): {"voucher_type": "Purchase Receipt", "voucher_no": self.name, "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") - if not stock_value_diff: - continue - warehouse_account_name = warehouse_account[d.warehouse]["account"] warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index 77711de93f..4029f0c127 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/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b87d9205e0..5ab7929a2a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4,6 +4,7 @@ import json import unittest +from collections import defaultdict import frappe from frappe.utils import add_days, cint, cstr, flt, today @@ -16,7 +17,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.tests.utils import ERPNextTestCase +from erpnext.tests.utils import ERPNextTestCase, change_settings class TestPurchaseReceipt(ERPNextTestCase): @@ -1387,6 +1388,36 @@ class TestPurchaseReceipt(ERPNextTestCase): automatically_fetch_payment_terms(enable=0) + @change_settings("Stock Settings", {"allow_negative_stock": 1}) + def test_neg_to_positive(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item_code = "_TestNegToPosItem" + warehouse = "Stores - TCP1" + company = "_Test Company with perpetual inventory" + account = "Stock Received But Not Billed - TCP1" + + make_item(item_code) + se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) + se.items[0].allow_zero_valuation_rate = 1 + se.save() + se.submit() + + pr = make_purchase_receipt( + qty=50, + rate=1, + item_code=item_code, + warehouse=warehouse, + get_taxes_and_charges=True, + company=company, + ) + gles = get_gl_entries(pr.doctype, pr.name) + + for gle in gles: + if gle.account == account: + self.assertEqual(gle.credit, 50) + + def get_sl_entries(voucher_type, voucher_no): return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 01c5e3e4e2..977d470995 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -13,7 +13,7 @@ from erpnext.accounts.utils import ( check_if_stock_and_account_balance_synced, update_gl_entries_after, ) -from erpnext.stock.stock_ledger import repost_future_sle +from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle class RepostItemValuation(Document): @@ -138,13 +138,20 @@ def repost_gl_entries(doc): if doc.based_on == 'Transaction': ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) - items, warehouses = ref_doc.get_items_and_warehouses() + doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() + + sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no) + sle_items = [sle.item_code for sle in sles] + sle_warehouse = [sle.warehouse for sle in sles] + + items = list(set(doc_items).union(set(sle_items))) + warehouses = list(set(doc_warehouses).union(set(sle_warehouse))) else: items = [doc.item_code] warehouses = [doc.warehouse] update_gl_entries_after(doc.posting_date, doc.posting_time, - warehouses, items, company=doc.company) + for_warehouses=warehouses, for_items=items, company=doc.company) def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json index 3402972bb8..a882a61e5a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json @@ -18,7 +18,6 @@ "items", "section_break_9", "expense_account", - "reconciliation_json", "column_break_13", "difference_amount", "amended_from", @@ -111,15 +110,6 @@ "label": "Cost Center", "options": "Cost Center" }, - { - "fieldname": "reconciliation_json", - "fieldtype": "Long Text", - "hidden": 1, - "label": "Reconciliation JSON", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "fieldname": "column_break_13", "fieldtype": "Column Break" @@ -155,7 +145,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-30 01:33:51.437194", + "modified": "2022-02-06 14:28:19.043905", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", @@ -178,5 +168,6 @@ "search_fields": "posting_date", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 428370cc75..86af0a0cf3 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -25,8 +25,8 @@ from erpnext.tests.utils import ERPNextTestCase, change_settings class TestStockReconciliation(ERPNextTestCase): @classmethod def setUpClass(cls): - super().setUpClass() create_batch_or_serial_no_items() + super().setUpClass() frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) def tearDown(self): 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 0000000000..d78689eb5b --- /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 0000000000..64e2ff4218 --- /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