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/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 55ea571ebf..9a35a247dd 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -7,35 +7,30 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "accounts_transactions_settings_section",
- "over_billing_allowance",
- "role_allowed_to_over_bill",
- "credit_controller",
- "make_payment_via_journal_entry",
- "column_break_11",
- "check_supplier_invoice_uniqueness",
+ "invoice_and_billing_tab",
+ "enable_features_section",
"unlink_payment_on_cancellation_of_invoice",
- "automatically_fetch_payment_terms",
- "delete_linked_ledger_entries",
- "book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
+ "column_break_13",
+ "delete_linked_ledger_entries",
+ "invoicing_features_section",
+ "check_supplier_invoice_uniqueness",
+ "automatically_fetch_payment_terms",
+ "column_break_17",
"enable_common_party_accounting",
- "post_change_gl_entries",
"enable_discount_accounting",
- "tax_settings_section",
- "determine_address_tax_category_from",
- "column_break_19",
- "add_taxes_from_item_tax_template",
- "period_closing_settings_section",
- "acc_frozen_upto",
- "frozen_accounts_modifier",
- "column_break_4",
+ "report_setting_section",
+ "use_custom_cash_flow",
"deferred_accounting_settings_section",
"book_deferred_entries_based_on",
"column_break_18",
"automatically_process_deferred_accounting_entry",
"book_deferred_entries_via_journal_entry",
"submit_journal_entries",
+ "tax_settings_section",
+ "determine_address_tax_category_from",
+ "column_break_19",
+ "add_taxes_from_item_tax_template",
"print_settings",
"show_inclusive_tax_in_print",
"column_break_12",
@@ -43,8 +38,25 @@
"currency_exchange_section",
"allow_stale",
"stale_days",
- "report_settings_sb",
- "use_custom_cash_flow"
+ "invoicing_settings_tab",
+ "accounts_transactions_settings_section",
+ "over_billing_allowance",
+ "column_break_11",
+ "role_allowed_to_over_bill",
+ "credit_controller",
+ "make_payment_via_journal_entry",
+ "pos_tab",
+ "pos_setting_section",
+ "post_change_gl_entries",
+ "assets_tab",
+ "asset_settings_section",
+ "book_asset_depreciation_entry_automatically",
+ "closing_settings_tab",
+ "period_closing_settings_section",
+ "acc_frozen_upto",
+ "column_break_25",
+ "frozen_accounts_modifier",
+ "report_settings_sb"
],
"fields": [
{
@@ -70,10 +82,6 @@
"label": "Determine Address Tax Category From",
"options": "Billing Address\nShipping Address"
},
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
{
"fieldname": "credit_controller",
"fieldtype": "Link",
@@ -83,6 +91,7 @@
},
{
"default": "0",
+ "description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
"fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness"
@@ -168,7 +177,7 @@
"description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow",
"fieldtype": "Check",
- "label": "Use Custom Cash Flow Format"
+ "label": "Enable Custom Cash Flow Format"
},
{
"default": "0",
@@ -241,7 +250,7 @@
{
"fieldname": "accounts_transactions_settings_section",
"fieldtype": "Section Break",
- "label": "Transactions Settings"
+ "label": "Credit Limit Settings"
},
{
"fieldname": "column_break_11",
@@ -272,9 +281,72 @@
},
{
"default": "0",
+ "description": "Learn about Common Party",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
+ },
+ {
+ "fieldname": "enable_features_section",
+ "fieldtype": "Section Break",
+ "label": "Invoice Cancellation"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_25",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "asset_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Asset Settings"
+ },
+ {
+ "fieldname": "invoicing_settings_tab",
+ "fieldtype": "Tab Break",
+ "label": "Credit Limits"
+ },
+ {
+ "fieldname": "assets_tab",
+ "fieldtype": "Tab Break",
+ "label": "Assets"
+ },
+ {
+ "fieldname": "closing_settings_tab",
+ "fieldtype": "Tab Break",
+ "label": "Accounts Closing"
+ },
+ {
+ "fieldname": "pos_setting_section",
+ "fieldtype": "Section Break",
+ "label": "POS Setting"
+ },
+ {
+ "fieldname": "invoice_and_billing_tab",
+ "fieldtype": "Tab Break",
+ "label": "Invoice and Billing"
+ },
+ {
+ "fieldname": "invoicing_features_section",
+ "fieldtype": "Section Break",
+ "label": "Invoicing Features"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "pos_tab",
+ "fieldtype": "Tab Break",
+ "label": "POS"
+ },
+ {
+ "fieldname": "report_setting_section",
+ "fieldtype": "Section Break",
+ "label": "Report Setting"
}
],
"icon": "icon-cog",
@@ -282,7 +354,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-10-11 17:42:36.427699",
+ "modified": "2022-02-04 12:32:36.805652",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -309,5 +381,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
index 77c9e95b75..b42d712d88 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
- "creation": "2018-11-22 22:45:00.370913",
+ "creation": "2022-01-19 01:09:13.297137",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
@@ -10,6 +10,9 @@
"field_order": [
"title",
"company",
+ "column_break_3",
+ "disabled",
+ "section_break_5",
"taxes"
],
"fields": [
@@ -36,10 +39,24 @@
"label": "Company",
"options": "Company",
"reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
}
],
"links": [],
- "modified": "2021-03-08 19:50:21.416513",
+ "modified": "2022-01-18 21:11:23.105589",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -82,6 +99,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
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/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json
index f7145af44c..44a339f31d 100644
--- a/erpnext/accounts/doctype/tax_category/tax_category.json
+++ b/erpnext/accounts/doctype/tax_category/tax_category.json
@@ -2,12 +2,13 @@
"actions": [],
"allow_rename": 1,
"autoname": "field:title",
- "creation": "2018-11-22 23:38:39.668804",
+ "creation": "2022-01-19 01:09:28.920486",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "title"
+ "title",
+ "disabled"
],
"fields": [
{
@@ -18,14 +19,21 @@
"label": "Title",
"reqd": 1,
"unique": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-03 11:50:38.748872",
+ "modified": "2022-01-18 21:13:41.161017",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -65,5 +73,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
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/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index b828a43d3c..50321baa2e 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -6,14 +6,17 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
+ "supplier_and_price_defaults_section",
"supp_master_name",
"supplier_group",
+ "column_break_4",
"buying_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
- "column_break_3",
+ "transaction_settings_section",
"po_required",
"pr_required",
+ "column_break_12",
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
@@ -42,10 +45,6 @@
"label": "Default Buying Price List",
"options": "Price List"
},
- {
- "fieldname": "column_break_3",
- "fieldtype": "Column Break"
- },
{
"fieldname": "po_required",
"fieldtype": "Select",
@@ -73,7 +72,7 @@
{
"fieldname": "subcontract",
"fieldtype": "Section Break",
- "label": "Subcontract"
+ "label": "Subcontracting Settings"
},
{
"default": "Material Transferred for Subcontract",
@@ -116,6 +115,24 @@
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
+ },
+ {
+ "fieldname": "supplier_and_price_defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Supplier and Price Defaults"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "transaction_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Transaction Settings"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
@@ -123,7 +140,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-08 19:26:23.548837",
+ "modified": "2022-01-27 17:57:58.367048",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -141,5 +158,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
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/queries.py b/erpnext/controllers/queries.py
index 902e1153b4..dd9b45cc3f 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -710,6 +710,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
item_group = filters.get('item_group')
+ company = filters.get('company')
taxes = item_doc.taxes or []
while item_group:
@@ -718,7 +719,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_group = item_group_doc.parent_item_group
if not taxes:
- return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
+ return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True)
else:
valid_from = filters.get('valid_from')
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
@@ -727,7 +728,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
'item_code': filters.get('item_code'),
'posting_date': valid_from,
'tax_category': filters.get('tax_category'),
- 'company': filters.get('company')
+ 'company': company
}
taxes = _get_item_tax_template(args, taxes, for_validate=True)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 75fcaee383..31b2209399 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -74,7 +74,8 @@ class SellingController(StockController):
doctype=self.doctype, company=self.company,
posting_date=self.get('posting_date'),
fetch_payment_terms_template=fetch_payment_terms_template,
- party_address=self.customer_address, shipping_address=self.shipping_address_name)
+ party_address=self.customer_address, shipping_address=self.shipping_address_name,
+ company_address=self.get('company_address'))
if not self.meta.get_field("sales_team"):
party_details.pop("sales_team")
self.update_if_missing(party_details)
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/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index f9c201ab60..940a1bbc00 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', {
});
});
- $.each(["payment_account", "loan_account"], function (i, field) {
+ $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
@@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', {
frm.add_custom_button(__('Loan Write Off'), function() {
frm.trigger("make_loan_write_off_entry");
},__('Create'));
+
+ frm.add_custom_button(__('Loan Refund'), function() {
+ frm.trigger("make_loan_refund");
+ },__('Create'));
}
}
frm.trigger("toggle_fields");
@@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', {
})
},
+ make_loan_refund: function(frm) {
+ frappe.call({
+ args: {
+ "loan": frm.doc.name
+ },
+ method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
+ callback: function (r) {
+ if (r.message) {
+ let doc = frappe.model.sync(r.message)[0];
+ frappe.set_route("Form", doc.doctype, doc.name);
+ }
+ }
+ })
+ },
+
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index af26f7bc5c..196f36f0f4 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"autoname": "ACC-LOAN-.YYYY.-.#####",
- "creation": "2019-08-29 17:29:18.176786",
+ "creation": "2022-01-25 10:30:02.294967",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
@@ -34,6 +34,7 @@
"is_term_loan",
"account_info",
"mode_of_payment",
+ "disbursement_account",
"payment_account",
"column_break_9",
"loan_account",
@@ -356,12 +357,21 @@
"fieldtype": "Date",
"label": "Closure Date",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_type.disbursement_account",
+ "fieldname": "disbursement_account",
+ "fieldtype": "Link",
+ "label": "Disbursement Account",
+ "options": "Account",
+ "read_only": 1,
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-10-12 18:10:32.360818",
+ "modified": "2022-01-25 16:29:16.325501",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
@@ -391,5 +401,6 @@
"search_fields": "posting_date",
"sort_field": "creation",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index f660a24a6d..b798e088b4 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -10,6 +10,7 @@ from frappe import _
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
import erpnext
+from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
@@ -233,17 +234,15 @@ def request_loan_closure(loan, posting_date=None):
loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
- # checking greater than 0 as there may be some minor precision error
- if not pending_amount:
- frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
- elif pending_amount < write_off_limit:
+ if pending_amount and abs(pending_amount) < write_off_limit:
# Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan)
write_off.submit()
- frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
- else:
+ elif pending_amount > 0:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
+ frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
@@ -400,4 +399,39 @@ def add_single_month(date):
if getdate(date) == get_last_day(date):
return get_last_day(add_months(date, 1))
else:
- return add_months(date, 1)
\ No newline at end of file
+ return add_months(date, 1)
+
+@frappe.whitelist()
+def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
+ loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant',
+ 'loan_account', 'payment_account', 'posting_date', 'company', 'name',
+ 'total_payment', 'total_principal_paid'], as_dict=1)
+
+ loan_details.doctype = 'Loan'
+ loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
+
+ if not amount:
+ amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
+
+ if amount < 0:
+ frappe.throw(_('No excess amount pending for refund'))
+
+ refund_jv = get_payment_entry(loan_details, {
+ "party_type": loan_details.applicant_type,
+ "party_account": loan_details.loan_account,
+ "amount_field_party": 'debit_in_account_currency',
+ "amount_field_bank": 'credit_in_account_currency',
+ "amount": amount,
+ "bank_account": loan_details.payment_account
+ })
+
+ if reference_number:
+ refund_jv.cheque_no = reference_number
+
+ if reference_date:
+ refund_jv.cheque_date = reference_date
+
+ if submit:
+ refund_jv.submit()
+
+ return refund_jv
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index 1676c218c8..5ebb2e1bdc 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -42,16 +42,17 @@ class TestLoan(unittest.TestCase):
create_loan_type("Personal Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
+ disbursement_account='Disbursement Account - _TC',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC')
- create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
- 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC',
+ 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
- create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
- 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
+ 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
@@ -679,6 +680,29 @@ class TestLoan(unittest.TestCase):
loan.load_from_db()
self.assertEqual(loan.status, "Loan Closure Requested")
+ def test_loan_repayment_against_partially_disbursed_loan(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ first_date = '2019-10-01'
+ last_date = '2019-10-30'
+
+ make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date)
+
+ loan.load_from_db()
+
+ self.assertEqual(loan.status, "Partially Disbursed")
+ create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
+ flt(loan.loan_amount/3))
+
def test_loan_amount_write_off(self):
pledge = [{
"loan_security": "Test Security 1",
@@ -790,6 +814,18 @@ def create_loan_accounts():
"account_type": "Bank",
}).insert(ignore_permissions=True)
+ if not frappe.db.exists("Account", "Disbursement Account - _TC"):
+ frappe.get_doc({
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Disbursement Account",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Bank Accounts - _TC",
+ "account_type": "Bank",
+ }).insert(ignore_permissions=True)
+
if not frappe.db.exists("Account", "Interest Income Account - _TC"):
frappe.get_doc({
"doctype": "Account",
@@ -815,7 +851,7 @@ def create_loan_accounts():
}).insert(ignore_permissions=True)
def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None,
- mode_of_payment=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
+ mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None,
repayment_method=None, repayment_periods=None):
if not frappe.db.exists("Loan Type", loan_name):
@@ -829,6 +865,7 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i
"penalty_interest_rate": penalty_interest_rate,
"grace_period_in_days": grace_period_in_days,
"mode_of_payment": mode_of_payment,
+ "disbursement_account": disbursement_account,
"payment_account": payment_account,
"loan_account": loan_account,
"interest_income_account": interest_income_account,
diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
index d367e92ac4..640709c095 100644
--- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py
@@ -15,7 +15,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
class TestLoanApplication(unittest.TestCase):
def setUp(self):
create_loan_accounts()
- create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
+ create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC',
'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18)
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR')
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index e2d758b1b9..df3aadfb18 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -122,7 +122,7 @@ class LoanDisbursement(AccountsController):
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
- "against": loan_details.payment_account,
+ "against": loan_details.disbursement_account,
"debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
@@ -137,7 +137,7 @@ class LoanDisbursement(AccountsController):
gle_map.append(
self.get_gl_dict({
- "account": loan_details.payment_account,
+ "account": loan_details.disbursement_account,
"against": loan_details.loan_account,
"credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount,
diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
index 94ec84ea5d..10be750b44 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py
@@ -44,8 +44,8 @@ class TestLoanDisbursement(unittest.TestCase):
def setUp(self):
create_loan_accounts()
- create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
- 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
+ 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
index 46aaaad9fd..e8c77506fc 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
@@ -30,8 +30,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
def setUp(self):
create_loan_accounts()
- create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Payment Account - _TC', 'Loan Account - _TC',
- 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
+ create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC',
+ 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC')
create_loan_security_type()
create_loan_security()
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 7e997e87c3..acf3a655de 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -125,7 +125,7 @@ class LoanRepayment(AccountsController):
def update_paid_amount(self):
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
- 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
+ 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
'written_off_amount'], as_dict=1)
loan.update({
@@ -153,7 +153,7 @@ class LoanRepayment(AccountsController):
def mark_as_unpaid(self):
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
- 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
+ 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable',
'written_off_amount'], as_dict=1)
no_of_repayments = len(self.repayment_details)
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.js b/erpnext/loan_management/doctype/loan_type/loan_type.js
index 04c89c4549..9f9137cfbc 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.js
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.js
@@ -15,7 +15,7 @@ frappe.ui.form.on('Loan Type', {
});
});
- $.each(["payment_account", "loan_account"], function (i, field) {
+ $.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index c0a5d2cda1..00337e4b4c 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -19,9 +19,10 @@
"description",
"account_details_section",
"mode_of_payment",
+ "disbursement_account",
"payment_account",
- "loan_account",
"column_break_12",
+ "loan_account",
"interest_income_account",
"penalty_income_account",
"amended_from"
@@ -79,7 +80,7 @@
{
"fieldname": "payment_account",
"fieldtype": "Link",
- "label": "Payment Account",
+ "label": "Repayment Account",
"options": "Account",
"reqd": 1
},
@@ -149,15 +150,23 @@
"fieldtype": "Currency",
"label": "Auto Write Off Amount ",
"options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "disbursement_account",
+ "fieldtype": "Link",
+ "label": "Disbursement Account",
+ "options": "Account",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:10:57.368490",
+ "modified": "2022-01-25 16:23:57.009349",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -181,5 +190,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index a399edda70..76978017a6 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -703,7 +703,8 @@ class TestWorkOrder(ERPNextTestCase):
wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse,
company=company)
- self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
+ stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture'))
+ self.assertRaises(frappe.ValidationError, stock_entry.save)
def test_wo_completion_with_pl_bom(self):
from erpnext.manufacturing.doctype.bom.test_bom import (
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index 090a3e74fc..2693352324 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -89,10 +89,10 @@ def get_bom_stock(filters):
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
+ details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"])
manufacture_details = frappe._dict()
for detail in details:
- dic = manufacture_details.setdefault(detail.get('parent'), {})
+ dic = manufacture_details.setdefault(detail.get('item_code'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
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/patches.txt b/erpnext/patches.txt
index eace7ca7f9..feafecbc04 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -349,3 +349,4 @@ erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v14_0.migrate_cost_center_allocations
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.shopping_cart_to_ecommerce
+erpnext.patches.v13_0.update_disbursement_account
diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py
new file mode 100644
index 0000000000..c56fa8fdc6
--- /dev/null
+++ b/erpnext/patches/v13_0/update_disbursement_account.py
@@ -0,0 +1,22 @@
+import frappe
+
+
+def execute():
+
+ frappe.reload_doc("loan_management", "doctype", "loan_type")
+ frappe.reload_doc("loan_management", "doctype", "loan")
+
+ loan_type = frappe.qb.DocType("Loan Type")
+ loan = frappe.qb.DocType("Loan")
+
+ frappe.qb.update(
+ loan_type
+ ).set(
+ loan_type.disbursement_account, loan_type.payment_account
+ ).run()
+
+ frappe.qb.update(
+ loan
+ ).set(
+ loan.disbursement_account, loan.payment_account
+ ).run()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index 4f097fa2c3..5f836db2f0 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -214,6 +214,7 @@ class TestPayrollEntry(unittest.TestCase):
create_loan_type("Car Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
+ disbursement_account='Disbursement Account - _TC',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 597fd5a250..30b604b2c0 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -370,6 +370,7 @@ class TestSalarySlip(unittest.TestCase):
create_loan_type("Car Loan", 500000, 8.4,
is_term_loan=1,
mode_of_payment='Cash',
+ disbursement_account='Disbursement Account - _TC',
payment_account='Payment Account - _TC',
loan_account='Loan Account - _TC',
interest_income_account='Interest Income Account - _TC',
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/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 27bc541d62..7c4a3f63dc 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -80,7 +80,7 @@
"description": "How often should Project and Company be updated based on Sales Transactions?",
"fieldname": "sales_update_frequency",
"fieldtype": "Select",
- "label": "Sales Update Frequency",
+ "label": "Sales Update Frequency in Company and Project",
"options": "Each Transaction\nDaily\nMonthly",
"reqd": 1
},
@@ -171,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-13 12:32:17.004404",
+ "modified": "2022-02-04 15:41:59.939261",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@@ -189,5 +189,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
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_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 2f37778896..c38dfaa1c8 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -8,7 +8,6 @@
"engine": "InnoDB",
"field_order": [
"items_section",
- "title",
"naming_series",
"stock_entry_type",
"outgoing_stock_entry",
@@ -83,14 +82,6 @@
"fieldtype": "Section Break",
"oldfieldtype": "Section Break"
},
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Title",
- "no_copy": 1,
- "print_hide": 1
- },
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -353,9 +344,9 @@
},
{
"fieldname": "scan_barcode",
- "options": "Barcode",
"fieldtype": "Data",
- "label": "Scan Barcode"
+ "label": "Scan Barcode",
+ "options": "Barcode"
},
{
"allow_bulk_edit": 1,
@@ -628,10 +619,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-08-20 19:19:31.514846",
+ "modified": "2022-02-07 12:55:14.614077",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -698,6 +690,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "title",
+ "states": [],
+ "title_field": "stock_entry_type",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index c51c9bc5f4..782fcf04a5 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -76,7 +76,6 @@ class StockEntry(StockController):
self.validate_posting_time()
self.validate_purpose()
- self.set_title()
self.validate_item()
self.validate_customer_provided_item()
self.validate_qty()
@@ -1116,7 +1115,7 @@ class StockEntry(StockController):
self.set_actual_qty()
self.update_items_for_process_loss()
self.validate_customer_provided_item()
- self.calculate_rate_and_amount()
+ self.calculate_rate_and_amount(raise_error_if_no_rate=False)
def set_scrap_items(self):
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
@@ -1835,14 +1834,6 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
- def set_title(self):
- if frappe.flags.in_import and self.title:
- # Allow updating title during data import/update
- return
-
- self.title = self.purpose
-
-
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, str):
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/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 33d9a6ce41..438ec16096 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -5,35 +5,41 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "defaults_tab",
"item_defaults_section",
"item_naming_by",
"item_group",
"stock_uom",
- "default_warehouse",
"column_break_4",
- "valuation_method",
+ "default_warehouse",
"sample_retention_warehouse",
- "use_naming_series",
- "naming_series_prefix",
+ "valuation_method",
+ "price_list_defaults_section",
+ "auto_insert_price_list_rate_if_missing",
+ "column_break_12",
+ "update_existing_price_list_rate",
+ "stock_validations_tab",
"section_break_9",
"over_delivery_receipt_allowance",
- "role_allowed_to_over_deliver_receive",
"mr_qty_allowance",
- "column_break_12",
- "auto_insert_price_list_rate_if_missing",
- "update_existing_price_list_rate",
+ "column_break_121",
+ "role_allowed_to_over_deliver_receive",
"allow_negative_stock",
"show_barcode_field",
"clean_description_html",
"quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
- "column_break_21",
+ "column_break_23",
"action_if_quality_inspection_is_rejected",
+ "serial_and_batch_item_settings_tab",
"section_break_7",
"automatically_set_serial_nos_based_on_fifo",
"set_qty_in_transactions_based_on_serial_no_input",
"column_break_10",
"disable_serial_no_and_batch_selector",
+ "use_naming_series",
+ "naming_series_prefix",
+ "stock_planning_tab",
"auto_material_request",
"auto_indent",
"column_break_27",
@@ -42,6 +48,7 @@
"allow_from_dn",
"column_break_31",
"allow_from_pr",
+ "stock_closing_tab",
"control_historical_stock_transactions_section",
"stock_frozen_upto",
"stock_frozen_upto_days",
@@ -122,7 +129,7 @@
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
- "label": "Serialised and Batch Setting"
+ "label": "Serial & Batch Item Settings"
},
{
"default": "0",
@@ -275,10 +282,6 @@
"fieldtype": "Section Break",
"label": "Quality Inspection Settings"
},
- {
- "fieldname": "column_break_21",
- "fieldtype": "Column Break"
- },
{
"default": "Stop",
"fieldname": "action_if_quality_inspection_is_rejected",
@@ -298,6 +301,44 @@
"fieldname": "update_existing_price_list_rate",
"fieldtype": "Check",
"label": "Update Existing Price List Rate"
+ },
+ {
+ "fieldname": "defaults_tab",
+ "fieldtype": "Tab Break",
+ "label": "Defaults"
+ },
+ {
+ "fieldname": "stock_validations_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Validations"
+ },
+ {
+ "fieldname": "stock_planning_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Planning"
+ },
+ {
+ "fieldname": "stock_closing_tab",
+ "fieldtype": "Tab Break",
+ "label": "Stock Closing"
+ },
+ {
+ "fieldname": "serial_and_batch_item_settings_tab",
+ "fieldtype": "Tab Break",
+ "label": "Serial & Batch Item"
+ },
+ {
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "price_list_defaults_section",
+ "fieldtype": "Section Break",
+ "label": "Price List Defaults"
+ },
+ {
+ "fieldname": "column_break_121",
+ "fieldtype": "Column Break"
}
],
"icon": "icon-cog",
@@ -305,7 +346,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-11-06 19:40:02.183592",
+ "modified": "2022-02-04 15:33:43.692736",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
@@ -324,5 +365,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
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