diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index d961ead642d..1f2d6ce59a8 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: {
bank_account: frm.doc.bank_account,
- till_date: frm.doc.bank_statement_from_date,
+ till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1)
},
callback: (response) => {
frm.set_value("account_opening_balance", response.message);
diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json
index 1b38f0d36d7..051b44b5868 100644
--- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.json
+++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.json
@@ -1,457 +1,152 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
+ "actions": [],
"autoname": "naming_series:",
- "beta": 0,
"creation": "2018-06-18 16:51:49.994750",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "user",
+ "date",
+ "from_time",
+ "time",
+ "expense",
+ "custody",
+ "returns",
+ "outstanding_amount",
+ "payments",
+ "net_amount",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "POS-CLO-",
"fieldname": "naming_series",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 1,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Series",
- "length": 0,
- "no_copy": 0,
"options": "POS-CLO-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "user",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
- "length": 0,
- "no_copy": 0,
"options": "User",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Today",
"fieldname": "date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "from_time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "From Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
"fieldname": "time",
"fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "To Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "expense",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expense",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Expense"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "custody",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Custody",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Custody"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "returns",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Returns",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "precision": "2"
},
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0.00",
"fieldname": "outstanding_amount",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Outstanding Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0.0",
"fieldname": "payments",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Payments",
- "length": 0,
- "no_copy": 0,
- "options": "Cashier Closing Payments",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Cashier Closing Payments"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "net_amount",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
"in_filter": 1,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Net Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Amended From",
- "length": 0,
"no_copy": 1,
"options": "Cashier Closing",
- "permlevel": 0,
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
"is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-02-19 08:35:24.157327",
+ "links": [],
+ "modified": "2023-12-28 13:15:46.858427",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cashier Closing",
- "name_case": "",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index de2a9db2c8f..0800971269b 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -21,6 +21,7 @@
"against_voucher_type",
"against_voucher",
"voucher_type",
+ "voucher_subtype",
"voucher_no",
"voucher_detail_no",
"project",
@@ -278,13 +279,18 @@
"fieldtype": "Currency",
"label": "Credit Amount in Transaction Currency",
"options": "transaction_currency"
+ },
+ {
+ "fieldname": "voucher_subtype",
+ "fieldtype": "Small Text",
+ "label": "Voucher Subtype"
}
],
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2023-08-16 21:38:44.072267",
+ "modified": "2023-12-18 15:38:14.006208",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f7dd29ab1c1..139f52696bc 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -39,6 +39,8 @@ class GLEntry(Document):
account: DF.Link | None
account_currency: DF.Link | None
against: DF.Text | None
+ against_link: DF.DynamicLink | None
+ against_type: DF.Link | None
against_voucher: DF.DynamicLink | None
against_voucher_type: DF.Link | None
company: DF.Link | None
@@ -66,6 +68,7 @@ class GLEntry(Document):
transaction_exchange_rate: DF.Float
voucher_detail_no: DF.Data | None
voucher_no: DF.DynamicLink | None
+ voucher_subtype: DF.SmallText | None
voucher_type: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index bfb51da58b3..e542d3cc630 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -765,7 +765,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
- .select(Sum(p_item.qty).as_("qty"))
+ .select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@@ -775,7 +775,7 @@ def get_pos_reserved_qty(item_code, warehouse):
)
).run(as_dict=True)
- return reserved_qty[0].qty or 0 if reserved_qty else 0
+ return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist()
diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
index 67a7f90042b..bf1cb418fe4 100644
--- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py
@@ -440,7 +440,7 @@ def reconcile(doc: None | str = None) -> None:
# Update the parent doc about the exception
frappe.db.rollback()
- traceback = frappe.get_traceback()
+ traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback:
" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index db33271bccf..657723796cf 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -1135,11 +1135,17 @@ class PurchaseInvoice(BuyingController):
)
assets = frappe.db.get_all(
- "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
+ "Asset",
+ filters={"purchase_invoice": self.name, "item_code": item.item_code},
+ fields=["name", "asset_quantity"],
)
for asset in assets:
- frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
- frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
+ frappe.db.set_value(
+ "Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity
+ )
+ frappe.db.set_value(
+ "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity
+ )
def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index e43ea6ecbe0..e41cec7eee3 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1985,6 +1985,26 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
+ def test_debit_note_without_item(self):
+ pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
+ pi.items[0].item_code = ""
+ pi.save()
+
+ self.assertFalse(pi.items[0].item_code)
+ pi.submit()
+
+ return_pi = make_purchase_invoice(
+ item_name="_Test Item",
+ is_return=1,
+ return_against=pi.name,
+ qty=-10,
+ do_not_save=True,
+ )
+ return_pi.items[0].item_code = ""
+ return_pi.save()
+ return_pi.submit()
+ self.assertEqual(return_pi.docstatus, 1)
+
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@@ -2121,6 +2141,7 @@ def make_purchase_invoice(**args):
"items",
{
"item_code": args.item or args.item_code or "_Test Item",
+ "item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 5,
"received_qty": args.received_qty or 0,
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
index 38bc1a6fb82..2d10f7c7624 100644
--- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py
@@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None):
except Exception as e:
frappe.db.rollback()
- traceback = frappe.get_traceback()
+ traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback:
" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 6aba1faa84b..5924586e73f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -586,6 +586,8 @@ class SalesInvoice(SellingController):
"Serial and Batch Bundle",
)
+ self.delete_auto_created_batches()
+
def update_status_updater_args(self):
if cint(self.update_stock):
self.status_updater.append(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 450c8effea7..98d4ed46ccc 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1414,10 +1414,11 @@ class TestSalesInvoice(FrappeTestCase):
def test_serialized_cancel(self):
si = self.test_serialized()
- si.cancel()
-
+ si.reload()
serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
+ si.cancel()
+
self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
)
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index a403b14c54c..ec9e792d7d4 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -81,6 +81,7 @@
"warehouse",
"target_warehouse",
"quality_inspection",
+ "pick_serial_and_batch",
"serial_and_batch_bundle",
"batch_no",
"incoming_rate",
@@ -897,12 +898,18 @@
"options": "Serial and Batch Bundle",
"print_hide": 1,
"search_index": 1
+ },
+ {
+ "depends_on": "eval:parent.update_stock === 1",
+ "fieldname": "pick_serial_and_batch",
+ "fieldtype": "Button",
+ "label": "Pick Serial / Batch No"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:34:10.479329",
+ "modified": "2023-12-29 13:03:14.121298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index 187b7abce18..97fd4d040f1 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -148,13 +148,13 @@
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
- "label": "Additional DIscount Percentage"
+ "label": "Additional Discount Percentage"
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
- "label": "Additional DIscount Amount"
+ "label": "Additional Discount Amount"
},
{
"collapsible": 1,
@@ -267,7 +267,7 @@
"link_fieldname": "subscription"
}
],
- "modified": "2023-09-18 17:48:21.900252",
+ "modified": "2023-12-28 17:20:42.687789",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 6cc2d1e9ee9..94f5e29f068 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -356,18 +356,20 @@ class Subscription(Document):
self,
from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None,
+ posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document:
"""
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
saves the `Subscription`.
Backwards compatibility
"""
- return self.create_invoice(from_date=from_date, to_date=to_date)
+ return self.create_invoice(from_date=from_date, to_date=to_date, posting_date=posting_date)
def create_invoice(
self,
from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None,
+ posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document:
"""
Creates a `Invoice`, submits it and returns it
@@ -385,11 +387,13 @@ class Subscription(Document):
invoice = frappe.new_doc(self.invoice_document_type)
invoice.company = company
invoice.set_posting_time = 1
- invoice.posting_date = (
- self.current_invoice_start
- if self.generate_invoice_at == "Beginning of the current subscription period"
- else self.current_invoice_end
- )
+
+ if self.generate_invoice_at == "Beginning of the current subscription period":
+ invoice.posting_date = self.current_invoice_start
+ elif self.generate_invoice_at == "Days before the current subscription period":
+ invoice.posting_date = posting_date or self.current_invoice_start
+ else:
+ invoice.posting_date = self.current_invoice_end
invoice.cost_center = self.cost_center
@@ -413,6 +417,7 @@ class Subscription(Document):
# Subscription is better suited for service items. I won't update `update_stock`
# for that reason
items_list = self.get_items_from_plans(self.plans, is_prorate())
+
for item in items_list:
item["cost_center"] = self.cost_center
invoice.append("items", item)
@@ -556,7 +561,7 @@ class Subscription(Document):
if not self.is_current_invoice_generated(
self.current_invoice_start, self.current_invoice_end
) and self.can_generate_new_invoice(posting_date):
- self.generate_invoice()
+ self.generate_invoice(posting_date=posting_date)
self.update_subscription_period(add_days(self.current_invoice_end, 1))
if self.cancel_at_period_end and (
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js
index 5f78b779342..06fa9f3175d 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js
@@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1
},
+ {
+ "fieldname":"group_by",
+ "label": __("Group By"),
+ "fieldtype": "Select",
+ "options": ["Asset Category", "Asset"],
+ "default": "Asset Category",
+ },
{
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
- "options": "Asset Category"
- }
+ "options": "Asset Category",
+ "depends_on": "eval: doc.group_by == 'Asset Category'",
+ },
+ {
+ "fieldname":"asset",
+ "label": __("Asset"),
+ "fieldtype": "Link",
+ "options": "Asset",
+ "depends_on": "eval: doc.group_by == 'Asset'",
+ },
]
}
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index bdc8d8504f8..48da17ab625 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -14,10 +14,17 @@ def execute(filters=None):
def get_data(filters):
+ if filters.get("group_by") == "Asset Category":
+ return get_group_by_asset_category_data(filters)
+ elif filters.get("group_by") == "Asset":
+ return get_group_by_asset_data(filters)
+
+
+def get_group_by_asset_category_data(filters):
data = []
- asset_categories = get_asset_categories(filters)
- assets = get_assets(filters)
+ asset_categories = get_asset_categories_for_grouped_by_category(filters)
+ assets = get_assets_for_grouped_by_category(filters)
for asset_category in asset_categories:
row = frappe._dict()
@@ -38,6 +45,7 @@ def get_data(filters):
if asset["asset_category"] == asset_category.get("asset_category", "")
)
)
+
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
@@ -57,7 +65,7 @@ def get_data(filters):
return data
-def get_asset_categories(filters):
+def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
@@ -116,7 +124,105 @@ def get_asset_categories(filters):
)
-def get_assets(filters):
+def get_asset_details_for_grouped_by_category(filters):
+ condition = ""
+ if filters.get("asset"):
+ condition += " and name = %(asset)s"
+ return frappe.db.sql(
+ """
+ SELECT name,
+ ifnull(sum(case when purchase_date < %(from_date)s then
+ case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
+ gross_purchase_amount
+ else
+ 0
+ end
+ else
+ 0
+ end), 0) as cost_as_on_from_date,
+ ifnull(sum(case when purchase_date >= %(from_date)s then
+ gross_purchase_amount
+ else
+ 0
+ end), 0) as cost_of_new_purchase,
+ ifnull(sum(case when ifnull(disposal_date, 0) != 0
+ and disposal_date >= %(from_date)s
+ and disposal_date <= %(to_date)s then
+ case when status = "Sold" then
+ gross_purchase_amount
+ else
+ 0
+ end
+ else
+ 0
+ end), 0) as cost_of_sold_asset,
+ ifnull(sum(case when ifnull(disposal_date, 0) != 0
+ and disposal_date >= %(from_date)s
+ and disposal_date <= %(to_date)s then
+ case when status = "Scrapped" then
+ gross_purchase_amount
+ else
+ 0
+ end
+ else
+ 0
+ end), 0) as cost_of_scrapped_asset
+ from `tabAsset`
+ where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
+ group by name
+ """.format(
+ condition
+ ),
+ {
+ "to_date": filters.to_date,
+ "from_date": filters.from_date,
+ "company": filters.company,
+ "asset": filters.get("asset"),
+ },
+ as_dict=1,
+ )
+
+
+def get_group_by_asset_data(filters):
+ data = []
+
+ asset_details = get_asset_details_for_grouped_by_category(filters)
+ assets = get_assets_for_grouped_by_asset(filters)
+
+ for asset_detail in asset_details:
+ row = frappe._dict()
+ # row.asset_category = asset_category
+ row.update(asset_detail)
+
+ row.cost_as_on_to_date = (
+ flt(row.cost_as_on_from_date)
+ + flt(row.cost_of_new_purchase)
+ - flt(row.cost_of_sold_asset)
+ - flt(row.cost_of_scrapped_asset)
+ )
+
+ row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
+
+ row.accumulated_depreciation_as_on_to_date = (
+ flt(row.accumulated_depreciation_as_on_from_date)
+ + flt(row.depreciation_amount_during_the_period)
+ - flt(row.depreciation_eliminated_during_the_period)
+ )
+
+ row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
+ row.accumulated_depreciation_as_on_from_date
+ )
+
+ row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
+ row.accumulated_depreciation_as_on_to_date
+ )
+
+ data.append(row)
+
+ return data
+
+
+def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
@@ -178,15 +284,93 @@ def get_assets(filters):
)
+def get_assets_for_grouped_by_asset(filters):
+ condition = ""
+ if filters.get("asset"):
+ condition = " and a.name = '{}'".format(filters.get("asset"))
+ return frappe.db.sql(
+ """
+ SELECT results.name as asset,
+ sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
+ sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
+ sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
+ from (SELECT a.name as name,
+ ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
+ gle.debit
+ else
+ 0
+ end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
+ gle.debit
+ else
+ 0
+ end), 0) as depreciation_eliminated_during_the_period,
+ ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
+ and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
+ gle.debit
+ else
+ 0
+ end), 0) as depreciation_amount_during_the_period
+ from `tabGL Entry` gle
+ join `tabAsset` a on
+ gle.against_voucher = a.name
+ join `tabAsset Category Account` aca on
+ aca.parent = a.asset_category and aca.company_name = %(company)s
+ join `tabCompany` company on
+ company.name = %(company)s
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
+ group by a.name
+ union
+ SELECT a.name as name,
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
+ 0
+ else
+ a.opening_accumulated_depreciation
+ end), 0) as accumulated_depreciation_as_on_from_date,
+ ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
+ a.opening_accumulated_depreciation
+ else
+ 0
+ end), 0) as depreciation_eliminated_during_the_period,
+ 0 as depreciation_amount_during_the_period
+ from `tabAsset` a
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
+ group by a.name) as results
+ group by results.name
+ """.format(
+ condition
+ ),
+ {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
+ as_dict=1,
+ )
+
+
def get_columns(filters):
- return [
- {
- "label": _("Asset Category"),
- "fieldname": "asset_category",
- "fieldtype": "Link",
- "options": "Asset Category",
- "width": 120,
- },
+ columns = []
+
+ if filters.get("group_by") == "Asset Category":
+ columns.append(
+ {
+ "label": _("Asset Category"),
+ "fieldname": "asset_category",
+ "fieldtype": "Link",
+ "options": "Asset Category",
+ "width": 120,
+ }
+ )
+ elif filters.get("group_by") == "Asset":
+ columns.append(
+ {
+ "label": _("Asset"),
+ "fieldname": "asset",
+ "fieldtype": "Link",
+ "options": "Asset",
+ "width": 120,
+ }
+ )
+
+ columns += [
{
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "cost_as_on_from_date",
@@ -254,3 +438,5 @@ def get_columns(filters):
"width": 200,
},
]
+
+ return columns
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
index 15aa265b56a..9c356bf28ea 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js
@@ -2,7 +2,44 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Budget Variance Report"] = {
- "filters": [
+ "filters": get_filters(),
+ "formatter": function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname.includes(__("variance"))) {
+
+ if (data[column.fieldname] < 0) {
+ value = "" + value + "";
+ }
+ else if (data[column.fieldname] > 0) {
+ value = "" + value + "";
+ }
+ }
+
+ return value;
+ }
+}
+function get_filters() {
+ function get_dimensions() {
+ let result = [];
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ args: {
+ 'with_cost_center_and_project': true
+ },
+ async: false,
+ callback: function(r) {
+ if(!r.exc) {
+ result = r.message[0].map(elem => elem.document_type);
+ }
+ }
+ });
+ return result;
+ }
+
+ let budget_against_options = get_dimensions();
+
+ let filters = [
{
fieldname: "from_fiscal_year",
label: __("From Fiscal Year"),
@@ -44,9 +81,13 @@ frappe.query_reports["Budget Variance Report"] = {
fieldname: "budget_against",
label: __("Budget Against"),
fieldtype: "Select",
- options: ["Cost Center", "Project"],
+ options: budget_against_options,
default: "Cost Center",
reqd: 1,
+ get_data: function() {
+ console.log(this.options);
+ return ["Emacs", "Rocks"];
+ },
on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh();
@@ -71,24 +112,8 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check",
default: 0,
},
- ],
- "formatter": function (value, row, column, data, default_formatter) {
- value = default_formatter(value, row, column, data);
+ ]
- if (column.fieldname.includes(__("variance"))) {
-
- if (data[column.fieldname] < 0) {
- value = "" + value + "";
- }
- else if (data[column.fieldname] > 0) {
- value = "" + value + "";
- }
- }
-
- return value;
- }
+ return filters;
}
-erpnext.dimension_filters.forEach((dimension) => {
- frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
-});
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
index 4765e3b318a..0464f99d200 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
@@ -3,7 +3,7 @@
import frappe
-from frappe import _, scrub
+from frappe import _, qb, scrub
from frappe.utils import getdate, nowdate
@@ -38,7 +38,6 @@ class PartyLedgerSummaryReport(object):
"""
Additional Columns for 'User Permission' based access control
"""
- from frappe import qb
if self.filters.party_type == "Customer":
self.territories = frappe._dict({})
@@ -365,13 +364,29 @@ class PartyLedgerSummaryReport(object):
def get_party_adjustment_amounts(self):
conditions = self.prepare_conditions()
- income_or_expense = (
- "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
+ account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
+ income_or_expense_accounts = frappe.db.get_all(
+ "Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
)
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
+ gl = qb.DocType("GL Entry")
+ if not income_or_expense_accounts:
+ # prevent empty 'in' condition
+ income_or_expense_accounts.append("")
+
+ accounts_query = (
+ qb.from_(gl)
+ .select(gl.voucher_type, gl.voucher_no)
+ .where(
+ (gl.account.isin(income_or_expense_accounts))
+ & (gl.posting_date.gte(self.filters.from_date))
+ & (gl.posting_date.lte(self.filters.to_date))
+ )
+ )
+
gl_entries = frappe.db.sql(
"""
select
@@ -381,16 +396,15 @@ class PartyLedgerSummaryReport(object):
where
docstatus < 2 and is_cancelled = 0
and (voucher_type, voucher_no) in (
- select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
- where acc.name = gle.account and acc.account_type = '{income_or_expense}'
- and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2
+ {accounts_query}
) and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle
where gle.party_type=%(party_type)s and ifnull(party, '') != ''
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
)
- """.format(
- conditions=conditions, income_or_expense=income_or_expense
+ """.format(
+ accounts_query=accounts_query,
+ conditions=conditions,
),
self.filters,
as_dict=True,
@@ -414,7 +428,7 @@ class PartyLedgerSummaryReport(object):
elif gle.party:
parties.setdefault(gle.party, 0)
parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr)
- elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense:
+ elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type:
accounts.setdefault(gle.account, 0)
accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
else:
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 7355c4b8a16..004a9299ea2 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -211,7 +211,13 @@ def get_data(
ignore_accumulated_values_for_fy,
)
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
- out = prepare_data(accounts, balance_must_be, period_list, company_currency)
+ out = prepare_data(
+ accounts,
+ balance_must_be,
+ period_list,
+ company_currency,
+ accumulated_values=filters.accumulated_values,
+ )
out = filter_out_zero_value_rows(out, parent_children_map)
if out and total:
@@ -270,7 +276,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list):
) + d.get("opening_balance", 0.0)
-def prepare_data(accounts, balance_must_be, period_list, company_currency):
+def prepare_data(accounts, balance_must_be, period_list, company_currency, accumulated_values):
data = []
year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d")
year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d")
@@ -310,8 +316,14 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency):
has_value = True
total += flt(row[period.key])
- row["has_value"] = has_value
- row["total"] = total
+ if accumulated_values:
+ # when 'accumulated_values' is enabled, periods have running balance.
+ # so, last period will have the net amount.
+ row["has_value"] = has_value
+ row["total"] = flt(d.get(period_list[-1].key, 0.0), 3)
+ else:
+ row["has_value"] = has_value
+ row["total"] = total
data.append(row)
return data
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 4cb443cf920..79b5e4d9ec8 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -52,6 +52,11 @@ frappe.query_reports["General Ledger"] = {
frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)");
}
},
+ {
+ "fieldname":"against_voucher_no",
+ "label": __("Against Voucher No"),
+ "fieldtype": "Data",
+ },
{
"fieldtype": "Break",
},
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 896c4c98002..ff6cd9f4b25 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -200,7 +200,7 @@ def get_gl_entries(filters, accounting_dimensions):
"""
select
name as gl_entry, posting_date, account, party_type, party,
- voucher_type, voucher_no, {dimension_fields}
+ voucher_type, voucher_subtype, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
against, is_opening, creation {select_fields}
@@ -238,6 +238,9 @@ def get_conditions(filters):
if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s")
+ if filters.get("against_voucher_no"):
+ conditions.append("against_voucher=%(against_voucher_no)s")
+
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
@@ -608,6 +611,12 @@ def get_columns(filters):
columns += [
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
+ {
+ "label": _("Voucher Subtype"),
+ "fieldname": "voucher_subtype",
+ "fieldtype": "Data",
+ "width": 180,
+ },
{
"label": _("Voucher No"),
"fieldname": "voucher_no",
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
index 66353358a06..0b7ce518918 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
@@ -82,14 +82,25 @@ def get_report_summary(
if filters.get("accumulated_in_group_company"):
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
- for period in period_list:
- key = period if consolidated else period.key
+ if filters.accumulated_values:
+ # when 'accumulated_values' is enabled, periods have running balance.
+ # so, last period will have the net amount.
+ key = period_list[-1].key
if income:
- net_income += income[-2].get(key)
+ net_income = income[-2].get(key)
if expense:
- net_expense += expense[-2].get(key)
+ net_expense = expense[-2].get(key)
if net_profit_loss:
- net_profit += net_profit_loss.get(key)
+ net_profit = net_profit_loss.get(key)
+ else:
+ for period in period_list:
+ key = period if consolidated else period.key
+ if income:
+ net_income += income[-2].get(key)
+ if expense:
+ net_expense += expense[-2].get(key)
+ if net_profit_loss:
+ net_profit += net_profit_loss.get(key)
if len(period_list) == 1 and periodicity == "Yearly":
profit_label = _("Profit This Year")
diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py
new file mode 100644
index 00000000000..b4423abc7ff
--- /dev/null
+++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, getdate, today
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.financial_statements import get_period_list
+from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import execute
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
+ frappe.set_user("Administrator")
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=rate,
+ price_list_rate=rate,
+ qty=qty,
+ do_not_save=1,
+ )
+ si = si.save()
+ if not do_not_submit:
+ si = si.submit()
+ return si
+
+ def get_fiscal_year(self):
+ active_fy = frappe.db.get_all(
+ "Fiscal Year",
+ filters={"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
+ )[0]
+ return frappe.get_doc("Fiscal Year", active_fy.name)
+
+ def get_report_filters(self):
+ fy = self.get_fiscal_year()
+ return frappe._dict(
+ company=self.company,
+ from_fiscal_year=fy.name,
+ to_fiscal_year=fy.name,
+ period_start_date=fy.year_start_date,
+ period_end_date=fy.year_end_date,
+ filter_based_on="Fiscal Year",
+ periodicity="Monthly",
+ accumulated_vallues=True,
+ )
+
+ def test_profit_and_loss_output_and_summary(self):
+ si = self.create_sales_invoice(qty=1, rate=150)
+
+ filters = self.get_report_filters()
+ period_list = get_period_list(
+ filters.from_fiscal_year,
+ filters.to_fiscal_year,
+ filters.period_start_date,
+ filters.period_end_date,
+ filters.filter_based_on,
+ filters.periodicity,
+ company=filters.company,
+ )
+
+ result = execute(filters)[1]
+ current_period = [x for x in period_list if x.from_date <= getdate() and x.to_date >= getdate()][
+ 0
+ ]
+ current_period_key = current_period.key
+ without_current_period = [x for x in period_list if x.key != current_period.key]
+ # all period except current period(whence invoice was posted), should be '0'
+ for acc in result:
+ if acc:
+ with self.subTest(acc=acc):
+ for period in without_current_period:
+ self.assertEqual(acc[period.key], 0)
+
+ for acc in result:
+ if acc:
+ with self.subTest(current_period_key=current_period_key):
+ self.assertEqual(acc[current_period_key], 150)
+ self.assertEqual(acc["total"], 150)
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
index b6bbd979ed9..5dd3617a926 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js
@@ -117,8 +117,3 @@ frappe.query_reports["Profitability Analysis"] = {
"parent_field": "parent_account",
"initial_depth": 3
}
-
-erpnext.dimension_filters.forEach((dimension) => {
- frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
-});
-
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 4447b076c71..9c3135d6b10 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -225,6 +225,11 @@ class AccountsController(TransactionBase):
apply_pricing_rule_on_transaction(self)
self.set_total_in_words()
+ self.set_default_letter_head()
+
+ def set_default_letter_head(self):
+ if hasattr(self, "letter_head") and not self.letter_head:
+ self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")
def init_internal_values(self):
# init all the internal values as 0 on sa
@@ -874,6 +879,7 @@ class AccountsController(TransactionBase):
"project": self.get("project"),
"post_net_value": args.get("post_net_value"),
"voucher_detail_no": args.get("voucher_detail_no"),
+ "voucher_subtype": self.get_voucher_subtype(),
}
)
@@ -927,8 +933,33 @@ class AccountsController(TransactionBase):
}
)
+ if not args.get("against_voucher_type") and self.get("against_voucher_type"):
+ gl_dict.update({"against_voucher_type": self.get("against_voucher_type")})
+
+ if not args.get("against_voucher") and self.get("against_voucher"):
+ gl_dict.update({"against_voucher": self.get("against_voucher")})
+
return gl_dict
+ def get_voucher_subtype(self):
+ voucher_subtypes = {
+ "Journal Entry": "voucher_type",
+ "Payment Entry": "payment_type",
+ "Stock Entry": "stock_entry_type",
+ "Asset Capitalization": "entry_type",
+ }
+ if self.doctype in voucher_subtypes:
+ return self.get(voucher_subtypes[self.doctype])
+ elif self.doctype == "Purchase Receipt" and self.is_return:
+ return "Purchase Return"
+ elif self.doctype == "Delivery Note" and self.is_return:
+ return "Sales Return"
+ elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice":
+ return "Credit Note"
+ elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice":
+ return "Debit Note"
+ return self.doctype
+
def get_value_in_transaction_currency(self, account_currency, args, field):
if account_currency == self.get("currency"):
return args.get(field + "_in_account_currency")
@@ -2393,6 +2424,7 @@ def validate_taxes_and_charges(tax):
def validate_account_head(idx, account, company, context=""):
account_company = frappe.get_cached_value("Account", account, "company")
+ is_group = frappe.get_cached_value("Account", account, "is_group")
if account_company != company:
frappe.throw(
@@ -2402,6 +2434,12 @@ def validate_account_head(idx, account, company, context=""):
title=_("Invalid Account"),
)
+ if is_group:
+ frappe.throw(
+ _("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)),
+ title=_("Invalid Account"),
+ )
+
def validate_cost_center(tax, doc):
if not tax.cost_center:
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 81080f02665..e7bd2a7265c 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -562,16 +562,17 @@ def make_return_doc(
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
- item_details = frappe.get_cached_value(
- "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
- )
+ if source_doc.item_code:
+ item_details = frappe.get_cached_value(
+ "Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
+ )
- if not item_details.has_batch_no and not item_details.has_serial_no:
- return
+ if not item_details.has_batch_no and not item_details.has_serial_no:
+ return
- for qty_field in ["stock_qty", "rejected_qty"]:
- if target_doc.get(qty_field):
- update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
+ for qty_field in ["stock_qty", "rejected_qty"]:
+ if target_doc.get(qty_field):
+ update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
def update_terms(source_doc, target_doc, source_parent):
target_doc.payment_amount = -source_doc.payment_amount
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index f6b6802d581..78bb2d2c270 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -37,6 +37,7 @@ welcome_email = "erpnext.setup.utils.welcome_email"
# setup wizard
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages"
+setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo"
setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test"
before_install = [
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 8cb024209c2..682c4fb82a7 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1486,3 +1486,47 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
)
return doc
+
+
+def get_op_cost_from_sub_assemblies(bom_no, op_cost=0):
+ # Get operating cost from sub-assemblies
+
+ bom_items = frappe.get_all(
+ "BOM Item", filters={"parent": bom_no, "docstatus": 1}, fields=["bom_no"], order_by="idx asc"
+ )
+
+ for row in bom_items:
+ if not row.bom_no:
+ continue
+
+ if cost := frappe.get_cached_value("BOM", row.bom_no, "operating_cost_per_bom_quantity"):
+ op_cost += flt(cost)
+ get_op_cost_from_sub_assemblies(row.bom_no, op_cost)
+
+ return op_cost
+
+
+def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
+ if not scrap_items:
+ scrap_items = {}
+
+ bom_items = frappe.get_all(
+ "BOM Item",
+ filters={"parent": bom_no, "docstatus": 1},
+ fields=["bom_no", "qty"],
+ order_by="idx asc",
+ )
+
+ for row in bom_items:
+ if not row.bom_no:
+ continue
+
+ qty = flt(row.qty) * flt(qty)
+ items = get_bom_items_as_dict(
+ row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
+ )
+ scrap_items.update(items)
+
+ get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items)
+
+ return scrap_items
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index bd010d9f9a6..1709a1f71af 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -251,7 +251,7 @@ class BOMCreator(Document):
frappe.msgprint(_("BOMs created successfully"))
except Exception:
- traceback = frappe.get_traceback()
+ traceback = frappe.get_traceback(with_context=True)
self.db_set(
{
"status": "Failed",
diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
index c75ac32cd12..27ecd57b873 100644
--- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
+++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
@@ -37,7 +37,8 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "item_name",
@@ -170,7 +171,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:35:40.856895",
+ "modified": "2024-01-02 13:49:36.211586",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Explosion Item",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index d696cc4082f..23650b68736 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -273,35 +273,39 @@ class JobCard(Document):
def has_overlap(self, production_capacity, time_logs):
overlap = False
- if production_capacity == 1 and len(time_logs) > 0:
+ if production_capacity == 1 and len(time_logs) >= 1:
return True
+ if not len(time_logs):
+ return False
- # Check overlap exists or not between the overlapping time logs with the current Job Card
- for row in time_logs:
- count = 1
- for next_row in time_logs:
- if row.name == next_row.name:
- continue
-
- if (
- (
- get_datetime(next_row.from_time) >= get_datetime(row.from_time)
- and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
- )
- or (
- get_datetime(next_row.to_time) >= get_datetime(row.from_time)
- and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
- )
- or (
- get_datetime(next_row.from_time) <= get_datetime(row.from_time)
- and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
- )
- ):
- count += 1
-
- if count > production_capacity:
- return True
-
+ # sorting overlapping job cards as per from_time
+ time_logs = sorted(time_logs, key=lambda x: x.get("from_time"))
+ # alloted_capacity has key number starting from 1. Key number will increment by 1 if non sequential job card found
+ # if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
+ # this will store last to_time of sequential job cards
+ alloted_capacity = {1: time_logs[0]["to_time"]}
+ # flag for sequential Job card found
+ sequential_job_card_found = False
+ for i in range(1, len(time_logs)):
+ # scanning for all Existing keys
+ for key in alloted_capacity.keys():
+ # if current Job Card from time is greater than last to_time in that key means these job card are sequential
+ if alloted_capacity[key] <= time_logs[i]["from_time"]:
+ # So update key's value with last to_time
+ alloted_capacity[key] = time_logs[i]["to_time"]
+ # flag is true as we get sequential Job Card for that key
+ sequential_job_card_found = True
+ # Immediately break so that job card to time is not added with any other key except this
+ break
+ # if sequential job card not found above means it is overlapping so increment key number to alloted_capacity
+ if not sequential_job_card_found:
+ # increment key number
+ key = key + 1
+ # for that key last to time is assigned.
+ alloted_capacity[key] = time_logs[i]["to_time"]
+ if len(alloted_capacity) >= production_capacity:
+ # if number of keys greater or equal to production caoacity means full capacity is utilized and we should throw overlap error
+ return True
return overlap
def get_time_logs(self, args, doctype, check_next_available_slot=False):
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index 01647d56c91..d3ad51f7236 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -31,6 +31,7 @@
"job_card_excess_transfer",
"other_settings_section",
"update_bom_costs_automatically",
+ "set_op_cost_and_scrape_from_sub_assemblies",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
@@ -194,13 +195,20 @@
"fieldname": "job_card_excess_transfer",
"fieldtype": "Check",
"label": "Allow Excess Material Transfer"
+ },
+ {
+ "default": "0",
+ "description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",
+ "fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
+ "fieldtype": "Check",
+ "label": "Set Operating Cost / Scrape Items From Sub-assemblies"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-13 22:09:09.401559",
+ "modified": "2023-12-28 16:37:44.874096",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@@ -216,5 +224,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
index bfc8f4e9150..463ba9fe4bf 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
@@ -32,6 +32,7 @@ class ManufacturingSettings(Document):
mins_between_operations: DF.Int
overproduction_percentage_for_sales_order: DF.Percent
overproduction_percentage_for_work_order: DF.Percent
+ set_op_cost_and_scrape_from_sub_assemblies: DF.Check
update_bom_costs_automatically: DF.Check
# end: auto-generated types
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index cb99b8845a3..f6dfaa50586 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1602,6 +1602,10 @@ def make_bom(**args):
}
)
+ if args.operating_cost_per_bom_quantity:
+ bom.fg_based_operating_cost = 1
+ bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity
+
for item in args.raw_materials:
item_doc = frappe.get_doc("Item", item)
bom.append(
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 802c23d660a..19d8b9a1b2d 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -920,11 +920,9 @@ class TestWorkOrder(FrappeTestCase):
"Test RM Item 2 for Scrap Item Test",
]
- from_time = add_days(now(), -1)
job_cards = frappe.get_all(
"Job Card Time Log",
fields=["distinct parent as name", "docstatus"],
- filters={"from_time": (">", from_time)},
order_by="creation asc",
)
@@ -1731,6 +1729,93 @@ class TestWorkOrder(FrappeTestCase):
job_card2.time_logs = []
job_card2.save()
+ def test_op_cost_and_scrap_based_on_sub_assemblies(self):
+ # Make Sub Assembly BOM 1
+
+ frappe.db.set_single_value(
+ "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1
+ )
+
+ items = {
+ "Test Final FG Item": 0,
+ "Test Final SF Item 1": 0,
+ "Test Final SF Item 2": 0,
+ "Test Final RM Item 1": 100,
+ "Test Final RM Item 2": 200,
+ "Test Final Scrap Item 1": 50,
+ "Test Final Scrap Item 2": 60,
+ }
+
+ for item in items:
+ if not frappe.db.exists("Item", item):
+ item_properties = {"is_stock_item": 1, "valuation_rate": items[item]}
+
+ make_item(item_code=item, properties=item_properties),
+
+ prepare_boms_for_sub_assembly_test()
+
+ wo_order = make_wo_order_test_record(
+ production_item="Test Final FG Item",
+ qty=10,
+ use_multi_level_bom=1,
+ skip_transfer=1,
+ from_wip_warehouse=1,
+ )
+
+ se_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+ se_doc.save()
+
+ self.assertTrue(se_doc.additional_costs)
+ scrap_items = []
+ for item in se_doc.items:
+ if item.is_scrap_item:
+ scrap_items.append(item.item_code)
+
+ self.assertEqual(
+ sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])
+ )
+ for row in se_doc.additional_costs:
+ self.assertEqual(row.amount, 3000)
+
+ frappe.db.set_single_value(
+ "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
+ )
+
+
+def prepare_boms_for_sub_assembly_test():
+ if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
+ bom = make_bom(
+ item="Test Final SF Item 1",
+ source_warehouse="Stores - _TC",
+ raw_materials=["Test Final RM Item 1"],
+ operating_cost_per_bom_quantity=100,
+ do_not_submit=True,
+ )
+
+ bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1})
+
+ bom.submit()
+
+ if not frappe.db.exists("BOM", {"item": "Test Final SF Item 2"}):
+ bom = make_bom(
+ item="Test Final SF Item 2",
+ source_warehouse="Stores - _TC",
+ raw_materials=["Test Final RM Item 2"],
+ operating_cost_per_bom_quantity=200,
+ do_not_submit=True,
+ )
+
+ bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1})
+
+ bom.submit()
+
+ if not frappe.db.exists("BOM", {"item": "Test Final FG Item"}):
+ bom = make_bom(
+ item="Test Final FG Item",
+ source_warehouse="Stores - _TC",
+ raw_materials=["Test Final SF Item 1", "Test Final SF Item 2"],
+ )
+
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
@@ -1977,6 +2062,7 @@ def make_wo_order_test_record(**args):
wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
+ wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
if args.source_warehouse:
for item in wo_order.get("required_items"):
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index b0ea56833b7..866e94f95aa 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -843,7 +843,7 @@ erpnext.utils.map_current_doc = function(opts) {
freeze_message: __("Mapping {0} ...", [opts.source_doctype]),
callback: function(r) {
if(!r.exc) {
- var doc = frappe.model.sync(r.message);
+ frappe.model.sync(r.message);
cur_frm.dirty();
cur_frm.refresh();
}
@@ -870,6 +870,11 @@ erpnext.utils.map_current_doc = function(opts) {
target: opts.target,
date_field: opts.date_field || undefined,
setters: opts.setters,
+ data_fields: [{
+ fieldname: 'merge_taxes',
+ fieldtype: 'Check',
+ label: __('Merge taxes from multiple documents'),
+ }],
get_query: opts.get_query,
add_filters_group: 1,
allow_child_item_selection: opts.allow_child_item_selection,
@@ -883,10 +888,7 @@ erpnext.utils.map_current_doc = function(opts) {
return;
}
opts.source_name = values;
- if (opts.allow_child_item_selection) {
- // args contains filtered child docnames
- opts.args = args;
- }
+ opts.args = args;
d.dialog.hide();
_map();
},
diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py
index efeaeed324c..634a152147d 100644
--- a/erpnext/regional/united_arab_emirates/utils.py
+++ b/erpnext/regional/united_arab_emirates/utils.py
@@ -25,7 +25,7 @@ def update_itemised_tax_data(doc):
# dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate
item_code = row.item_code or row.item_name
if itemised_tax.get(item_code):
- for tax in itemised_tax.get(row.item_code).values():
+ for tax in itemised_tax.get(item_code).values():
_tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate"))
tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount"))
tax_rate += _tax_rate
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 31bbbcf51b8..db712d96b50 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -448,7 +448,6 @@
"report_hide": 1
},
{
- "default": "0",
"fieldname": "credit_limits",
"fieldtype": "Table",
"label": "Credit Limit",
@@ -584,7 +583,7 @@
"link_fieldname": "party"
}
],
- "modified": "2023-10-19 16:56:27.327035",
+ "modified": "2023-12-28 13:15:36.298369",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py
index 2da107e4e94..9a49af2b10e 100644
--- a/erpnext/setup/setup_wizard/setup_wizard.py
+++ b/erpnext/setup/setup_wizard/setup_wizard.py
@@ -37,11 +37,6 @@ def get_setup_stages(args=None):
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")},
],
},
- {
- "status": _("Setting up demo data"),
- "fail_msg": _("Failed to setup demo data"),
- "tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to setup demo data")}],
- },
{
"status": _("Wrapping up"),
"fail_msg": _("Failed to login"),
diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
index 7db8522f639..f71d21dd0b8 100644
--- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
+++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py
@@ -149,6 +149,4 @@ def prepare_closing_stock_balance(name):
doc.db_set("status", "Completed")
except Exception as e:
doc.db_set("status", "Failed")
- traceback = frappe.get_traceback()
-
- frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name)
+ doc.log_error(title="Closing Stock Balance Failed")
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 132f8f2e29f..7d7b0cd4769 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -431,6 +431,8 @@ class DeliveryNote(SellingController):
"Serial and Batch Bundle",
)
+ self.delete_auto_created_batches()
+
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 933be53b078..3abd1d9e5ed 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1478,6 +1478,46 @@ class TestDeliveryNote(FrappeTestCase):
returned_dn.reload()
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0)
+ def test_batch_with_non_stock_uom(self):
+ frappe.db.set_single_value(
+ "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1
+ )
+
+ item = make_item(
+ properties={
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TESTBATCH.#####",
+ "stock_uom": "Nos",
+ }
+ )
+ if not frappe.db.exists("UOM Conversion Detail", {"parent": item.name, "uom": "Kg"}):
+ item.append("uoms", {"uom": "Kg", "conversion_factor": 5.0})
+ item.save()
+
+ item_code = item.name
+
+ make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100.0)
+ dn = create_delivery_note(
+ item_code=item_code, qty=1, rate=500, warehouse="_Test Warehouse - _TC", do_not_save=True
+ )
+ dn.items[0].uom = "Kg"
+ dn.items[0].conversion_factor = 5.0
+
+ dn.save()
+ dn.submit()
+
+ self.assertEqual(dn.items[0].stock_qty, 5.0)
+ voucher_detail_no = dn.items[0].name
+ delivered_batch_qty = frappe.db.get_value(
+ "Serial and Batch Bundle", {"voucher_detail_no": voucher_detail_no}, "total_qty"
+ )
+ self.assertEqual(abs(delivered_batch_qty), 5.0)
+
+ frappe.db.set_single_value(
+ "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
+ )
+
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 23956ce0b7d..b7a64bb8b1a 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -1121,8 +1121,39 @@ def get_item_wise_returned_qty(pr_doc):
)
+def merge_taxes(source_taxes, target_doc):
+ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
+ update_item_wise_tax_detail,
+ )
+
+ existing_taxes = target_doc.get("taxes") or []
+ idx = 1
+ for tax in source_taxes:
+ found = False
+ for t in existing_taxes:
+ if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
+ t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
+ t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
+ update_item_wise_tax_detail(t, tax)
+ found = True
+
+ if not found:
+ tax.charge_type = "Actual"
+ tax.idx = idx
+ idx += 1
+ tax.included_in_print_rate = 0
+ tax.dont_recompute_tax = 1
+ tax.row_id = ""
+ tax.tax_amount = tax.tax_amount_after_discount_amount
+ tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
+ tax.item_wise_tax_detail = tax.item_wise_tax_detail
+ existing_taxes.append(tax)
+
+ target_doc.set("taxes", existing_taxes)
+
+
@frappe.whitelist()
-def make_purchase_invoice(source_name, target_doc=None):
+def make_purchase_invoice(source_name, target_doc=None, args=None):
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc("Purchase Receipt", source_name)
@@ -1139,6 +1170,10 @@ def make_purchase_invoice(source_name, target_doc=None):
)
doc.run_method("onload")
doc.run_method("set_missing_values")
+
+ if args and args.get("merge_taxes"):
+ merge_taxes(source.get("taxes") or [], doc)
+
doc.run_method("calculate_taxes_and_totals")
doc.set_payment_schedule()
@@ -1202,7 +1237,11 @@ def make_purchase_invoice(source_name, target_doc=None):
if not doc.get("is_return")
else get_pending_qty(d)[0] > 0,
},
- "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
+ "Purchase Taxes and Charges": {
+ "doctype": "Purchase Taxes and Charges",
+ "add_if_empty": True,
+ "ignore": args.get("merge_taxes") if args else 0,
+ },
},
target_doc,
set_missing_values,
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 97ada06e1dd..79b8ee30cfe 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -294,7 +294,7 @@ def repost(doc):
raise
frappe.db.rollback()
- traceback = frappe.get_traceback()
+ traceback = frappe.get_traceback(with_context=True)
doc.log_error("Unable to repost item valuation")
message = frappe.message_log.pop() if frappe.message_log else ""
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index a4fb5324ee6..218406f56fd 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -85,6 +85,7 @@ class SerialandBatchBundle(Document):
# end: auto-generated types
def validate(self):
+ self.reset_serial_batch_bundle()
self.set_batch_no()
self.validate_serial_and_batch_no()
self.validate_duplicate_serial_and_batch_no()
@@ -100,6 +101,15 @@ class SerialandBatchBundle(Document):
self.set_incoming_rate()
self.calculate_qty_and_amount()
+ def reset_serial_batch_bundle(self):
+ if self.is_new() and self.amended_from:
+ for field in ["is_cancelled", "is_rejected"]:
+ if self.get(field):
+ self.set(field, 0)
+
+ if self.voucher_detail_no:
+ self.voucher_detail_no = None
+
def set_batch_no(self):
if self.has_serial_no and self.has_batch_no:
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
@@ -483,7 +493,11 @@ class SerialandBatchBundle(Document):
if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]:
qty_field = "consumed_qty"
- if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
+ qty = row.get(qty_field)
+ if qty_field == "qty" and row.get("stock_qty"):
+ qty = row.get("stock_qty")
+
+ if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01:
self.throw_error_message(
f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}"
)
@@ -910,7 +924,11 @@ def upload_csv_file(item_code, file_path):
def get_serial_batch_from_csv(item_code, file_path):
- file_path = frappe.get_site_path() + file_path
+ if "private" in file_path:
+ file_path = frappe.get_site_path() + file_path
+ else:
+ file_path = frappe.get_site_path() + "/public" + file_path
+
serial_nos = []
batch_nos = []
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 478cfa4d1c7..19757479a5a 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -427,11 +427,12 @@ class TestSerialandBatchBundle(FrappeTestCase):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item = make_item(
+ "Test Serial and Batch Bundle Company Item",
properties={
"has_serial_no": 1,
"serial_no_series": "TT-SER-VAL-.#####",
- }
- )
+ },
+ ).name
pr = make_purchase_receipt(
item_code=item,
@@ -460,6 +461,26 @@ class TestSerialandBatchBundle(FrappeTestCase):
sn_doc = add_serial_batch_ledgers(entries, item_row, pr, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.company, "_Test Company")
+ def test_auto_cancel_serial_and_batch(self):
+ item_code = make_item(
+ properties={"has_serial_no": 1, "serial_no_series": "ATC-TT-SER-VAL-.#####"}
+ ).name
+
+ se = make_stock_entry(
+ item_code=item_code,
+ target="_Test Warehouse - _TC",
+ qty=5,
+ rate=500,
+ )
+
+ bundle = se.items[0].serial_and_batch_bundle
+ docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
+ self.assertEqual(docstatus, 1)
+
+ se.cancel()
+ docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
+ self.assertEqual(docstatus, 2)
+
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 2f1520f0d52..d35288a91cb 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -25,7 +25,12 @@ from frappe.utils import (
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
-from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no
+from erpnext.manufacturing.doctype.bom.bom import (
+ add_additional_cost,
+ get_op_cost_from_sub_assemblies,
+ get_scrap_items_from_sub_assemblies,
+ validate_bom_no,
+)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.batch.batch import get_batch_qty
@@ -1898,11 +1903,22 @@ class StockEntry(StockController):
def get_bom_scrap_material(self, qty):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
- # item dict = { item_code: {qty, description, stock_uom} }
- item_dict = (
- get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
- or {}
- )
+ if (
+ frappe.db.get_single_value(
+ "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
+ )
+ and self.work_order
+ and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
+ ):
+ item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty)
+ else:
+ # item dict = { item_code: {qty, description, stock_uom} }
+ item_dict = (
+ get_bom_items_as_dict(
+ self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
+ )
+ or {}
+ )
for item in item_dict.values():
item.from_warehouse = ""
@@ -2653,6 +2669,15 @@ def get_work_order_details(work_order, company):
def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0
if work_order:
+ if (
+ bom_no
+ and frappe.db.get_single_value(
+ "Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
+ )
+ and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom")
+ ):
+ return get_op_cost_from_sub_assemblies(bom_no)
+
if not bom_no:
bom_no = work_order.bom_no
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 907560826b3..a6f52f3731d 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -141,7 +141,7 @@ def create_material_request(material_requests):
exceptions_list.extend(frappe.local.message_log)
frappe.local.message_log = []
else:
- exceptions_list.append(frappe.get_traceback())
+ exceptions_list.append(frappe.get_traceback(with_context=True))
mr.log_error("Unable to create material request")
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index a1874b84dc7..39df2279cd2 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -242,6 +242,12 @@ class SerialBatchBundle:
if self.item_details.has_batch_no == 1:
self.update_batch_qty()
+ if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
+ self.cancel_serial_and_batch_bundle()
+
+ def cancel_serial_and_batch_bundle(self):
+ frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
+
def submit_serial_and_batch_bundle(self):
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
self.validate_actual_qty(doc)
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 05f153b4a0c..4d8990ae40b 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -195,7 +195,6 @@ class TestFIFOValuation(unittest.TestCase):
total_value -= sum(q * r for q, r in consumed)
self.assertTotalQty(total_qty)
self.assertTotalValue(total_value)
- self.assertGreaterEqual(total_value, 0)
class TestLIFOValuation(unittest.TestCase):
diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py
index df21b61139a..679d5bd348e 100644
--- a/erpnext/utilities/bulk_transaction.py
+++ b/erpnext/utilities/bulk_transaction.py
@@ -62,7 +62,7 @@ def retry_failed_transactions(failed_docs: list | None):
task(log.transaction_name, log.from_doctype, log.to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_state")
- update_log(log.name, "Failed", 1, str(frappe.get_traceback()))
+ update_log(log.name, "Failed", 1, str(frappe.get_traceback(with_context=True)))
else:
update_log(log.name, "Success", 1)
@@ -86,7 +86,7 @@ def job(deserialized_data, from_doctype, to_doctype):
fail_count += 1
create_log(
doc_name,
- str(frappe.get_traceback()),
+ str(frappe.get_traceback(with_context=True)),
from_doctype,
to_doctype,
status="Failed",