diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 411b90f84ee..ff44723afee 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -22,8 +22,7 @@ frappe.ui.form.on("Account", {
// hide fields if group
frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0);
- // disable fields
- frm.toggle_enable(["is_group", "company"], false);
+ frm.toggle_enable(["is_group", "company", "account_number"], frm.is_new());
if (cint(frm.doc.is_group) == 0) {
frm.toggle_display("freeze_account", frm.doc.__onload && frm.doc.__onload.can_freeze_account);
diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index a009a758a1b..97c1d4ae65a 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -55,8 +55,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
- "label": "Account Number",
- "read_only": 1
+ "label": "Account Number"
},
{
"default": "0",
@@ -74,7 +73,6 @@
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
- "read_only": 1,
"remember_last_selected_value": 1,
"reqd": 1
},
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json
index b6673795bea..d599ea65ea6 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/fr_plan_comptable_general_avec_code.json
@@ -1525,7 +1525,8 @@
"41-Clients et comptes rattach\u00e9s (PASSIF)": {
"Clients cr\u00e9diteurs": {
"Clients - Avances et acomptes re\u00e7us sur commandes": {
- "account_number": "4191"
+ "account_number": "4191",
+ "account_type": "Income Account"
},
"Clients - Dettes pour emballages et mat\u00e9riels consign\u00e9s": {
"account_number": "4196"
@@ -3141,4 +3142,4 @@
"account_number": "7"
}
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
index 230407ba5a4..8aa870f6dce 100644
--- a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
+++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py
@@ -68,6 +68,9 @@ class AutoMatchbyAccountIBAN:
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
+ if "bank_ac_no" in or_filters:
+ or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
+
if party_result:
result = (
party,
diff --git a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
index 784ad27dd47..34b2ec68743 100644
--- a/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
+++ b/erpnext/accounts/doctype/cost_center_allocation/cost_center_allocation.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import add_days, format_date, getdate
+from frappe.utils import add_days, flt, format_date, getdate
class MainCostCenterCantBeChild(frappe.ValidationError):
@@ -60,7 +60,7 @@ class CostCenterAllocation(Document):
self.validate_child_cost_centers()
def validate_total_allocation_percentage(self):
- total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
+ total_percentage = sum([flt(d.percentage) for d in self.get("allocation_percentages", [])])
if total_percentage != 100:
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 84d05465bfc..6a3254197cc 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -194,6 +194,7 @@ class JournalEntry(AccountsController):
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
+ self.update_booked_depreciation()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
@@ -225,6 +226,7 @@ class JournalEntry(AccountsController):
self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry()
self.update_invoice_discounting()
+ self.update_booked_depreciation()
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
@@ -442,6 +444,28 @@ class JournalEntry(AccountsController):
if status:
inv_disc_doc.set_status(status=status)
+ def update_booked_depreciation(self):
+ for d in self.get("accounts"):
+ if (
+ self.voucher_type == "Depreciation Entry"
+ and d.reference_type == "Asset"
+ and d.reference_name
+ and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
+ and d.debit
+ ):
+ asset = frappe.get_doc("Asset", d.reference_name)
+ for fb_row in asset.get("finance_books"):
+ if fb_row.finance_book == self.finance_book:
+ depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book)
+ total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations
+ for je in depr_schedule:
+ if je.journal_entry:
+ total_number_of_booked_depreciations += 1
+ fb_row.db_set(
+ "total_number_of_booked_depreciations", total_number_of_booked_depreciations
+ )
+ break
+
def unlink_advance_entry_reference(self):
for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
@@ -1034,6 +1058,17 @@ class JournalEntry(AccountsController):
def build_gl_map(self):
gl_map = []
+
+ company_currency = erpnext.get_company_currency(self.company)
+ if self.multi_currency:
+ for row in self.get("accounts"):
+ if row.account_currency != company_currency:
+ self.currency = row.account_currency
+ self.conversion_rate = row.exchange_rate
+ break
+ else:
+ self.currency = company_currency
+
for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index f911352c884..5983198928f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -89,6 +89,7 @@
"custom_remarks",
"remarks",
"base_in_words",
+ "is_opening",
"column_break_16",
"letter_head",
"print_heading",
@@ -778,6 +779,16 @@
"label": "Reconcile on Advance Payment Date",
"no_copy": 1,
"read_only": 1
+ },
+ {
+ "default": "No",
+ "depends_on": "eval: doc.book_advance_payments_in_separate_party_account == 1",
+ "fieldname": "is_opening",
+ "fieldtype": "Select",
+ "label": "Is Opening",
+ "options": "No\nYes",
+ "print_hide": 1,
+ "search_index": 1
}
],
"index_web_pages_for_search": 1,
@@ -791,7 +802,7 @@
"table_fieldname": "payment_entries"
}
],
- "modified": "2024-05-17 10:21:11.199445",
+ "modified": "2024-05-31 17:07:06.197249",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 4e05afa6be9..993074565b5 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -199,11 +199,13 @@ class PaymentEntry(AccountsController):
self.book_advance_payments_in_separate_party_account = False
if self.party_type not in ("Customer", "Supplier"):
+ self.is_opening = "No"
return
if not frappe.db.get_value(
"Company", self.company, "book_advance_payments_in_separate_party_account"
):
+ self.is_opening = "No"
return
# Important to set this flag for the gl building logic to work properly
@@ -215,6 +217,7 @@ class PaymentEntry(AccountsController):
if (account_type == "Payable" and self.party_type == "Customer") or (
account_type == "Receivable" and self.party_type == "Supplier"
):
+ self.is_opening = "No"
return
if self.references:
@@ -224,6 +227,7 @@ class PaymentEntry(AccountsController):
# If there are referencers other than `allowed_types`, treat this as a normal payment entry
if reference_types - allowed_types:
self.book_advance_payments_in_separate_party_account = False
+ self.is_opening = "No"
return
liability_account = get_party_account(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index bcf3ccbfe46..280ec79cc3f 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -1729,6 +1729,68 @@ class TestPaymentEntry(FrappeTestCase):
self.check_gl_entries()
self.check_pl_entries()
+ def test_opening_flag_for_advance_as_liability(self):
+ company = "_Test Company"
+
+ advance_account = create_account(
+ parent_account="Current Assets - _TC",
+ account_name="Advances Received",
+ company=company,
+ account_type="Receivable",
+ )
+
+ # Enable Advance in separate party account
+ frappe.db.set_value(
+ "Company",
+ company,
+ {
+ "book_advance_payments_in_separate_party_account": 1,
+ "default_advance_received_account": advance_account,
+ },
+ )
+ # Advance Payment
+ adv = create_payment_entry(
+ party_type="Customer",
+ party="_Test Customer",
+ payment_type="Receive",
+ paid_from="Debtors - _TC",
+ paid_to="_Test Cash - _TC",
+ )
+ adv.is_opening = "Yes"
+ adv.save() # use save() to trigger set_liability_account()
+ adv.submit()
+
+ gl_with_opening_set = frappe.db.get_all(
+ "GL Entry", filters={"voucher_no": adv.name, "is_opening": "Yes"}
+ )
+ # 'Is Opening' can be 'Yes' for Advances in separate party account
+ self.assertNotEqual(gl_with_opening_set, [])
+
+ # Disable Advance in separate party account
+ frappe.db.set_value(
+ "Company",
+ company,
+ {
+ "book_advance_payments_in_separate_party_account": 0,
+ "default_advance_received_account": None,
+ },
+ )
+ payment = create_payment_entry(
+ party_type="Customer",
+ party="_Test Customer",
+ payment_type="Receive",
+ paid_from="Debtors - _TC",
+ paid_to="_Test Cash - _TC",
+ )
+ payment.is_opening = "Yes"
+ payment.save()
+ payment.submit()
+ gl_with_opening_set = frappe.db.get_all(
+ "GL Entry", filters={"voucher_no": payment.name, "is_opening": "Yes"}
+ )
+ # 'Is Opening' should always be 'No' for normal advance payments
+ self.assertEqual(gl_with_opening_set, [])
+
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 932060895b0..3709bc6257d 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -2,6 +2,7 @@
# See license.txt
import unittest
+from unittest.mock import patch
import frappe
@@ -13,7 +14,12 @@ from erpnext.setup.utils import get_exchange_rate
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
-payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
+PAYMENT_URL = "https://example.com/payment"
+
+payment_gateways = [
+ {"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
+ {"doctype": "Payment Gateway", "gateway": "_Test Gateway Phone"},
+]
payment_method = [
{
@@ -29,13 +35,21 @@ payment_method = [
"payment_account": "_Test Bank USD - _TC",
"currency": "USD",
},
+ {
+ "doctype": "Payment Gateway Account",
+ "payment_gateway": "_Test Gateway Phone",
+ "payment_account": "_Test Bank USD - _TC",
+ "payment_channel": "Phone",
+ "currency": "USD",
+ },
]
class TestPaymentRequest(unittest.TestCase):
def setUp(self):
- if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
- frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
+ for payment_gateway in payment_gateways:
+ if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
+ frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
for method in payment_method:
if not frappe.db.get_value(
@@ -45,6 +59,25 @@ class TestPaymentRequest(unittest.TestCase):
):
frappe.get_doc(method).insert(ignore_permissions=True)
+ send_email = patch(
+ "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
+ return_value=None,
+ )
+ self.send_email = send_email.start()
+ self.addCleanup(send_email.stop)
+ get_payment_url = patch(
+ # this also shadows one (1) call to _get_payment_gateway_controller
+ "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
+ return_value=PAYMENT_URL,
+ )
+ self.get_payment_url = get_payment_url.start()
+ self.addCleanup(get_payment_url.stop)
+ _get_payment_gateway_controller = patch(
+ "erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
+ )
+ self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
+ self.addCleanup(_get_payment_gateway_controller.stop)
+
def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR", do_not_save=True)
so_inr.disable_rounded_total = 1
@@ -75,6 +108,83 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD")
+ def test_payment_channels(self):
+ so = make_sales_order(currency="USD")
+
+ pr = make_payment_request(
+ dt="Sales Order",
+ dn=so.name,
+ payment_gateway_account="_Test Gateway - USD", # email channel
+ submit_doc=False,
+ return_doc=True,
+ )
+ pr.flags.mute_email = True # but temporarily prohibit sending
+ pr.submit()
+ pr.reload()
+ self.assertEqual(pr.payment_channel, "Email")
+ self.assertEqual(pr.mute_email, False)
+
+ self.assertIsNone(pr.payment_url)
+ self.assertEqual(self.send_email.call_count, 0) # hence: no increment
+ self.assertEqual(self._get_payment_gateway_controller.call_count, 1)
+ pr.cancel()
+
+ pr = make_payment_request(
+ dt="Sales Order",
+ dn=so.name,
+ payment_gateway_account="_Test Gateway Phone - USD",
+ submit_doc=True,
+ return_doc=True,
+ )
+ pr.reload()
+
+ self.assertEqual(pr.payment_channel, "Phone")
+ self.assertEqual(pr.mute_email, False)
+
+ self.assertIsNone(pr.payment_url)
+ self.assertEqual(self.send_email.call_count, 0) # no increment on phone channel
+ self.assertEqual(self._get_payment_gateway_controller.call_count, 3)
+ pr.cancel()
+
+ pr = make_payment_request(
+ dt="Sales Order",
+ dn=so.name,
+ payment_gateway_account="_Test Gateway - USD", # email channel
+ submit_doc=True,
+ return_doc=True,
+ )
+ pr.reload()
+
+ self.assertEqual(pr.payment_channel, "Email")
+ self.assertEqual(pr.mute_email, False)
+
+ self.assertEqual(pr.payment_url, PAYMENT_URL)
+ self.assertEqual(self.send_email.call_count, 1) # increment on normal email channel
+ self.assertEqual(self._get_payment_gateway_controller.call_count, 4)
+ pr.cancel()
+
+ so = make_sales_order(currency="USD", do_not_save=True)
+ # no-op; for optical consistency with how a webshop SO would look like
+ so.order_type = "Shopping Cart"
+ so.save()
+ pr = make_payment_request(
+ dt="Sales Order",
+ dn=so.name,
+ payment_gateway_account="_Test Gateway - USD", # email channel
+ order_type="Shopping Cart",
+ submit_doc=True,
+ return_doc=True,
+ )
+ pr.reload()
+
+ self.assertEqual(pr.payment_channel, "Email")
+ self.assertEqual(pr.mute_email, False)
+
+ self.assertIsNone(pr.payment_url)
+ self.assertEqual(self.send_email.call_count, 1) # no increment on shopping cart
+ self.assertEqual(self._get_payment_gateway_controller.call_count, 5)
+ pr.cancel()
+
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index b158edaed5f..9faf8693a8a 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
- _(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
+ _("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 0ce2c1a4f25..9e6c518dfc0 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
+ self.make_bundle_for_sales_purchase_return()
self.submit_serial_batch_bundle()
if self.coupon_code:
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 773ef0103b6..9159f837269 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
pos.insert()
pos.submit()
+ pos.reload()
pos_return1 = make_sales_return(pos.name)
# partial return 1
pos_return1.get("items")[0].qty = -1
+ pos_return1.submit()
+ pos_return1.reload()
bundle_id = frappe.get_doc(
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
)
- bundle_id.remove(bundle_id.entries[1])
- bundle_id.save()
-
bundle_id.load_from_db()
serial_no = bundle_id.entries[0].serial_no
self.assertEqual(serial_no, serial_nos[0])
- pos_return1.insert()
- pos_return1.submit()
-
# partial return 2
pos_return2 = make_sales_return(pos.name)
+ pos_return2.submit()
+
self.assertEqual(pos_return2.get("items")[0].qty, -1)
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
self.assertEqual(serial_no, serial_nos[1])
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index b8548c1f4ce..e8f94b880e2 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
- _(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
+ _("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
)
if error_list:
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index b047898b771..7f9c55ff24f 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -929,6 +929,30 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]:
doc.delete()
+ def test_pricing_rule_for_transaction_with_condition(self):
+ make_item("PR Transaction Condition")
+ frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
+ make_pricing_rule(
+ selling=1,
+ min_qty=0,
+ price_or_product_discount="Product",
+ apply_on="Transaction",
+ free_item="PR Transaction Condition",
+ free_qty=1,
+ free_item_rate=10,
+ condition="customer=='_Test Customer 1'",
+ )
+
+ si = create_sales_invoice(qty=5, customer="_Test Customer 1", do_not_submit=True)
+ self.assertEqual(len(si.items), 2)
+ self.assertEqual(si.items[1].rate, 10)
+
+ si1 = create_sales_invoice(qty=5, customer="_Test Customer 2", do_not_submit=True)
+ self.assertEqual(len(si1.items), 1)
+
+ for doc in [si, si1]:
+ doc.delete()
+
def test_remove_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 32221b6d9a8..9bd417fa5b3 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -564,6 +564,7 @@ def apply_pricing_rule_on_transaction(doc):
if pricing_rules:
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, doc.total, pricing_rules)
+ pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
if not pricing_rules:
remove_free_item(doc)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 51b2e29f5e7..e4fe9e9a616 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -681,7 +681,7 @@ frappe.ui.form.on("Purchase Invoice", {
if (frm.doc.supplier) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
- if (!frm.doc.__onload.supplier_tds) {
+ if (!frm.doc.__onload.enable_apply_tds) {
frm.set_df_property("apply_tds", "read_only", 1);
}
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index cebd730e489..07b13ae8a1e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -348,6 +348,22 @@ class PurchaseInvoice(BuyingController):
self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", tds_category)
+ # If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
+ if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
+ po = qb.DocType("Purchase Order")
+ po_with_tds = (
+ qb.from_(po)
+ .select(po.name)
+ .where(
+ po.docstatus.eq(1)
+ & (po.name.isin(purchase_orders))
+ & (po.apply_tds.eq(1))
+ & (po.tax_withholding_category.notnull())
+ )
+ .run()
+ )
+ self.set_onload("enable_apply_tds", True if po_with_tds else False)
+
super().set_missing_values(for_validate)
def validate_credit_to_acc(self):
@@ -449,7 +465,7 @@ class PurchaseInvoice(BuyingController):
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
stock_items = self.get_stock_items()
- asset_received_but_not_billed = None
+ self.asset_received_but_not_billed = None
if self.update_stock:
self.validate_item_code()
@@ -532,26 +548,45 @@ class PurchaseInvoice(BuyingController):
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
- elif item.is_fixed_asset and item.pr_detail:
- if not asset_received_but_not_billed:
- asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
- item.expense_account = asset_received_but_not_billed
elif item.is_fixed_asset:
- account_type = (
- "capital_work_in_progress_account"
- if is_cwip_accounting_enabled(item.asset_category)
- else "fixed_asset_account"
- )
- asset_category_account = get_asset_category_account(
- account_type, item=item.item_code, company=self.company
- )
- if not asset_category_account:
- form_link = get_link_to_form("Asset Category", item.asset_category)
- throw(
- _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
- title=_("Missing Account"),
+ account = None
+ if item.pr_detail:
+ if not self.asset_received_but_not_billed:
+ self.asset_received_but_not_billed = self.get_company_default(
+ "asset_received_but_not_billed"
+ )
+
+ # check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
+ arbnb_booked_in_pr = frappe.db.get_value(
+ "GL Entry",
+ {
+ "voucher_type": "Purchase Receipt",
+ "voucher_no": item.purchase_receipt,
+ "account": self.asset_received_but_not_billed,
+ },
+ "name",
)
- item.expense_account = asset_category_account
+ if arbnb_booked_in_pr:
+ account = self.asset_received_but_not_billed
+
+ if not account:
+ account_type = (
+ "capital_work_in_progress_account"
+ if is_cwip_accounting_enabled(item.asset_category)
+ else "fixed_asset_account"
+ )
+ account = get_asset_category_account(
+ account_type, item=item.item_code, company=self.company
+ )
+ if not account:
+ form_link = get_link_to_form("Asset Category", item.asset_category)
+ throw(
+ _("Please set Fixed Asset Account in {} against {}.").format(
+ form_link, self.company
+ ),
+ title=_("Missing Account"),
+ )
+ item.expense_account = account
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -708,6 +743,7 @@ class PurchaseInvoice(BuyingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
+ self.make_bundle_for_sales_purchase_return()
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
index 88e1e96569f..818b0e38fe1 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
+++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
@@ -53,13 +53,15 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-05-23 17:00:42.984798",
+ "modified": "2024-06-03 17:30:37.012593",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger",
"owner": "Administrator",
"permissions": [
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -68,7 +70,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
}
],
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
index 7cf5284d0ba..2ad0cf27625 100644
--- a/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
+++ b/erpnext/accounts/doctype/repost_accounting_ledger_settings/repost_accounting_ledger_settings.json
@@ -17,7 +17,7 @@
"in_create": 1,
"issingle": 1,
"links": [],
- "modified": "2024-03-27 13:10:32.287007",
+ "modified": "2024-06-06 13:56:37.908879",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
@@ -30,13 +30,17 @@
"print": 1,
"read": 1,
"role": "Administrator",
+ "select": 1,
"share": 1,
"write": 1
},
{
+ "create": 1,
+ "delete": 1,
"read": 1,
"role": "System Manager",
- "select": 1
+ "select": 1,
+ "write": 1
}
],
"sort_field": "creation",
diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
index 54df45ad03e..ef29b098ad3 100644
--- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
+++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json
@@ -98,13 +98,15 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-05-23 17:00:31.540640",
+ "modified": "2024-06-03 17:31:04.472279",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
"owner": "Administrator",
"permissions": [
{
+ "amend": 1,
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -113,7 +115,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
},
{
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index a13d982be7e..48f28c59fb0 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -455,6 +455,7 @@ class SalesInvoice(SellingController):
if not self.get(table_name):
continue
+ self.make_bundle_for_sales_purchase_return(table_name)
self.make_bundle_using_old_serial_batch_fields(table_name)
self.update_stock_ledger()
@@ -2643,6 +2644,10 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language")
+ # update outstanding
+ if source.payment_schedule and len(source.payment_schedule) == 1:
+ target.overdue_payments[0].outstanding = source.get("outstanding_amount")
+
target.validate()
return get_mapped_doc(
diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py
index 628aca5bfc9..b98ae11cdd7 100644
--- a/erpnext/accounts/report/account_balance/account_balance.py
+++ b/erpnext/accounts/report/account_balance/account_balance.py
@@ -49,7 +49,6 @@ def get_conditions(filters):
if filters.account_type:
conditions["account_type"] = filters.account_type
- return conditions
if filters.company:
conditions["company"] = filters.company
diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
index d285f28d8e3..e1545bdcd87 100644
--- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
+++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _
+from frappe.query_builder import DocType
from frappe.utils import cstr, flt
@@ -75,11 +76,24 @@ def get_data(filters):
asset_data = assets_details.get(d.against_voucher)
if asset_data:
if not asset_data.get("accumulated_depreciation_amount"):
- asset_data.accumulated_depreciation_amount = d.debit + asset_data.get(
- "opening_accumulated_depreciation"
- )
+ AssetDepreciationSchedule = DocType("Asset Depreciation Schedule")
+ DepreciationSchedule = DocType("Depreciation Schedule")
+ query = (
+ frappe.qb.from_(DepreciationSchedule)
+ .join(AssetDepreciationSchedule)
+ .on(DepreciationSchedule.parent == AssetDepreciationSchedule.name)
+ .select(DepreciationSchedule.accumulated_depreciation_amount)
+ .where(
+ (AssetDepreciationSchedule.asset == d.against_voucher)
+ & (DepreciationSchedule.parenttype == "Asset Depreciation Schedule")
+ & (DepreciationSchedule.schedule_date == d.posting_date)
+ )
+ ).run(as_dict=True)
+ asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
+
else:
asset_data.accumulated_depreciation_amount += d.debit
+ asset_data.opening_accumulated_depreciation = asset_data.accumulated_depreciation_amount - d.debit
row = frappe._dict(asset_data)
row.update(
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 4fa485f54b3..f9a008ade7f 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
@@ -69,48 +69,50 @@ def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
+ # nosemgrep
return frappe.db.sql(
f"""
- SELECT asset_category,
- 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
+ SELECT a.asset_category,
+ ifnull(sum(case when a.purchase_date < %(from_date)s then
+ case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
+ a.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
+ ifnull(sum(case when a.purchase_date >= %(from_date)s then
+ a.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
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
+ and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s then
+ case when a.status = "Sold" then
+ a.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
+ ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
+ and a.disposal_date >= %(from_date)s
+ and a.disposal_date <= %(to_date)s then
+ case when a.status = "Scrapped" then
+ a.gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
- from `tabAsset`
+ from `tabAsset` a
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
- group by asset_category
+ and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name)
+ group by a.asset_category
""",
{
"to_date": filters.to_date,
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py
index b2e55b66e39..ccadaac3ca2 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.py
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py
@@ -109,7 +109,7 @@ def get_provisional_profit_loss(
):
provisional_profit_loss = {}
total_row = {}
- if asset and (liability or equity):
+ if asset:
total = total_row_total = 0
currency = currency or frappe.get_cached_value("Company", company, "default_currency")
total_row = {
@@ -122,14 +122,20 @@ def get_provisional_profit_loss(
for period in period_list:
key = period if consolidated else period.key
- effective_liability = 0.0
- if liability:
- effective_liability += flt(liability[0].get(key))
- if equity:
- effective_liability += flt(equity[0].get(key))
+ total_assets = flt(asset[0].get(key))
- provisional_profit_loss[key] = flt(asset[0].get(key)) - effective_liability
- total_row[key] = effective_liability + provisional_profit_loss[key]
+ if liability or equity:
+ effective_liability = 0.0
+ if liability:
+ effective_liability += flt(liability[0].get(key))
+ if equity:
+ effective_liability += flt(equity[0].get(key))
+
+ provisional_profit_loss[key] = total_assets - effective_liability
+ else:
+ provisional_profit_loss[key] = total_assets
+
+ total_row[key] = provisional_profit_loss[key]
if provisional_profit_loss[key]:
has_value = True
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index e1b980387e1..8ce3c7f424c 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -421,6 +421,8 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
if filters.get("show_net_values_in_party_account"):
account_type_map = get_account_type_map(filters.get("company"))
+ immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
+
def update_value_in_dict(data, key, gle):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -485,12 +487,17 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif group_by_voucher_consolidated:
keylist = [
+ gle.get("posting_date"),
gle.get("voucher_type"),
gle.get("voucher_no"),
gle.get("account"),
gle.get("party_type"),
gle.get("party"),
]
+
+ if immutable_ledger:
+ keylist.append(gle.get("creation"))
+
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index 4e7ab1bde6e..80c246cad55 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -5,14 +5,15 @@
import frappe
from frappe import _
from frappe.utils import flt
+from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
add_sub_total_row,
add_total_row,
+ apply_group_by_conditions,
get_grand_total,
get_group_by_and_display_fields,
- get_group_by_conditions,
get_tax_accounts,
)
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
@@ -29,7 +30,7 @@ def _execute(filters=None, additional_table_columns=None):
company_currency = erpnext.get_company_currency(filters.company)
- item_list = get_items(filters, get_query_columns(additional_table_columns))
+ item_list = get_items(filters, additional_table_columns)
aii_account_map = get_aii_accounts()
if item_list:
itemised_tax, tax_columns = get_tax_accounts(
@@ -287,59 +288,87 @@ def get_columns(additional_table_columns, filters):
return columns
-def get_conditions(filters):
- conditions = ""
+def apply_conditions(query, pi, pii, filters):
+ for opts in ("company", "supplier", "item_code", "mode_of_payment"):
+ if filters.get(opts):
+ query = query.where(pi[opts] == filters[opts])
- for opts in (
- ("company", " and `tabPurchase Invoice`.company=%(company)s"),
- ("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"),
- ("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"),
- ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"),
- ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"),
- ("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"),
- ("item_group", " and ifnull(`tabPurchase Invoice Item`.item_group, '') = %(item_group)s"),
- ):
- if filters.get(opts[0]):
- conditions += opts[1]
+ if filters.get("from_date"):
+ query = query.where(pi.posting_date >= filters.get("from_date"))
+
+ if filters.get("to_date"):
+ query = query.where(pi.posting_date <= filters.get("to_date"))
+
+ if filters.get("item_group"):
+ query = query.where(pii.item_group == filters.get("item_group"))
if not filters.get("group_by"):
- conditions += (
- "ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc"
- )
+ query = query.orderby(pi.posting_date, order=Order.desc)
+ query = query.orderby(pii.item_group, order=Order.desc)
else:
- conditions += get_group_by_conditions(filters, "Purchase Invoice")
+ query = apply_group_by_conditions(filters, "Purchase Invoice")
- return conditions
+ return query
-def get_items(filters, additional_query_columns):
- conditions = get_conditions(filters)
- if additional_query_columns:
- additional_query_columns = "," + ",".join(additional_query_columns)
- return frappe.db.sql(
- f"""
- select
- `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
- `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
- `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
- `tabPurchase Invoice`.unrealized_profit_loss_account,
- `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, `tabPurchase Invoice Item`.`item_group`,
- `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
- `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
- `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
- `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
- `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
- `tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
- `tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {additional_query_columns}
- from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
- where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
- `tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
- `tabPurchase Invoice`.docstatus = 1 {conditions}
- """,
- filters,
- as_dict=1,
+def get_items(filters, additional_table_columns):
+ pi = frappe.qb.DocType("Purchase Invoice")
+ pii = frappe.qb.DocType("Purchase Invoice Item")
+ Item = frappe.qb.DocType("Item")
+ query = (
+ frappe.qb.from_(pi)
+ .join(pii)
+ .on(pi.name == pii.parent)
+ .left_join(Item)
+ .on(pii.item_code == Item.name)
+ .select(
+ pii.name.as_("pii_name"),
+ pii.parent,
+ pi.posting_date,
+ pi.credit_to,
+ pi.company,
+ pi.supplier,
+ pi.remarks,
+ pi.base_net_total,
+ pi.unrealized_profit_loss_account,
+ pii.item_code,
+ pii.description,
+ pii.item_group,
+ pii.item_name.as_("pi_item_name"),
+ pii.item_group.as_("pi_item_group"),
+ Item.item_name.as_("i_item_name"),
+ Item.item_group.as_("i_item_group"),
+ pii.project,
+ pii.purchase_order,
+ pii.purchase_receipt,
+ pii.po_detail,
+ pii.expense_account,
+ pii.stock_qty,
+ pii.stock_uom,
+ pii.base_net_amount,
+ pi.supplier_name,
+ pi.mode_of_payment,
+ )
+ .where(pi.docstatus == 1)
)
+ if filters.get("supplier"):
+ query = query.where(pi.supplier == filters["supplier"])
+ if filters.get("company"):
+ query = query.where(pi.company == filters["company"])
+
+ if additional_table_columns:
+ for column in additional_table_columns:
+ if column.get("_doctype"):
+ table = frappe.qb.DocType(column.get("_doctype"))
+ query = query.select(table[column.get("fieldname")])
+ else:
+ query = query.select(pi[column.get("fieldname")])
+
+ query = apply_conditions(query, pi, pii, filters)
+
+ return query.run(as_dict=True)
+
def get_aii_accounts():
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js
index 193e17597db..de3976bdcb5 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.js
@@ -41,6 +41,12 @@ frappe.query_reports["Item-wise Sales Register"] = {
label: __("Warehouse"),
fieldtype: "Link",
options: "Warehouse",
+ get_query: function () {
+ const company = frappe.query_report.get_filter_value("company");
+ return {
+ filters: { company: company },
+ };
+ },
},
{
fieldname: "brand",
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index ce04af1e79d..cd50b118715 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -7,6 +7,7 @@ from frappe import _
from frappe.model.meta import get_field_precision
from frappe.utils import cstr, flt
from frappe.utils.xlsxutils import handle_html
+from pypika import Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
@@ -26,7 +27,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
- item_list = get_items(filters, get_query_columns(additional_table_columns), additional_conditions)
+ item_list = get_items(filters, additional_table_columns, additional_conditions)
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
@@ -340,101 +341,140 @@ def get_columns(additional_table_columns, filters):
return columns
-def get_conditions(filters, additional_conditions=None):
- conditions = ""
+def apply_conditions(query, si, sii, filters, additional_conditions=None):
+ for opts in ("company", "customer", "item_code"):
+ if filters.get(opts):
+ query = query.where(si[opts] == filters[opts])
- for opts in (
- ("company", " and `tabSales Invoice`.company=%(company)s"),
- ("customer", " and `tabSales Invoice`.customer = %(customer)s"),
- ("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"),
- ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"),
- ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"),
- ):
- if filters.get(opts[0]):
- conditions += opts[1]
+ if filters.get("from_date"):
+ query = query.where(si.posting_date >= filters.get("from_date"))
- if additional_conditions:
- conditions += additional_conditions
+ if filters.get("to_date"):
+ query = query.where(si.posting_date <= filters.get("to_date"))
if filters.get("mode_of_payment"):
- conditions += """ and exists(select name from `tabSales Invoice Payment`
- where parent=`tabSales Invoice`.name
- and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
+ sales_invoice = frappe.db.get_all(
+ "Sales Invoice Payment", {"mode_of_payment": filters.get("mode_of_payment")}, pluck="parent"
+ )
+ query = query.where(si.name.isin(sales_invoice))
if filters.get("warehouse"):
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
lft, rgt = frappe.db.get_all(
"Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True
)[0]
- conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) "
+ warehouses = frappe.db.get_all("Warehouse", {"lft": (">", lft), "rgt": ("<", rgt)}, pluck="name")
+ query = query.where(sii.warehouse.isin(warehouses))
else:
- conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s"""
+ query = query.where(sii.warehouse == filters.get("warehouse"))
if filters.get("brand"):
- conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s"""
+ query = query.where(sii.brand == filters.get("brand"))
if filters.get("item_group"):
- conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s"""
+ query = query.where(sii.item_group == filters.get("item_group"))
if filters.get("income_account"):
- conditions += """
- and (ifnull(`tabSales Invoice Item`.income_account, '') = %(income_account)s
- or ifnull(`tabSales Invoice Item`.deferred_revenue_account, '') = %(income_account)s
- or ifnull(`tabSales Invoice`.unrealized_profit_loss_account, '') = %(income_account)s)
- """
+ query = query.where(
+ (sii.income_account == filters.get("income_account"))
+ | (sii.deferred_revenue_account == filters.get("income_account"))
+ | (si.unrealized_profit_loss_account == filters.get("income_account"))
+ )
if not filters.get("group_by"):
- conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc"
+ query = query.orderby(si.posting_date, order=Order.desc)
+ query = query.orderby(sii.item_group, order=Order.desc)
else:
- conditions += get_group_by_conditions(filters, "Sales Invoice")
+ query = apply_group_by_conditions(query, si, sii, filters)
- return conditions
+ for key, value in (additional_conditions or {}).items():
+ query = query.where(si[key] == value)
+
+ return query
-def get_group_by_conditions(filters, doctype):
+def apply_group_by_conditions(query, si, ii, filters):
if filters.get("group_by") == "Invoice":
- return f"ORDER BY `tab{doctype} Item`.parent desc"
+ query = query.orderby(ii.parent, order=Order.desc)
elif filters.get("group_by") == "Item":
- return f"ORDER BY `tab{doctype} Item`.`item_code`"
+ query = query.orderby(ii.item_code)
elif filters.get("group_by") == "Item Group":
- return "ORDER BY `tab{} Item`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
+ query = query.orderby(ii.item_group)
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
- return "ORDER BY `tab{}`.{}".format(doctype, frappe.scrub(filters.get("group_by")))
+ query = query.orderby(si[frappe.scrub(filters.get("group_by"))])
+
+ return query
def get_items(filters, additional_query_columns, additional_conditions=None):
- conditions = get_conditions(filters, additional_conditions)
+ si = frappe.qb.DocType("Sales Invoice")
+ sii = frappe.qb.DocType("Sales Invoice Item")
+ item = frappe.qb.DocType("Item")
+
+ query = (
+ frappe.qb.from_(si)
+ .join(sii)
+ .on(si.name == sii.parent)
+ .left_join(item)
+ .on(sii.item_code == item.name)
+ .select(
+ sii.name,
+ sii.parent,
+ si.posting_date,
+ si.debit_to,
+ si.unrealized_profit_loss_account,
+ si.is_internal_customer,
+ si.customer,
+ si.remarks,
+ si.territory,
+ si.company,
+ si.base_net_total,
+ sii.project,
+ sii.item_code,
+ sii.description,
+ sii.item_name,
+ sii.item_group,
+ sii.item_name.as_("si_item_name"),
+ sii.item_group.as_("si_item_group"),
+ item.item_name.as_("i_item_name"),
+ item.item_group.as_("i_item_group"),
+ sii.sales_order,
+ sii.delivery_note,
+ sii.income_account,
+ sii.cost_center,
+ sii.enable_deferred_revenue,
+ sii.deferred_revenue_account,
+ sii.stock_qty,
+ sii.stock_uom,
+ sii.base_net_rate,
+ sii.base_net_amount,
+ si.customer_name,
+ si.customer_group,
+ sii.so_detail,
+ si.update_stock,
+ sii.uom,
+ sii.qty,
+ )
+ .where(si.docstatus == 1)
+ )
+
if additional_query_columns:
- additional_query_columns = "," + ",".join(additional_query_columns)
- return frappe.db.sql(
- """
- select
- `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
- `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
- `tabSales Invoice`.unrealized_profit_loss_account,
- `tabSales Invoice`.is_internal_customer,
- `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
- `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
- `tabSales Invoice Item`.project,
- `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
- `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
- `tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
- `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
- `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
- `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
- `tabSales Invoice Item`.enable_deferred_revenue, `tabSales Invoice Item`.deferred_revenue_account,
- `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
- `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
- `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
- `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {}
- from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
- where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
- `tabItem`.name = `tabSales Invoice Item`.`item_code` and
- `tabSales Invoice`.docstatus = 1 {}
- """.format(additional_query_columns, conditions),
- filters,
- as_dict=1,
- ) # nosec
+ for column in additional_query_columns:
+ if column.get("_doctype"):
+ table = frappe.qb.DocType(column.get("_doctype"))
+ query = query.select(table[column.get("fieldname")])
+ else:
+ query = query.select(si[column.get("fieldname")])
+
+ if filters.get("customer"):
+ query = query.where(si.customer == filters["customer"])
+
+ if filters.get("customer_group"):
+ query = query.where(si.customer_group == filters["customer_group"])
+
+ query = apply_conditions(query, si, sii, filters, additional_conditions)
+
+ return query.run(as_dict=True)
def get_delivery_notes_against_sales_order(item_list):
@@ -442,16 +482,14 @@ def get_delivery_notes_against_sales_order(item_list):
so_item_rows = list(set([d.so_detail for d in item_list]))
if so_item_rows:
- delivery_notes = frappe.db.sql(
- """
- select parent, so_detail
- from `tabDelivery Note Item`
- where docstatus=1 and so_detail in (%s)
- group by so_detail, parent
- """
- % (", ".join(["%s"] * len(so_item_rows))),
- tuple(so_item_rows),
- as_dict=1,
+ dn_item = frappe.qb.DocType("Delivery Note Item")
+ delivery_notes = (
+ frappe.qb.from_(dn_item)
+ .select(dn_item.parent, dn_item.so_detail)
+ .where(dn_item.docstatus == 1)
+ .where(dn_item.so_detail.isin(so_item_rows))
+ .groupby(dn_item.so_detail, dn_item.parent)
+ .run(as_dict=True)
)
for dn in delivery_notes:
@@ -461,15 +499,16 @@ def get_delivery_notes_against_sales_order(item_list):
def get_grand_total(filters, doctype):
- return frappe.db.sql(
- f""" SELECT
- SUM(`tab{doctype}`.base_grand_total)
- FROM `tab{doctype}`
- WHERE `tab{doctype}`.docstatus = 1
- and posting_date between %s and %s
- """,
- (filters.get("from_date"), filters.get("to_date")),
- )[0][0] # nosec
+ return flt(
+ frappe.db.get_value(
+ doctype,
+ {
+ "docstatus": 1,
+ "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
+ },
+ "sum(base_grand_total)",
+ )
+ )
def get_tax_accounts(
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 5980ca13e02..587faaed14e 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -45,7 +45,7 @@
"calculate_depreciation",
"column_break_33",
"opening_accumulated_depreciation",
- "number_of_depreciations_booked",
+ "opening_number_of_booked_depreciations",
"is_fully_depreciated",
"section_break_36",
"finance_books",
@@ -257,12 +257,6 @@
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency"
},
- {
- "depends_on": "eval:(doc.is_existing_asset)",
- "fieldname": "number_of_depreciations_booked",
- "fieldtype": "Int",
- "label": "Number of Depreciations Booked"
- },
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
@@ -546,6 +540,12 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "eval:(doc.is_existing_asset)",
+ "fieldname": "opening_number_of_booked_depreciations",
+ "fieldtype": "Int",
+ "label": "Opening Number of Booked Depreciations"
}
],
"idx": 72,
@@ -589,7 +589,7 @@
"link_fieldname": "target_asset"
}
],
- "modified": "2024-04-18 16:45:47.306032",
+ "modified": "2024-05-21 13:46:21.066483",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 15ad6c4fb42..a5ca6ce1be8 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -89,8 +89,8 @@ class Asset(AccountsController):
maintenance_required: DF.Check
naming_series: DF.Literal["ACC-ASS-.YYYY.-"]
next_depreciation_date: DF.Date | None
- number_of_depreciations_booked: DF.Int
opening_accumulated_depreciation: DF.Currency
+ opening_number_of_booked_depreciations: DF.Int
policy_number: DF.Data | None
purchase_amount: DF.Currency
purchase_date: DF.Date | None
@@ -145,7 +145,7 @@ class Asset(AccountsController):
"Asset Depreciation Schedules created:
{0}
Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
-
+ self.set_total_booked_depreciations()
self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status()
@@ -419,7 +419,7 @@ class Asset(AccountsController):
if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0
- self.number_of_depreciations_booked = 0
+ self.opening_number_of_booked_depreciations = 0
else:
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
@@ -430,15 +430,15 @@ class Asset(AccountsController):
)
if self.opening_accumulated_depreciation:
- if not self.number_of_depreciations_booked:
- frappe.throw(_("Please set Number of Depreciations Booked"))
+ if not self.opening_number_of_booked_depreciations:
+ frappe.throw(_("Please set Opening Number of Booked Depreciations"))
else:
- self.number_of_depreciations_booked = 0
+ self.opening_number_of_booked_depreciations = 0
- if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
+ if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
frappe.throw(
_(
- "Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked"
+ "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
).format(row.idx),
title=_("Invalid Schedule"),
)
@@ -459,6 +459,17 @@ class Asset(AccountsController):
).format(row.idx)
)
+ def set_total_booked_depreciations(self):
+ # set value of total number of booked depreciations field
+ for fb_row in self.get("finance_books"):
+ total_number_of_booked_depreciations = self.opening_number_of_booked_depreciations
+ depr_schedule = get_depr_schedule(self.name, "Active", fb_row.finance_book)
+ if depr_schedule:
+ for je in depr_schedule:
+ if je.journal_entry:
+ total_number_of_booked_depreciations += 1
+ fb_row.db_set("total_number_of_booked_depreciations", total_number_of_booked_depreciations)
+
def validate_expected_value_after_useful_life(self):
for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
@@ -1139,6 +1150,8 @@ def create_new_asset_after_split(asset, split_qty):
for row in new_asset.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
+ if not current_asset_depr_schedule_doc:
+ continue
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index b6a4d912d63..f06abaa1202 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -323,6 +323,7 @@ def _make_journal_entry_for_depreciation(
if not je.meta.get_workflow():
je.submit()
+ asset.reload()
idx = cint(asset_depr_schedule_doc.finance_book_id)
row = asset.get("finance_books")[idx - 1]
row.value_after_depreciation -= depr_schedule.depreciation_amount
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 5c49c0a8451..f3ec6122d53 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -355,7 +355,7 @@ class TestAsset(AssetSetup):
purchase_date="2020-04-01",
expected_value_after_useful_life=0,
total_number_of_depreciations=5,
- number_of_depreciations_booked=2,
+ opening_number_of_booked_depreciations=2,
frequency_of_depreciation=12,
depreciation_start_date="2023-03-31",
opening_accumulated_depreciation=24000,
@@ -453,7 +453,7 @@ class TestAsset(AssetSetup):
purchase_date="2020-01-01",
expected_value_after_useful_life=0,
total_number_of_depreciations=6,
- number_of_depreciations_booked=1,
+ opening_number_of_booked_depreciations=1,
frequency_of_depreciation=10,
depreciation_start_date="2021-01-01",
opening_accumulated_depreciation=20000,
@@ -739,7 +739,7 @@ class TestDepreciationMethods(AssetSetup):
calculate_depreciation=1,
available_for_use_date="2030-06-06",
is_existing_asset=1,
- number_of_depreciations_booked=2,
+ opening_number_of_booked_depreciations=2,
opening_accumulated_depreciation=47095.89,
expected_value_after_useful_life=10000,
depreciation_start_date="2032-12-31",
@@ -789,7 +789,7 @@ class TestDepreciationMethods(AssetSetup):
available_for_use_date="2030-01-01",
is_existing_asset=1,
depreciation_method="Double Declining Balance",
- number_of_depreciations_booked=1,
+ opening_number_of_booked_depreciations=1,
opening_accumulated_depreciation=50000,
expected_value_after_useful_life=10000,
depreciation_start_date="2031-12-31",
@@ -1123,8 +1123,8 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
- def test_number_of_depreciations_booked(self):
- """Tests if an error is raised when number_of_depreciations_booked is not specified when opening_accumulated_depreciation is."""
+ def test_opening_booked_depreciations(self):
+ """Tests if an error is raised when opening_number_of_booked_depreciations is not specified when opening_accumulated_depreciation is."""
asset = create_asset(
item_code="Macbook Pro",
@@ -1140,9 +1140,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self):
- """Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
+ """Tests if an error is raised when opening_number_of_booked_depreciations >= total_number_of_depreciations."""
- # number_of_depreciations_booked > total_number_of_depreciations
+ # opening_number_of_booked_depreciations > total_number_of_depreciations
asset = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
@@ -1151,13 +1151,13 @@ class TestDepreciationBasics(AssetSetup):
expected_value_after_useful_life=10000,
depreciation_start_date="2020-07-01",
opening_accumulated_depreciation=10000,
- number_of_depreciations_booked=5,
+ opening_number_of_booked_depreciations=5,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, asset.save)
- # number_of_depreciations_booked = total_number_of_depreciations
+ # opening_number_of_booked_depreciations = total_number_of_depreciations
asset_2 = create_asset(
item_code="Macbook Pro",
calculate_depreciation=1,
@@ -1166,7 +1166,7 @@ class TestDepreciationBasics(AssetSetup):
expected_value_after_useful_life=10000,
depreciation_start_date="2020-07-01",
opening_accumulated_depreciation=10000,
- number_of_depreciations_booked=5,
+ opening_number_of_booked_depreciations=5,
do_not_save=1,
)
@@ -1502,7 +1502,7 @@ class TestDepreciationBasics(AssetSetup):
asset = create_asset(calculate_depreciation=1)
asset.opening_accumulated_depreciation = 2000
- asset.number_of_depreciations_booked = 1
+ asset.opening_number_of_booked_depreciations = 1
asset.finance_books[0].expected_value_after_useful_life = 100
asset.save()
@@ -1696,7 +1696,7 @@ def create_asset(**args):
"purchase_date": args.purchase_date or "2015-01-01",
"calculate_depreciation": args.calculate_depreciation or 0,
"opening_accumulated_depreciation": args.opening_accumulated_depreciation or 0,
- "number_of_depreciations_booked": args.number_of_depreciations_booked or 0,
+ "opening_number_of_booked_depreciations": args.opening_number_of_booked_depreciations or 0,
"gross_purchase_amount": args.gross_purchase_amount or 100000,
"purchase_amount": args.purchase_amount or 100000,
"maintenance_required": args.maintenance_required or 0,
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
index ffb06c7e276..76565cb4e38 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -13,7 +13,7 @@
"column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation",
- "number_of_depreciations_booked",
+ "opening_number_of_booked_depreciations",
"finance_book",
"finance_book_id",
"depreciation_details_section",
@@ -171,10 +171,10 @@
"read_only": 1
},
{
- "fieldname": "number_of_depreciations_booked",
+ "fieldname": "opening_number_of_booked_depreciations",
"fieldtype": "Int",
"hidden": 1,
- "label": "Number of Depreciations Booked",
+ "label": "Opening Number of Booked Depreciations",
"print_hide": 1,
"read_only": 1
},
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index f64e9123dc0..d9fc5b3dd47 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -50,7 +50,7 @@ class AssetDepreciationSchedule(Document):
gross_purchase_amount: DF.Currency
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
notes: DF.SmallText | None
- number_of_depreciations_booked: DF.Int
+ opening_number_of_booked_depreciations: DF.Int
opening_accumulated_depreciation: DF.Currency
rate_of_depreciation: DF.Percent
shift_based: DF.Check
@@ -161,7 +161,7 @@ class AssetDepreciationSchedule(Document):
return (
asset_doc.gross_purchase_amount != self.gross_purchase_amount
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
- or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
+ or asset_doc.opening_number_of_booked_depreciations != self.opening_number_of_booked_depreciations
)
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
@@ -194,7 +194,7 @@ class AssetDepreciationSchedule(Document):
self.finance_book = row.finance_book
self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
- self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
+ self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0
self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations
@@ -263,7 +263,7 @@ class AssetDepreciationSchedule(Document):
row.db_update()
final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
- self.number_of_depreciations_booked
+ self.opening_number_of_booked_depreciations
)
has_pro_rata = _check_is_pro_rata(asset_doc, row)
@@ -328,7 +328,7 @@ class AssetDepreciationSchedule(Document):
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
from_date = add_months(
getdate(asset_doc.available_for_use_date),
- (asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
+ (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
)
if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date
@@ -378,13 +378,16 @@ class AssetDepreciationSchedule(Document):
from_date = get_last_day(
add_months(
getdate(asset_doc.available_for_use_date),
- ((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
+ (
+ (self.opening_number_of_booked_depreciations - 1)
+ * row.frequency_of_depreciation
+ ),
)
)
else:
from_date = add_months(
getdate(add_days(asset_doc.available_for_use_date, -1)),
- (self.number_of_depreciations_booked * row.frequency_of_depreciation),
+ (self.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
)
depreciation_amount, days, months = _get_pro_rata_amt(
row,
@@ -400,7 +403,8 @@ class AssetDepreciationSchedule(Document):
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
asset_doc.available_for_use_date,
- (n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
+ (n + self.opening_number_of_booked_depreciations)
+ * cint(row.frequency_of_depreciation),
)
depreciation_amount_without_pro_rata = depreciation_amount
@@ -546,7 +550,7 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
- # otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
+ # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1
@@ -567,12 +571,12 @@ def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=Fa
if wdv_or_dd_non_yearly:
return add_months(
asset_doc.available_for_use_date,
- (asset_doc.number_of_depreciations_booked * 12),
+ (asset_doc.opening_number_of_booked_depreciations * 12),
)
else:
return add_months(
asset_doc.available_for_use_date,
- (asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
+ (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
)
@@ -678,7 +682,7 @@ def get_straight_line_or_manual_depr_amount(
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+ ) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
def get_daily_prorata_based_straight_line_depr(
@@ -704,7 +708,7 @@ def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
flt(asset.gross_purchase_amount)
- flt(asset.opening_accumulated_depreciation)
- flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+ ) / flt(row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations)
asset_shift_factors_map = get_asset_shift_factors_map()
shift = (
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
index 6e4966ac6cf..6009ac1496c 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
@@ -5,6 +5,9 @@ import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr
+from erpnext.assets.doctype.asset.depreciation import (
+ post_depreciation_entries,
+)
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
@@ -28,7 +31,7 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
- def test_daily_prorata_based_depr_on_sl_methond(self):
+ def test_daily_prorata_based_depr_on_sl_method(self):
asset = create_asset(
calculate_depreciation=1,
depreciation_method="Straight Line",
@@ -160,3 +163,35 @@ class TestAssetDepreciationSchedule(FrappeTestCase):
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
+
+ def test_update_total_number_of_booked_depreciations(self):
+ # check if updates total number of booked depreciations when depreciation gets booked
+ asset = create_asset(
+ item_code="Macbook Pro",
+ calculate_depreciation=1,
+ opening_accumulated_depreciation=2000,
+ opening_number_of_booked_depreciations=2,
+ depreciation_method="Straight Line",
+ available_for_use_date="2020-03-01",
+ depreciation_start_date="2020-03-31",
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=24,
+ submit=1,
+ )
+
+ post_depreciation_entries(date="2021-03-31")
+ asset.reload()
+ """
+ opening_number_of_booked_depreciations = 2
+ number_of_booked_depreciations till 2021-03-31 = 13
+ total_number_of_booked_depreciations = 15
+ """
+ self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 15)
+
+ # cancel depreciation entry
+ depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
+
+ frappe.get_doc("Journal Entry", depr_entry).cancel()
+ asset.reload()
+
+ self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index a18e3a6e0f3..927cf91594d 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -8,6 +8,7 @@
"finance_book",
"depreciation_method",
"total_number_of_depreciations",
+ "total_number_of_booked_depreciations",
"daily_prorata_based",
"shift_based",
"column_break_5",
@@ -104,12 +105,19 @@
"fieldname": "shift_based",
"fieldtype": "Check",
"label": "Depreciate based on shifts"
+ },
+ {
+ "default": "0",
+ "fieldname": "total_number_of_booked_depreciations",
+ "fieldtype": "Int",
+ "label": "Total Number of Booked Depreciations ",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:34.342264",
+ "modified": "2024-05-21 15:48:20.907250",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py
index f812a0816dd..d06d6355ec3 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py
@@ -28,6 +28,7 @@ class AssetFinanceBook(Document):
rate_of_depreciation: DF.Percent
salvage_value_percentage: DF.Percent
shift_based: DF.Check
+ total_number_of_booked_depreciations: DF.Int
total_number_of_depreciations: DF.Int
value_after_depreciation: DF.Currency
# end: auto-generated types
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 27542bc6de8..ccde836fe0d 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -377,7 +377,7 @@ class AssetRepair(AccountsController):
def calculate_last_schedule_date(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
- asset.number_of_depreciations_booked
+ asset.opening_number_of_booked_depreciations
)
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
@@ -410,7 +410,7 @@ class AssetRepair(AccountsController):
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
- asset.number_of_depreciations_booked
+ asset.opening_number_of_booked_depreciations
)
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index d27d5838186..03184d33def 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -159,7 +159,7 @@ def prepare_chart_data(data, filters):
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
filters_filter_based_on = "Date Range"
date_field = "purchase_date"
- filtered_data = [d for d in data if not d.get(date_field)]
+ filtered_data = [d for d in data if d.get(date_field)]
filters_from_date = min(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
filters_to_date = max(filtered_data, key=lambda a: a.get(date_field)).get(date_field)
else:
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 13f1f3b8757..97f35d8dd9b 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -369,7 +369,7 @@ class PurchaseOrder(BuyingController):
item.idx, item.fg_item
)
)
- elif not frappe.get_value("Item", item.fg_item, "default_bom"):
+ elif not item.bom and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(
item.idx, item.fg_item
@@ -906,12 +906,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
)
target_doc.populate_items_table()
+ source_doc = frappe.get_doc("Purchase Order", source_name)
if target_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = target_doc.set_warehouse
else:
- source_doc = frappe.get_doc("Purchase Order", source_name)
if source_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = source_doc.set_warehouse
@@ -919,6 +919,14 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
for idx, item in enumerate(target_doc.items):
item.warehouse = source_doc.items[idx].warehouse
+ for idx, item in enumerate(target_doc.items):
+ item.job_card = source_doc.items[idx].job_card
+ if not target_doc.supplier_warehouse:
+ # WIP warehouse is set as Supplier Warehouse in Job Card
+ target_doc.supplier_warehouse = frappe.get_cached_value(
+ "Job Card", item.job_card, "wip_warehouse"
+ )
+
return target_doc
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index bce7ed15b12..5f4c9f0fd43 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -110,7 +110,9 @@
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
- "page_break"
+ "page_break",
+ "column_break_pjyo",
+ "job_card"
],
"fields": [
{
@@ -909,13 +911,24 @@
{
"fieldname": "column_break_fyqr",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_pjyo",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "job_card",
+ "fieldtype": "Link",
+ "label": "Job Card",
+ "options": "Job Card",
+ "search_index": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:10:24.979325",
+ "modified": "2024-03-27 13:12:24.979325",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js
index 56684a8659b..9b193a34d83 100644
--- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js
+++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js
@@ -2,3 +2,10 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters);
+
+frappe.query_reports["Purchase Order Trends"]["filters"].push({
+ fieldname: "include_closed_orders",
+ label: __("Include Closed Orders"),
+ fieldtype: "Check",
+ default: 0,
+});
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 47646295b2e..f25735575b7 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -357,6 +357,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
doctype = "Batch"
meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields()
+ page_len = 30
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
@@ -427,6 +428,7 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
& (stock_ledger_entry.batch_no.isnotnull())
)
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse)
+ .having(Sum(stock_ledger_entry.actual_qty) != 0)
.offset(start)
.limit(page_len)
)
@@ -477,6 +479,7 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
)
.groupby(bundle.batch_no, bundle.warehouse)
+ .having(Sum(bundle.qty) != 0)
.offset(start)
.limit(page_len)
)
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 83f3410d8fb..f27af755e33 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
+from collections import defaultdict
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
-from frappe.utils import flt, format_datetime, get_datetime
+from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.rejected_warehouse = ""
target_doc.warehouse = source_doc.rejected_warehouse
target_doc.received_qty = target_doc.qty
+ target_doc.return_qty_from_rejected_warehouse = 1
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
- if source_doc.item_code:
+ if (
+ (source_doc.serial_no or source_doc.batch_no)
+ and not source_doc.serial_and_batch_bundle
+ and not source_doc.use_serial_batch_fields
+ ):
+ target_doc.set("use_serial_batch_fields", 1)
+
+ if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if not item_details.has_batch_no and not item_details.has_serial_no:
return
- if not target_doc.get("use_serial_batch_fields"):
- for qty_field in ["stock_qty", "rejected_qty"]:
- if not target_doc.get(qty_field):
- continue
-
- update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
- elif target_doc.get("use_serial_batch_fields"):
- update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
+ update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -839,3 +841,243 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
batches.update(get_batches_from_bundle(ids))
return batches
+
+
+def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
+ available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
+ if not available_dict:
+ frappe.throw(_("No Serial / Batches are available for return"))
+
+ return available_dict
+
+
+def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
+ _bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
+ if not _bundle_ids:
+ return frappe._dict({})
+
+ return get_serial_batches_based_on_bundle(field, _bundle_ids)
+
+
+def get_serial_batches_based_on_bundle(field, _bundle_ids):
+ available_dict = frappe._dict({})
+ batch_serial_nos = frappe.get_all(
+ "Serial and Batch Bundle",
+ fields=[
+ "`tabSerial and Batch Entry`.`serial_no`",
+ "`tabSerial and Batch Entry`.`batch_no`",
+ "`tabSerial and Batch Entry`.`qty`",
+ "`tabSerial and Batch Bundle`.`voucher_detail_no`",
+ "`tabSerial and Batch Bundle`.`voucher_type`",
+ "`tabSerial and Batch Bundle`.`voucher_no`",
+ ],
+ filters=[
+ ["Serial and Batch Bundle", "name", "in", _bundle_ids],
+ ["Serial and Batch Entry", "docstatus", "=", 1],
+ ],
+ order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
+ )
+
+ for row in batch_serial_nos:
+ key = row.voucher_detail_no
+ if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
+ key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
+
+ if key not in available_dict:
+ available_dict[key] = frappe._dict(
+ {"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
+ )
+
+ available_dict[key]["qty"] += row.qty
+
+ if row.serial_no:
+ available_dict[key]["serial_nos"][row.serial_no] += row.qty
+ elif row.batch_no:
+ available_dict[key]["batches"][row.batch_no] += row.qty
+
+ return available_dict
+
+
+def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
+ filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
+
+ pluck_field = "serial_and_batch_bundle"
+ if is_rejected:
+ del filters["serial_and_batch_bundle"]
+ filters["rejected_serial_and_batch_bundle"] = ("is", "set")
+ pluck_field = "rejected_serial_and_batch_bundle"
+
+ _bundle_ids = frappe.get_all(
+ doctype,
+ filters=filters,
+ pluck=pluck_field,
+ )
+
+ if not _bundle_ids:
+ return {}
+
+ del filters["name"]
+
+ filters[field] = ("in", reference_ids)
+
+ if not is_rejected:
+ _bundle_ids.extend(
+ frappe.get_all(
+ doctype,
+ filters=filters,
+ pluck="serial_and_batch_bundle",
+ )
+ )
+ else:
+ fields = [
+ "serial_and_batch_bundle",
+ ]
+
+ if is_rejected:
+ fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
+
+ del filters["rejected_serial_and_batch_bundle"]
+ data = frappe.get_all(
+ doctype,
+ fields=fields,
+ filters=filters,
+ )
+
+ for d in data:
+ if not d.get("serial_and_batch_bundle") and not d.get("rejected_serial_and_batch_bundle"):
+ continue
+
+ if is_rejected:
+ if d.get("return_qty_from_rejected_warehouse"):
+ _bundle_ids.append(d.get("serial_and_batch_bundle"))
+ else:
+ _bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
+ else:
+ _bundle_ids.append(d.get("serial_and_batch_bundle"))
+
+ return _bundle_ids
+
+
+def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
+ if not qty_field:
+ qty_field = "qty"
+
+ if not warehouse_field:
+ warehouse_field = "warehouse"
+
+ warehouse = row.get(warehouse_field)
+ qty = abs(row.get(qty_field))
+
+ filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
+
+ if data.serial_nos:
+ available_serial_nos = []
+ for serial_no, sn_qty in data.serial_nos.items():
+ if sn_qty != 0:
+ available_serial_nos.append(serial_no)
+
+ if available_serial_nos:
+ if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
+ available_serial_nos = get_available_serial_nos(available_serial_nos)
+
+ if len(available_serial_nos) > qty:
+ filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
+ else:
+ filterd_serial_batch["serial_nos"] = available_serial_nos
+
+ elif data.batches:
+ for batch_no, batch_qty in data.batches.items():
+ if parent_doc.get("is_internal_customer"):
+ batch_qty = batch_qty * -1
+
+ if batch_qty <= 0:
+ continue
+
+ if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
+ batch_qty = get_available_batch_qty(
+ parent_doc,
+ batch_no,
+ warehouse,
+ )
+
+ if batch_qty <= 0:
+ frappe.throw(
+ _("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
+ title=_("Batch Not Available for Return"),
+ )
+
+ if qty <= 0:
+ break
+
+ if batch_qty > qty:
+ filterd_serial_batch["batches"][batch_no] = qty
+ qty = 0
+ else:
+ filterd_serial_batch["batches"][batch_no] += batch_qty
+ qty -= batch_qty
+
+ return filterd_serial_batch
+
+
+def get_available_batch_qty(parent_doc, batch_no, warehouse):
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ return get_batch_qty(
+ batch_no,
+ warehouse,
+ posting_date=parent_doc.posting_date,
+ posting_time=parent_doc.posting_time,
+ for_stock_levels=True,
+ )
+
+
+def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None, qty_field=None):
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ type_of_transaction = "Outward"
+ if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
+ type_of_transaction = "Inward"
+
+ if not warehouse_field:
+ warehouse_field = "warehouse"
+
+ if not qty_field:
+ qty_field = "qty"
+
+ warehouse = child_doc.get(warehouse_field)
+ if parent_doc.get("is_internal_customer"):
+ warehouse = child_doc.get("target_warehouse")
+ type_of_transaction = "Outward"
+
+ if not child_doc.get(qty_field):
+ frappe.throw(
+ _("For the {0}, the quantity is required to make the return entry").format(
+ frappe.bold(child_doc.item_code)
+ )
+ )
+
+ cls_obj = SerialBatchCreation(
+ {
+ "type_of_transaction": type_of_transaction,
+ "item_code": child_doc.item_code,
+ "warehouse": warehouse,
+ "serial_nos": data.get("serial_nos"),
+ "batches": data.get("batches"),
+ "posting_date": parent_doc.posting_date,
+ "posting_time": parent_doc.posting_time,
+ "voucher_type": parent_doc.doctype,
+ "voucher_no": parent_doc.name,
+ "voucher_detail_no": child_doc.name,
+ "qty": child_doc.get(qty_field),
+ "company": parent_doc.company,
+ "do_not_submit": True,
+ }
+ ).make_serial_and_batch_bundle()
+
+ return cls_obj.name
+
+
+def get_available_serial_nos(serial_nos, warehouse):
+ return frappe.get_all(
+ "Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
+ )
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 524bbfcc4b1..77580a33af2 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
)
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
+from erpnext.controllers.sales_and_purchase_return import (
+ available_serial_batch_for_return,
+ filter_serial_batches,
+ make_serial_batch_bundle_for_return,
+)
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
@@ -217,6 +222,132 @@ class StockController(AccountsController):
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
self.create_serial_batch_bundle(bundle_details, row)
+ def make_bundle_for_sales_purchase_return(self, table_name=None):
+ if not self.get("is_return"):
+ return
+
+ if not table_name:
+ table_name = "items"
+
+ self.make_bundle_for_non_rejected_qty(table_name)
+
+ if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
+ self.make_bundle_for_rejected_qty(table_name)
+
+ def make_bundle_for_rejected_qty(self, table_name=None):
+ field, reference_ids = self.get_reference_ids(
+ table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
+ )
+
+ if not reference_ids:
+ return
+
+ child_doctype = self.doctype + " Item"
+ available_dict = available_serial_batch_for_return(
+ field, child_doctype, reference_ids, is_rejected=True
+ )
+
+ for row in self.get(table_name):
+ if data := available_dict.get(row.get(field)):
+ qty_field = "rejected_qty"
+ warehouse_field = "rejected_warehouse"
+ if row.get("return_qty_from_rejected_warehouse"):
+ qty_field = "qty"
+ warehouse_field = "warehouse"
+
+ if not data.get("qty"):
+ frappe.throw(
+ _("For the {0}, no stock is available for the return in the warehouse {1}.").format(
+ frappe.bold(row.item_code), row.get(warehouse_field)
+ )
+ )
+
+ data = filter_serial_batches(
+ self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
+ )
+ bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field, qty_field)
+ if row.get("return_qty_from_rejected_warehouse"):
+ row.db_set(
+ {
+ "serial_and_batch_bundle": bundle,
+ "batch_no": "",
+ "serial_no": "",
+ }
+ )
+ else:
+ row.db_set(
+ {
+ "rejected_serial_and_batch_bundle": bundle,
+ "batch_no": "",
+ "rejected_serial_no": "",
+ }
+ )
+
+ def make_bundle_for_non_rejected_qty(self, table_name):
+ field, reference_ids = self.get_reference_ids(table_name)
+ if not reference_ids:
+ return
+
+ child_doctype = self.doctype + " Item"
+ available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
+
+ for row in self.get(table_name):
+ if data := available_dict.get(row.get(field)):
+ data = filter_serial_batches(self, data, row)
+ bundle = make_serial_batch_bundle_for_return(data, row, self)
+ row.db_set(
+ {
+ "serial_and_batch_bundle": bundle,
+ "batch_no": "",
+ "serial_no": "",
+ }
+ )
+
+ def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
+ field = {
+ "Sales Invoice": "sales_invoice_item",
+ "Delivery Note": "dn_detail",
+ "Purchase Receipt": "purchase_receipt_item",
+ "Purchase Invoice": "purchase_invoice_item",
+ "POS Invoice": "pos_invoice_item",
+ }.get(self.doctype)
+
+ if not bundle_field:
+ bundle_field = "serial_and_batch_bundle"
+
+ if not qty_field:
+ qty_field = "qty"
+
+ reference_ids = []
+
+ for row in self.get(table_name):
+ if not self.is_serial_batch_item(row.item_code):
+ continue
+
+ if (
+ row.get(field)
+ and (
+ qty_field == "qty"
+ and not row.get("return_qty_from_rejected_warehouse")
+ or qty_field == "rejected_qty"
+ and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
+ )
+ and not row.get("use_serial_batch_fields")
+ and not row.get(bundle_field)
+ ):
+ reference_ids.append(row.get(field))
+
+ return field, reference_ids
+
+ @frappe.request_cache
+ def is_serial_batch_item(self, item_code) -> bool:
+ item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
+
+ if item_details.has_serial_no or item_details.has_batch_no:
+ return True
+
+ return False
+
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -591,6 +722,9 @@ class StockController(AccountsController):
row.db_set("rejected_serial_and_batch_bundle", None)
+ if row.get("current_serial_and_batch_bundle"):
+ row.db_set("current_serial_and_batch_bundle", None)
+
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
if not table_name:
table_name = "items"
@@ -611,35 +745,16 @@ class StockController(AccountsController):
def make_package_for_transfer(
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
):
- bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
-
- if not type_of_transaction:
- type_of_transaction = "Inward"
-
- bundle_doc = frappe.copy_doc(bundle_doc)
- bundle_doc.warehouse = warehouse
- bundle_doc.type_of_transaction = type_of_transaction
- bundle_doc.voucher_type = self.doctype
- bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
- bundle_doc.is_cancelled = 0
-
- for row in bundle_doc.entries:
- row.is_outward = 0
- row.qty = abs(row.qty)
- row.stock_value_difference = abs(row.stock_value_difference)
- if type_of_transaction == "Outward":
- row.qty *= -1
- row.stock_value_difference *= row.stock_value_difference
- row.is_outward = 1
-
- row.warehouse = warehouse
-
- bundle_doc.calculate_qty_and_amount()
- bundle_doc.flags.ignore_permissions = True
- bundle_doc.flags.ignore_validate = True
- bundle_doc.save(ignore_permissions=True)
-
- return bundle_doc.name
+ return make_bundle_for_material_transfer(
+ is_new=self.is_new(),
+ docstatus=self.docstatus,
+ voucher_type=self.doctype,
+ voucher_no=self.name,
+ serial_and_batch_bundle=serial_and_batch_bundle,
+ warehouse=warehouse,
+ type_of_transaction=type_of_transaction,
+ do_not_submit=do_not_submit,
+ )
def get_sl_entries(self, d, args):
sl_dict = frappe._dict(
@@ -1557,3 +1672,38 @@ def create_item_wise_repost_entries(
repost_entries.append(repost_entry)
return repost_entries
+
+
+def make_bundle_for_material_transfer(**kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
+
+ if not kwargs.type_of_transaction:
+ kwargs.type_of_transaction = "Inward"
+
+ bundle_doc = frappe.copy_doc(bundle_doc)
+ bundle_doc.warehouse = kwargs.warehouse
+ bundle_doc.type_of_transaction = kwargs.type_of_transaction
+ bundle_doc.voucher_type = kwargs.voucher_type
+ bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
+ bundle_doc.is_cancelled = 0
+
+ for row in bundle_doc.entries:
+ row.is_outward = 0
+ row.qty = abs(row.qty)
+ row.stock_value_difference = abs(row.stock_value_difference)
+ if kwargs.type_of_transaction == "Outward":
+ row.qty *= -1
+ row.stock_value_difference *= row.stock_value_difference
+ row.is_outward = 1
+
+ row.warehouse = kwargs.warehouse
+
+ bundle_doc.calculate_qty_and_amount()
+ bundle_doc.flags.ignore_permissions = True
+ bundle_doc.flags.ignore_validate = True
+ bundle_doc.save(ignore_permissions=True)
+
+ return bundle_doc.name
diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py
index 18fe7767c5d..7f07466b3bc 100644
--- a/erpnext/controllers/trends.py
+++ b/erpnext/controllers/trends.py
@@ -74,8 +74,10 @@ def get_data(filters, conditions):
if conditions["based_on_select"] in ["t1.project,", "t2.project,"]:
cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL"
- if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
- cond += " and t1.status != 'Closed'"
+
+ if not filters.get("include_closed_orders"):
+ if conditions.get("trans") in ["Sales Order", "Purchase Order"]:
+ cond += " and t1.status != 'Closed'"
if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer":
cond += " and t1.quotation_to = 'Customer'"
diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js
index 848d697461a..8e568a2babe 100644
--- a/erpnext/crm/doctype/lead/lead.js
+++ b/erpnext/crm/doctype/lead/lead.js
@@ -29,13 +29,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
- this.frm.add_custom_button(
- __("Opportunity"),
- function () {
- me.frm.trigger("make_opportunity");
- },
- __("Create")
- );
+ this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
if (!doc.__onload.linked_prospects.length) {
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
@@ -100,6 +94,91 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
});
}
+ async make_opportunity() {
+ let existing_prospect = (
+ await frappe.db.get_value(
+ "Prospect Lead",
+ {
+ lead: this.frm.doc.name,
+ },
+ "name",
+ null,
+ "Prospect"
+ )
+ ).message.name;
+
+ if (!existing_prospect) {
+ var fields = [
+ {
+ label: "Create Prospect",
+ fieldname: "create_prospect",
+ fieldtype: "Check",
+ default: 1,
+ },
+ {
+ label: "Prospect Name",
+ fieldname: "prospect_name",
+ fieldtype: "Data",
+ default: this.frm.doc.company_name,
+ depends_on: "create_prospect",
+ },
+ ];
+ }
+ let existing_contact = (
+ await frappe.db.get_value(
+ "Contact",
+ {
+ first_name: this.frm.doc.first_name || this.frm.doc.lead_name,
+ last_name: this.frm.doc.last_name,
+ },
+ "name"
+ )
+ ).message.name;
+
+ if (!existing_contact) {
+ fields.push({
+ label: "Create Contact",
+ fieldname: "create_contact",
+ fieldtype: "Check",
+ default: "1",
+ });
+ }
+
+ if (fields) {
+ var d = new frappe.ui.Dialog({
+ title: __("Create Opportunity"),
+ fields: fields,
+ primary_action: function () {
+ var data = d.get_values();
+ frappe.call({
+ method: "create_prospect_and_contact",
+ doc: this.frm.doc,
+ args: {
+ data: data,
+ },
+ freeze: true,
+ callback: function (r) {
+ if (!r.exc) {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.crm.doctype.lead.lead.make_opportunity",
+ frm: this.frm,
+ });
+ }
+ d.hide();
+ },
+ });
+ },
+ primary_action_label: __("Create"),
+ });
+ d.show();
+ } else {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.crm.doctype.lead.lead.make_opportunity",
+ frm: this.frm,
+ });
+ }
+ }
+
make_prospect() {
const me = this;
frappe.model.with_doctype("Prospect", function () {
@@ -151,90 +230,3 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
};
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
-
-frappe.ui.form.on("Lead", {
- make_opportunity: async function (frm) {
- let existing_prospect = (
- await frappe.db.get_value(
- "Prospect Lead",
- {
- lead: frm.doc.name,
- },
- "name",
- null,
- "Prospect"
- )
- ).message.name;
-
- if (!existing_prospect) {
- var fields = [
- {
- label: "Create Prospect",
- fieldname: "create_prospect",
- fieldtype: "Check",
- default: 1,
- },
- {
- label: "Prospect Name",
- fieldname: "prospect_name",
- fieldtype: "Data",
- default: frm.doc.company_name,
- depends_on: "create_prospect",
- },
- ];
- }
- let existing_contact = (
- await frappe.db.get_value(
- "Contact",
- {
- first_name: frm.doc.first_name || frm.doc.lead_name,
- last_name: frm.doc.last_name,
- },
- "name"
- )
- ).message.name;
-
- if (!existing_contact) {
- fields.push({
- label: "Create Contact",
- fieldname: "create_contact",
- fieldtype: "Check",
- default: "1",
- });
- }
-
- if (fields) {
- var d = new frappe.ui.Dialog({
- title: __("Create Opportunity"),
- fields: fields,
- primary_action: function () {
- var data = d.get_values();
- frappe.call({
- method: "create_prospect_and_contact",
- doc: frm.doc,
- args: {
- data: data,
- },
- freeze: true,
- callback: function (r) {
- if (!r.exc) {
- frappe.model.open_mapped_doc({
- method: "erpnext.crm.doctype.lead.lead.make_opportunity",
- frm: frm,
- });
- }
- d.hide();
- },
- });
- },
- primary_action_label: __("Create"),
- });
- d.show();
- } else {
- frappe.model.open_mapped_doc({
- method: "erpnext.crm.doctype.lead.lead.make_opportunity",
- frm: frm,
- });
- }
- },
-});
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index bddb331e70c..34fd1e91ba2 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -118,6 +118,8 @@ class Opportunity(TransactionBase, CRMNote):
self.title = self.customer_name
self.calculate_totals()
+
+ def on_update(self):
self.update_prospect()
def map_fields(self):
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 6267ee4d029..f2692d21bff 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -19,6 +19,21 @@ frappe.ui.form.on("BOM", {
};
});
+ frm.set_query("bom_no", "operations", function (doc, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ return {
+ query: "erpnext.controllers.queries.bom",
+ filters: {
+ currency: frm.doc.currency,
+ company: frm.doc.company,
+ item: row.finished_good,
+ is_active: 1,
+ docstatus: 1,
+ track_semi_finished_goods: 0,
+ },
+ };
+ });
+
frm.set_query("source_warehouse", "items", function () {
return {
filters: {
@@ -85,6 +100,27 @@ frappe.ui.form.on("BOM", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
+ default_source_warehouse(frm) {
+ if (frm.doc.default_source_warehouse) {
+ frm.doc.operations.forEach((d) => {
+ frappe.model.set_value(
+ d.doctype,
+ d.name,
+ "source_warehouse",
+ frm.doc.default_source_warehouse
+ );
+ });
+ }
+ },
+
+ default_target_warehouse(frm) {
+ if (frm.doc.default_source_warehouse) {
+ frm.doc.operations.forEach((d) => {
+ frappe.model.set_value(d.doctype, d.name, "fg_warehouse", frm.doc.default_target_warehouse);
+ });
+ }
+ },
+
refresh(frm) {
frm.toggle_enable("item", frm.doc.__islocal);
@@ -96,22 +132,35 @@ frappe.ui.form.on("BOM", {
});
if (!frm.is_new() && frm.doc.docstatus < 2) {
- frm.add_custom_button(__("Update Cost"), function () {
- frm.events.update_cost(frm, true);
- });
- frm.add_custom_button(__("Browse BOM"), function () {
- frappe.route_options = {
- bom: frm.doc.name,
- };
- frappe.set_route("Tree", "BOM");
- });
+ frm.add_custom_button(
+ __("Update Cost"),
+ function () {
+ frm.events.update_cost(frm, true);
+ },
+ __("Actions")
+ );
+
+ frm.add_custom_button(
+ __("Browse BOM"),
+ function () {
+ frappe.route_options = {
+ bom: frm.doc.name,
+ };
+ frappe.set_route("Tree", "BOM");
+ },
+ __("Actions")
+ );
}
if (!frm.is_new() && !frm.doc.docstatus == 0) {
- frm.add_custom_button(__("New Version"), function () {
- let new_bom = frappe.model.copy_doc(frm.doc);
- frappe.set_route("Form", "BOM", new_bom.name);
- });
+ frm.add_custom_button(
+ __("New Version"),
+ function () {
+ let new_bom = frappe.model.copy_doc(frm.doc);
+ frappe.set_route("Form", "BOM", new_bom.name);
+ },
+ __("Actions")
+ );
}
if (frm.doc.docstatus == 1) {
@@ -432,6 +481,28 @@ frappe.ui.form.on("BOM", {
},
});
+frappe.ui.form.on("BOM Operation", {
+ bom_no(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+
+ if (row.bom_no && row.finished_good) {
+ frappe.call({
+ method: "add_materials_from_bom",
+ doc: frm.doc,
+ args: {
+ finished_good: row.finished_good,
+ bom_no: row.bom_no,
+ operation_row_id: row.idx,
+ qty: row.finished_good_qty,
+ },
+ callback(r) {
+ refresh_field("items");
+ },
+ });
+ }
+ },
+});
+
erpnext.bom.BomController = class BomController extends erpnext.TransactionController {
conversion_rate(doc) {
if (this.frm.doc.currency === this.get_company_currency()) {
@@ -801,3 +872,88 @@ function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
__("Set Quantity")
);
}
+
+frappe.ui.form.on("BOM Operation", {
+ add_raw_materials(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ frm.events._prompt_for_raw_materials(frm, row);
+ },
+});
+
+frappe.ui.form.on("BOM", {
+ _prompt_for_raw_materials(frm, row) {
+ let fields = frm.events.get_fields_for_prompt(frm, row);
+ frm._bom_rm_dialog = new frappe.ui.Dialog({
+ title: __("Add Raw Materials"),
+ fields: fields,
+ primary_action_label: __("Add"),
+ primary_action: () => {
+ let values = frm._bom_rm_dialog.get_values();
+ if (values) {
+ frm.events._add_raw_materials(frm, values);
+ frm._bom_rm_dialog.hide();
+ }
+ },
+ });
+
+ frm._bom_rm_dialog.show();
+ },
+
+ get_fields_for_prompt(frm, row) {
+ return [
+ {
+ label: __("Raw Materials"),
+ fieldname: "items",
+ fieldtype: "Table",
+ reqd: 1,
+ fields: [
+ {
+ label: __("Item"),
+ fieldname: "item_code",
+ fieldtype: "Link",
+ options: "Item",
+ reqd: 1,
+ in_list_view: 1,
+ change() {
+ let doc = this.doc;
+ doc.qty = 1.0;
+ this.grid.set_value("qty", 1.0, doc);
+ },
+ get_query() {
+ return {
+ filters: {
+ name: ["!=", row.finished_good],
+ },
+ };
+ },
+ },
+ {
+ label: __("Qty"),
+ fieldname: "qty",
+ default: 1.0,
+ fieldtype: "Float",
+ reqd: 1,
+ in_list_view: 1,
+ },
+ ],
+ },
+ {
+ fieldname: "operation_row_id",
+ fieldtype: "Data",
+ hidden: 1,
+ default: row.idx,
+ },
+ ];
+ },
+
+ _add_raw_materials(frm, values) {
+ frm.call({
+ method: "add_raw_materials",
+ doc: frm.doc,
+ args: {
+ operation_row_id: values.operation_row_id,
+ items: values.items,
+ },
+ });
+ },
+});
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 67de6a0632b..a8098329c10 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -26,19 +26,22 @@
"column_break_ivyw",
"currency",
"conversion_rate",
- "materials_section",
- "items",
- "section_break_21",
"operations_section_section",
"with_operations",
+ "track_semi_finished_goods",
"column_break_23",
"transfer_material_against",
"routing",
"fg_based_operating_cost",
+ "column_break_joxb",
+ "default_source_warehouse",
+ "default_target_warehouse",
"fg_based_section_section",
"operating_cost_per_bom_quantity",
"operations_section",
"operations",
+ "materials_section",
+ "items",
"scrap_section",
"scrap_items_section",
"scrap_items",
@@ -59,8 +62,8 @@
"base_total_cost",
"more_info_tab",
"item_name",
- "description",
"column_break_27",
+ "description",
"has_variants",
"quality_inspection_section_break",
"inspection_required",
@@ -211,7 +214,7 @@
},
{
"default": "Work Order",
- "depends_on": "with_operations",
+ "depends_on": "eval: doc.with_operations === 1 && doc.track_semi_finished_goods === 0",
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
@@ -406,8 +409,8 @@
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "section_break0",
- "fieldtype": "Section Break",
- "label": "Materials Required (Exploded)"
+ "fieldtype": "Tab Break",
+ "label": "Exploded Items"
},
{
"fieldname": "exploded_items",
@@ -485,11 +488,6 @@
"fieldtype": "Check",
"label": "Show Operations"
},
- {
- "fieldname": "section_break_21",
- "fieldtype": "Tab Break",
- "label": "Operations"
- },
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
@@ -534,6 +532,8 @@
"show_dashboard": 1
},
{
+ "collapsible": 1,
+ "collapsible_depends_on": "eval:doc.with_operations",
"fieldname": "operations_section_section",
"fieldtype": "Section Break",
"label": "Operations"
@@ -591,13 +591,13 @@
"default": "0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
- "label": "FG based Operating Cost"
+ "label": "Finished Goods based Operating Cost"
},
{
"depends_on": "fg_based_operating_cost",
"fieldname": "fg_based_section_section",
"fieldtype": "Section Break",
- "label": "FG Based Operating Cost Section"
+ "label": "Finished Goods Based Operating Cost"
},
{
"depends_on": "fg_based_operating_cost",
@@ -617,7 +617,8 @@
"no_copy": 1,
"options": "BOM Creator",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "bom_creator_item",
@@ -625,11 +626,36 @@
"label": "BOM Creator Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"fieldname": "column_break_oxbz",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "with_operations",
+ "description": "Users can consume raw materials and add semi-finished goods or final finished goods against the operation using job cards.",
+ "fieldname": "track_semi_finished_goods",
+ "fieldtype": "Check",
+ "label": "Track Semi Finished Goods"
+ },
+ {
+ "fieldname": "column_break_joxb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_source_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "default_target_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Target Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-sitemap",
@@ -637,7 +663,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2024-04-02 16:22:47.518411",
+ "modified": "2024-06-03 16:24:47.518411",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 40b4c4f7455..5ff531e797d 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -10,7 +10,7 @@ import frappe
from frappe import _
from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import cint, cstr, flt, today
+from frappe.utils import cint, cstr, flt, parse_json, today
from frappe.website.website_generator import WebsiteGenerator
import erpnext
@@ -125,6 +125,8 @@ class BOM(WebsiteGenerator):
company: DF.Link
conversion_rate: DF.Float
currency: DF.Link
+ default_source_warehouse: DF.Link | None
+ default_target_warehouse: DF.Link | None
description: DF.SmallText | None
exploded_items: DF.Table[BOMExplosionItem]
fg_based_operating_cost: DF.Check
@@ -136,6 +138,7 @@ class BOM(WebsiteGenerator):
item: DF.Link
item_name: DF.Data | None
items: DF.Table[BOMItem]
+ track_semi_finished_goods: DF.Check
operating_cost: DF.Currency
operating_cost_per_bom_quantity: DF.Currency
operations: DF.Table[BOMOperation]
@@ -245,6 +248,7 @@ class BOM(WebsiteGenerator):
self.clear_inspection()
self.validate_main_item()
self.validate_currency()
+ self.set_materials_based_on_operation_bom()
self.set_conversion_rate()
self.set_plc_conversion_rate()
self.validate_uom_is_interger()
@@ -544,6 +548,9 @@ class BOM(WebsiteGenerator):
if not self.with_operations:
self.set("operations", [])
+ if not self.with_operations and self.track_semi_finished_goods:
+ self.track_semi_finished_goods = 0
+
def clear_inspection(self):
if not self.inspection_required:
self.quality_inspection_template = None
@@ -645,6 +652,49 @@ class BOM(WebsiteGenerator):
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)
+ def set_materials_based_on_operation_bom(self):
+ if not self.track_semi_finished_goods:
+ return
+
+ for row in self.get("operations"):
+ if row.bom_no and row.finished_good:
+ self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty)
+
+ @frappe.whitelist()
+ def add_raw_materials(self, operation_row_id, items):
+ if isinstance(items, str):
+ items = parse_json(items)
+
+ for row in items:
+ row = parse_json(row)
+
+ row.update(get_item_details(row.get("item_code")))
+ row.operation_row_id = operation_row_id
+ row.idx = None
+ row.name = None
+ self.append("items", row)
+
+ self.save()
+
+ @frappe.whitelist()
+ def add_materials_from_bom(self, finished_good, bom_no, operation_row_id, qty=None):
+ if not frappe.db.exists("BOM", {"item": finished_good, "name": bom_no, "docstatus": 1}):
+ frappe.throw(_("BOM {0} not found for the item {1}").format(bom_no, finished_good))
+
+ if not qty:
+ qty = 1
+
+ for row in self.items:
+ if row.operation_row_id == operation_row_id:
+ return
+
+ bom_items = get_bom_items(bom_no, self.company, qty=qty, fetch_exploded=0)
+ for row in bom_items:
+ row.uom = row.stock_uom
+ row.operation_row_id = operation_row_id
+ row.idx = None
+ self.append("items", row)
+
def traverse_tree(self, bom_list=None):
def _get_children(bom_no):
children = frappe.cache().hget("bom_children", bom_no)
@@ -1094,6 +1144,11 @@ def get_bom_items_as_dict(
):
item_dict = {}
+ group_by_cond = "group by item_code, stock_uom"
+ if frappe.get_cached_value("BOM", bom, "track_semi_finished_goods"):
+ fetch_exploded = 0
+ group_by_cond = "group by item_code, operation_row_id, stock_uom"
+
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
query = """select
bom_item.item_code,
@@ -1122,7 +1177,7 @@ def get_bom_items_as_dict(
and bom.name = %(bom)s
and item.is_stock_item in (1, {is_stock_item})
{where_conditions}
- group by item_code, stock_uom
+ {group_by_cond}
order by idx"""
is_stock_item = 0 if include_non_stock_items else 1
@@ -1132,6 +1187,7 @@ def get_bom_items_as_dict(
where_conditions="",
is_stock_item=is_stock_item,
qty_field="stock_qty",
+ group_by_cond=group_by_cond,
select_columns=""", bom_item.source_warehouse, bom_item.operation,
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
@@ -1147,6 +1203,7 @@ def get_bom_items_as_dict(
select_columns=", item.description",
is_stock_item=is_stock_item,
qty_field="stock_qty",
+ group_by_cond=group_by_cond,
)
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
@@ -1158,15 +1215,20 @@ def get_bom_items_as_dict(
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
- bom_item.description, bom_item.base_rate as rate """,
+ bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id """,
+ group_by_cond=group_by_cond,
)
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
for item in items:
- if item.item_code in item_dict:
- item_dict[item.item_code]["qty"] += flt(item.qty)
+ key = item.item_code
+ if item.operation_row_id:
+ key = (item.item_code, item.operation_row_id)
+
+ if key in item_dict:
+ item_dict[key]["qty"] += flt(item.qty)
else:
- item_dict[item.item_code] = item
+ item_dict[key] = item
for item, item_details in item_dict.items():
for d in [
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
index 32231aa4949..b44c9f53f23 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
@@ -88,9 +88,77 @@ frappe.ui.form.on("BOM Creator", {
reqd: 1,
default: 1.0,
},
+ { fieldtype: "Section Break" },
+ {
+ label: __("Track Operations"),
+ fieldtype: "Check",
+ fieldname: "track_operations",
+ onchange: (r) => {
+ let track_operations = dialog.get_value("track_operations");
+ if (r.type === "input" && !track_operations) {
+ dialog.set_value("track_semi_finished_goods", 0);
+ }
+ },
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Track Semi Finished Goods"),
+ fieldtype: "Check",
+ fieldname: "track_semi_finished_goods",
+ depends_on: "eval:doc.track_operations",
+ },
+ {
+ fieldtype: "Section Break",
+ label: __("Final Product Operation"),
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ {
+ label: __("Operation"),
+ fieldtype: "Link",
+ fieldname: "operation",
+ options: "Operation",
+ default: "Assembly",
+ mandatory_depends_on: "eval:doc.track_semi_finished_goods",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ {
+ label: __("Operation Time (in mins)"),
+ fieldtype: "Float",
+ fieldname: "operation_time",
+ mandatory_depends_on: "eval:doc.track_semi_finished_goods",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Workstation Type"),
+ fieldtype: "Link",
+ fieldname: "workstation_type",
+ options: "Workstation",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ },
+ {
+ label: __("Workstation"),
+ fieldtype: "Link",
+ fieldname: "workstation",
+ options: "Workstation",
+ depends_on: "eval:doc.track_semi_finished_goods",
+ get_query() {
+ let workstation_type = dialog.get_value("workstation_type");
+
+ if (workstation_type) {
+ return {
+ filters: {
+ workstation_type: dialog.get_value("workstation_type"),
+ },
+ };
+ }
+ },
+ },
],
primary_action_label: __("Create"),
primary_action: (values) => {
+ frm.events.validate_dialog_values(frm, values);
+
values.doctype = frm.doc.doctype;
frappe.db.insert(values).then((doc) => {
frappe.set_route("Form", doc.doctype, doc.name);
@@ -102,6 +170,18 @@ frappe.ui.form.on("BOM Creator", {
dialog.show();
},
+ validate_dialog_values(frm, values) {
+ if (values.track_semi_finished_goods) {
+ if (values.final_operation_time <= 0) {
+ frappe.throw(__("Operation Time must be greater than 0"));
+ }
+
+ if (!values.workstation && !values.workstation_type) {
+ frappe.throw(__("Either Workstation or Workstation Type is mandatory"));
+ }
+ }
+ },
+
set_queries(frm) {
frm.set_query("bom_no", "items", function (doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
@@ -121,6 +201,16 @@ frappe.ui.form.on("BOM Creator", {
query: "erpnext.controllers.queries.item_query",
};
});
+
+ frm.set_query("workstation", (doc) => {
+ if (doc.workstation_type) {
+ return {
+ filters: {
+ workstation_type: doc.workstation_type,
+ },
+ };
+ }
+ });
},
refresh(frm) {
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
index 1e8237c03f7..2b4b9a055aa 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
@@ -37,6 +37,23 @@
"items",
"costing_detail",
"raw_material_cost",
+ "configuration_section",
+ "track_operations",
+ "column_break_obzr",
+ "track_semi_finished_goods",
+ "final_product_operation_section",
+ "operation",
+ "operation_time",
+ "column_break_xnlu",
+ "workstation_type",
+ "workstation",
+ "final_product_warehouse_section",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "source_warehouse",
+ "column_break_buha",
+ "wip_warehouse",
+ "fg_warehouse",
"remarks_tab",
"remarks",
"section_break_yixm",
@@ -278,6 +295,104 @@
"fieldtype": "Text",
"label": "Error Log",
"read_only": 1
+ },
+ {
+ "fieldname": "configuration_section",
+ "fieldtype": "Section Break",
+ "label": "Operation"
+ },
+ {
+ "default": "0",
+ "depends_on": "track_operations",
+ "fieldname": "track_semi_finished_goods",
+ "fieldtype": "Check",
+ "label": "Track Semi Finished Goods"
+ },
+ {
+ "fieldname": "column_break_obzr",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "track_operations",
+ "fieldtype": "Check",
+ "label": "Track Operations"
+ },
+ {
+ "depends_on": "eval:doc.track_semi_finished_goods === 1",
+ "fieldname": "final_product_operation_section",
+ "fieldtype": "Section Break",
+ "label": "Final Product Operation & Workstation"
+ },
+ {
+ "fieldname": "column_break_xnlu",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "label": "Operation",
+ "options": "Operation"
+ },
+ {
+ "fieldname": "operation_time",
+ "fieldtype": "Float",
+ "label": "Operation Time (in mins)"
+ },
+ {
+ "fieldname": "workstation",
+ "fieldtype": "Link",
+ "label": "Workstation",
+ "options": "Workstation"
+ },
+ {
+ "fieldname": "workstation_type",
+ "fieldtype": "Link",
+ "label": "Workstation Type",
+ "options": "Workstation Type"
+ },
+ {
+ "depends_on": "eval:!doc.backflush_from_wip_warehouse",
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "Work In Progress Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Finished Good Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "depends_on": "eval:doc.track_semi_finished_goods === 1",
+ "fieldname": "final_product_warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Final Product Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": "Skip Material Transfer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.skip_material_transfer",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
+ },
+ {
+ "fieldname": "column_break_buha",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-sitemap",
@@ -288,7 +403,7 @@
"link_fieldname": "bom_creator"
}
],
- "modified": "2024-04-02 16:30:59.779190",
+ "modified": "2024-05-26 15:47:10.101420",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator",
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
index e236e7a6345..c84b300622c 100644
--- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -43,16 +43,20 @@ class BOMCreator(Document):
from erpnext.manufacturing.doctype.bom_creator_item.bom_creator_item import BOMCreatorItem
amended_from: DF.Link | None
+ backflush_from_wip_warehouse: DF.Check
buying_price_list: DF.Link | None
company: DF.Link
conversion_rate: DF.Float
currency: DF.Link
default_warehouse: DF.Link | None
error_log: DF.Text | None
+ fg_warehouse: DF.Link | None
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
items: DF.Table[BOMCreatorItem]
+ operation: DF.Link | None
+ operation_time: DF.Float
plc_conversion_rate: DF.Float
price_list_currency: DF.Link | None
project: DF.Link | None
@@ -61,8 +65,15 @@ class BOMCreator(Document):
remarks: DF.TextEditor | None
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
set_rate_based_on_warehouse: DF.Check
+ skip_material_transfer: DF.Check
+ source_warehouse: DF.Link | None
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
+ track_operations: DF.Check
+ track_semi_finished_goods: DF.Check
uom: DF.Link | None
+ wip_warehouse: DF.Link | None
+ workstation: DF.Link | None
+ workstation_type: DF.Link | None
# end: auto-generated types
def before_save(self):
@@ -236,8 +247,10 @@ class BOMCreator(Document):
self.db_set("status", "In Progress")
production_item_wise_rm = OrderedDict({})
+
+ final_product = (self.item_code, self.name)
production_item_wise_rm.setdefault(
- (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
+ final_product, frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
)
for row in self.items:
@@ -257,9 +270,15 @@ class BOMCreator(Document):
try:
for d in reverse_tree:
+ if self.track_operations and self.track_semi_finished_goods and final_product == d:
+ continue
+
fg_item_data = production_item_wise_rm.get(d).fg_item_data
self.create_bom(fg_item_data, production_item_wise_rm)
+ if self.track_operations and self.track_semi_finished_goods:
+ self.make_bom_for_final_product(production_item_wise_rm)
+
frappe.msgprint(_("BOMs created successfully"))
except Exception:
traceback = frappe.get_traceback(with_context=True)
@@ -272,6 +291,81 @@ class BOMCreator(Document):
frappe.msgprint(_("BOMs creation failed"))
+ def make_bom_for_final_product(self, production_item_wise_rm):
+ bom = frappe.new_doc("BOM")
+ bom.update(
+ {
+ "item": self.item_code,
+ "bom_type": "Production",
+ "quantity": self.qty,
+ "allow_alternative_item": 1,
+ "bom_creator": self.name,
+ "bom_creator_item": self.name,
+ "rm_cost_as_per": "Manual",
+ "with_operations": 1,
+ "track_semi_finished_goods": 1,
+ }
+ )
+
+ for field in BOM_FIELDS:
+ if self.get(field):
+ bom.set(field, self.get(field))
+
+ for item in self.items:
+ if not item.is_expandable or not item.operation:
+ continue
+
+ bom.append(
+ "operations",
+ {
+ "operation": item.operation,
+ "workstation": item.workstation,
+ "source_warehouse": item.source_warehouse,
+ "wip_warehouse": item.wip_warehouse,
+ "fg_warehouse": item.fg_warehouse,
+ "finished_good": item.item_code,
+ "finished_good_qty": item.qty,
+ "bom_no": production_item_wise_rm[(item.item_code, item.name)].bom_no,
+ "workstation_type": item.workstation_type,
+ "time_in_mins": item.operation_time,
+ "is_subcontracted": item.is_subcontracted,
+ "skip_material_transfer": item.skip_material_transfer,
+ "backflush_from_wip_warehouse": item.backflush_from_wip_warehouse,
+ },
+ )
+
+ operation_row = bom.append(
+ "operations",
+ {
+ "operation": self.operation,
+ "time_in_mins": self.operation_time,
+ "workstation": self.workstation,
+ "workstation_type": self.workstation_type,
+ "finished_good": self.item_code,
+ "finished_good_qty": self.qty,
+ "source_warehouse": self.source_warehouse,
+ "wip_warehouse": self.wip_warehouse,
+ "fg_warehouse": self.fg_warehouse,
+ "skip_material_transfer": self.skip_material_transfer,
+ "backflush_from_wip_warehouse": self.backflush_from_wip_warehouse,
+ },
+ )
+
+ final_product = (self.item_code, self.name)
+ items = production_item_wise_rm.get(final_product).get("items")
+
+ bom.set_materials_based_on_operation_bom()
+
+ for item in items:
+ item_args = {"operation_row_id": operation_row.idx}
+ for field in BOM_ITEM_FIELDS:
+ item_args[field] = item.get(field)
+
+ bom.append("items", item_args)
+
+ bom.save(ignore_permissions=True)
+ bom.submit()
+
def create_bom(self, row, production_item_wise_rm):
bom_creator_item = row.name if row.name != self.name else ""
if frappe.db.exists(
@@ -297,6 +391,24 @@ class BOMCreator(Document):
}
)
+ if self.track_operations and not self.track_semi_finished_goods:
+ if row.item_code == self.item_code:
+ bom.with_operations = 1
+ bom.transfer_material_against = "Work Order"
+ for item in self.items:
+ if not item.operation:
+ continue
+
+ bom.append(
+ "operations",
+ {
+ "operation": item.operation,
+ "workstation_type": item.workstation_type,
+ "workstation": item.workstation,
+ "time_in_mins": item.operation_time,
+ },
+ )
+
for field in BOM_FIELDS:
if self.get(field):
bom.set(field, self.get(field))
@@ -352,6 +464,16 @@ def get_children(doctype=None, parent=None, **kwargs):
"uom",
"rate",
"amount",
+ "workstation_type",
+ "operation",
+ "operation_time",
+ "is_subcontracted",
+ "workstation",
+ "source_warehouse",
+ "wip_warehouse",
+ "fg_warehouse",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
]
query_filters = {
@@ -365,6 +487,12 @@ def get_children(doctype=None, parent=None, **kwargs):
return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
+def get_parent_row_no(doc, name):
+ for row in doc.items:
+ if row.name == name:
+ return row.idx
+
+
@frappe.whitelist()
def add_item(**kwargs):
if isinstance(kwargs, str):
@@ -375,6 +503,11 @@ def add_item(**kwargs):
doc = frappe.get_doc("BOM Creator", kwargs.parent)
item_info = get_item_details(kwargs.item_code)
+
+ parent_row_no = ""
+ if kwargs.fg_reference_id and doc.name != kwargs.fg_reference_id:
+ parent_row_no = get_parent_row_no(doc, kwargs.fg_reference_id)
+
kwargs.update(
{
"uom": item_info.stock_uom,
@@ -383,6 +516,9 @@ def add_item(**kwargs):
}
)
+ if parent_row_no:
+ kwargs.update({"parent_row_no": parent_row_no})
+
doc.append("items", kwargs)
doc.save()
@@ -402,6 +538,7 @@ def add_sub_assembly(**kwargs):
name = kwargs.fg_reference_id
parent_row_no = ""
+
if not kwargs.convert_to_sub_assembly:
item_info = get_item_details(bom_item.item_code)
item_row = doc.append(
@@ -417,6 +554,15 @@ def add_sub_assembly(**kwargs):
"do_not_explode": 1,
"is_expandable": 1,
"stock_uom": item_info.stock_uom,
+ "operation": bom_item.operation,
+ "workstation_type": bom_item.workstation_type,
+ "operation_time": bom_item.operation_time,
+ "is_subcontracted": bom_item.is_subcontracted,
+ "workstation": bom_item.workstation,
+ "source_warehouse": bom_item.source_warehouse,
+ "wip_warehouse": bom_item.wip_warehouse,
+ "fg_warehouse": bom_item.fg_warehouse,
+ "skip_material_transfer": bom_item.skip_material_transfer,
},
)
@@ -426,6 +572,20 @@ def add_sub_assembly(**kwargs):
parent_row_no = [row.idx for row in doc.items if row.name == kwargs.fg_reference_id]
if parent_row_no:
parent_row_no = parent_row_no[0]
+ doc.items[parent_row_no - 1].update(
+ {
+ "operation": bom_item.operation,
+ "workstation_type": bom_item.workstation_type,
+ "operation_time": bom_item.operation_time,
+ "is_subcontracted": bom_item.is_subcontracted,
+ "workstation": bom_item.workstation,
+ "source_warehouse": bom_item.source_warehouse,
+ "wip_warehouse": bom_item.wip_warehouse,
+ "fg_warehouse": bom_item.fg_warehouse,
+ "skip_material_transfer": bom_item.skip_material_transfer,
+ "backflush_from_wip_warehouse": bom_item.backflush_from_wip_warehouse,
+ }
+ )
for row in bom_item.get("items"):
row = frappe._dict(row)
@@ -482,10 +642,16 @@ def delete_node(**kwargs):
@frappe.whitelist()
-def edit_qty(doctype, docname, qty, parent):
- frappe.db.set_value(doctype, docname, "qty", qty)
+def edit_bom_creator(doctype, docname, data, parent):
+ if isinstance(data, str):
+ data = frappe.parse_json(data)
+
+ frappe.db.set_value(doctype, docname, data)
+
doc = frappe.get_doc("BOM Creator", parent)
doc.set_rate_for_items()
doc.save()
+ frappe.msgprint(_("Updated successfully"), alert=True)
+
return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
index e9545ac5385..7522bd24db8 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -11,10 +11,23 @@
"item_group",
"column_break_f63f",
"fg_item",
- "source_warehouse",
"is_expandable",
"sourced_by_supplier",
"bom_created",
+ "is_subcontracted",
+ "operation_section",
+ "operation",
+ "operation_time",
+ "column_break_cbnk",
+ "workstation_type",
+ "workstation",
+ "warehouse_section",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "source_warehouse",
+ "column_break_xutc",
+ "wip_warehouse",
+ "fg_warehouse",
"description_section",
"description",
"quantity_and_rate_section",
@@ -70,21 +83,23 @@
"fieldname": "fg_item",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "FG Item",
+ "label": "Finished Goods Item",
"options": "Item",
"reqd": 1
},
{
+ "depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
"fieldname": "source_warehouse",
"fieldtype": "Link",
- "in_list_view": 1,
"label": "Source Warehouse",
"options": "Warehouse"
},
{
+ "columns": 1,
"default": "0",
"fieldname": "is_expandable",
"fieldtype": "Check",
+ "in_list_view": 1,
"label": "Is Expandable",
"read_only": 1
},
@@ -203,7 +218,7 @@
{
"fieldname": "fg_reference_id",
"fieldtype": "Data",
- "label": "FG Reference",
+ "label": "Finished Goods Reference",
"no_copy": 1,
"read_only": 1
},
@@ -225,12 +240,87 @@
"label": "BOM Created",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "fieldname": "operation_section",
+ "fieldtype": "Section Break",
+ "label": "Operation"
+ },
+ {
+ "fieldname": "operation",
+ "fieldtype": "Link",
+ "label": "Operation",
+ "options": "Operation"
+ },
+ {
+ "fieldname": "column_break_cbnk",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "workstation_type",
+ "fieldtype": "Link",
+ "label": "Workstation Type",
+ "options": "Workstation Type"
+ },
+ {
+ "description": "In Mins",
+ "fieldname": "operation_time",
+ "fieldtype": "Int",
+ "label": "Operation Time"
+ },
+ {
+ "fieldname": "workstation",
+ "fieldtype": "Link",
+ "label": "Workstation",
+ "options": "Workstation"
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "Work In Progress Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_xutc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "label": "Finished Good Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": "Skip Material Transfer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.skip_material_transfer",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_subcontracted",
+ "fieldtype": "Check",
+ "label": "Is Subcontracted",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:40.764747",
+ "modified": "2024-06-03 18:45:24.339532",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Creator Item",
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
index e172f36224d..2510a02ddc7 100644
--- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -15,6 +15,7 @@ class BOMCreatorItem(Document):
from frappe.types import DF
amount: DF.Currency
+ backflush_from_wip_warehouse: DF.Check
base_amount: DF.Currency
base_rate: DF.Currency
bom_created: DF.Check
@@ -23,22 +24,30 @@ class BOMCreatorItem(Document):
do_not_explode: DF.Check
fg_item: DF.Link
fg_reference_id: DF.Data | None
+ fg_warehouse: DF.Link | None
instruction: DF.SmallText | None
is_expandable: DF.Check
+ is_subcontracted: DF.Check
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
+ operation: DF.Link | None
+ operation_time: DF.Int
parent: DF.Data
parent_row_no: DF.Data | None
parentfield: DF.Data
parenttype: DF.Data
qty: DF.Float
rate: DF.Currency
+ skip_material_transfer: DF.Check
source_warehouse: DF.Link | None
sourced_by_supplier: DF.Check
stock_qty: DF.Float
stock_uom: DF.Link | None
uom: DF.Link | None
+ wip_warehouse: DF.Link | None
+ workstation: DF.Link | None
+ workstation_type: DF.Link | None
# end: auto-generated types
pass
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json
index 226cfe0162f..1d530af34a2 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.json
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json
@@ -9,6 +9,7 @@
"item_code",
"item_name",
"operation",
+ "operation_row_id",
"column_break_3",
"do_not_explode",
"bom_no",
@@ -293,13 +294,19 @@
"fieldtype": "Check",
"label": "Is Stock Item",
"read_only": 1
+ },
+ {
+ "depends_on": "eval:parent.track_semi_finished_goods ==1",
+ "fieldname": "operation_row_id",
+ "fieldtype": "Int",
+ "label": "Operation ID"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:41.079752",
+ "modified": "2024-03-27 13:08:41.079752",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",
diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.py b/erpnext/manufacturing/doctype/bom_item/bom_item.py
index 466253bf0bf..87430d7d47d 100644
--- a/erpnext/manufacturing/doctype/bom_item/bom_item.py
+++ b/erpnext/manufacturing/doctype/bom_item/bom_item.py
@@ -25,9 +25,11 @@ class BOMItem(Document):
has_variants: DF.Check
image: DF.Attach | None
include_item_in_manufacturing: DF.Check
+ is_stock_item: DF.Check
item_code: DF.Link
item_name: DF.Data | None
operation: DF.Link | None
+ operation_row_id: DF.Int
original_item: DF.Link | None
parent: DF.Data
parentfield: DF.Data
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index aa62b027b06..3b403f4dbb8 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -6,24 +6,36 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "sequence_id",
"operation",
+ "sequence_id",
+ "finished_good",
+ "finished_good_qty",
+ "bom_no",
+ "add_raw_materials",
"col_break1",
"workstation_type",
"workstation",
"time_in_mins",
"fixed_time",
+ "is_subcontracted",
+ "is_final_finished_good",
+ "set_cost_based_on_bom_qty",
+ "warehouse_section",
+ "skip_material_transfer",
+ "backflush_from_wip_warehouse",
+ "source_warehouse",
+ "column_break_lbhy",
+ "wip_warehouse",
+ "fg_warehouse",
"costing_section",
"hour_rate",
"base_hour_rate",
- "column_break_9",
- "operating_cost",
- "base_operating_cost",
- "column_break_11",
"batch_size",
- "set_cost_based_on_bom_qty",
+ "column_break_11",
"cost_per_unit",
"base_cost_per_unit",
+ "operating_cost",
+ "base_operating_cost",
"more_information_section",
"description",
"column_break_18",
@@ -71,13 +83,14 @@
"precision": "2"
},
{
+ "columns": 1,
"description": "In minutes",
"fetch_from": "operation.total_operation_time",
"fetch_if_empty": 1,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Operation Time ",
+ "label": "Operation Time",
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"reqd": 1
@@ -87,7 +100,6 @@
"description": "Operation time does not depend on quantity to produce",
"fieldname": "fixed_time",
"fieldtype": "Check",
- "in_list_view": 1,
"label": "Fixed Time"
},
{
@@ -172,10 +184,6 @@
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
{
"default": "0",
"fieldname": "set_cost_based_on_bom_qty",
@@ -183,18 +191,106 @@
"label": "Set Operating Cost Based On BOM Quantity"
},
{
+ "columns": 1,
"fieldname": "workstation_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Workstation Type",
"options": "Workstation Type"
+ },
+ {
+ "fieldname": "finished_good",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Finished Goods / Semi Finished Goods Item",
+ "options": "Item"
+ },
+ {
+ "columns": 1,
+ "fieldname": "bom_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "BOM No",
+ "options": "BOM"
+ },
+ {
+ "columns": 1,
+ "default": "1",
+ "fieldname": "finished_good_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Finished Goods Qty"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_final_finished_good",
+ "fieldtype": "Check",
+ "label": "Is Final Finished Good"
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "depends_on": "eval:!doc.skip_material_transfer || doc.backflush_from_wip_warehouse",
+ "fieldname": "wip_warehouse",
+ "fieldtype": "Link",
+ "label": "WIP Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_lbhy",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 1,
+ "fieldname": "fg_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Finished Goods Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "columns": 1,
+ "depends_on": "eval:doc.skip_material_transfer && !doc.backflush_from_wip_warehouse",
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_subcontracted",
+ "fieldtype": "Check",
+ "label": "Is Subcontracted"
+ },
+ {
+ "depends_on": "eval:!doc.bom_no",
+ "fieldname": "add_raw_materials",
+ "fieldtype": "Button",
+ "label": "Add Raw Materials"
+ },
+ {
+ "default": "0",
+ "fieldname": "skip_material_transfer",
+ "fieldtype": "Check",
+ "label": " Skip Material Transfer"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.skip_material_transfer",
+ "fieldname": "backflush_from_wip_warehouse",
+ "fieldtype": "Check",
+ "label": "Backflush Materials From WIP Warehouse"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-03-27 13:06:41.248462",
+ "modified": "2024-06-03 15:46:49.404875",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
@@ -203,4 +299,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py
index 66ac02891b9..fd197e89e62 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py
@@ -14,15 +14,22 @@ class BOMOperation(Document):
if TYPE_CHECKING:
from frappe.types import DF
+ backflush_from_wip_warehouse: DF.Check
base_cost_per_unit: DF.Float
base_hour_rate: DF.Currency
base_operating_cost: DF.Currency
batch_size: DF.Int
+ bom_no: DF.Link | None
cost_per_unit: DF.Float
description: DF.TextEditor | None
+ fg_warehouse: DF.Link | None
+ finished_good: DF.Link | None
+ finished_good_qty: DF.Float
fixed_time: DF.Check
hour_rate: DF.Currency
image: DF.Attach | None
+ is_final_finished_good: DF.Check
+ is_subcontracted: DF.Check
operating_cost: DF.Currency
operation: DF.Link
parent: DF.Data
@@ -30,7 +37,10 @@ class BOMOperation(Document):
parenttype: DF.Data
sequence_id: DF.Int
set_cost_based_on_bom_qty: DF.Check
+ skip_material_transfer: DF.Check
+ source_warehouse: DF.Link | None
time_in_mins: DF.Float
+ wip_warehouse: DF.Link | None
workstation: DF.Link | None
workstation_type: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 4cc60a3b4a6..2de5d9dad11 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -32,21 +32,61 @@ frappe.ui.form.on("Job Card", {
});
},
+ make_fields_read_only(frm) {
+ if (frm.doc.docstatus === 1) {
+ frm.set_df_property("employee", "read_only", 1);
+ frm.set_df_property("time_logs", "read_only", 1);
+ }
+
+ if (frm.doc.is_subcontracted) {
+ frm.set_df_property("wip_warehouse", "label", __("Supplier Warehouse"));
+ }
+ },
+
+ setup_stock_entry(frm) {
+ if (
+ frm.doc.finished_good &&
+ frm.doc.docstatus === 1 &&
+ !frm.doc.is_subcontracted &&
+ flt(frm.doc.for_quantity) + flt(frm.doc.process_loss_qty) > flt(frm.doc.manufactured_qty)
+ ) {
+ frm.add_custom_button(__("Make Stock Entry"), () => {
+ frm.call({
+ method: "make_stock_entry_for_semi_fg_item",
+ args: {
+ auto_submit: 1,
+ },
+ doc: frm.doc,
+ freeze: true,
+ callback() {
+ frm.reload_doc();
+ },
+ });
+ }).addClass("btn-primary");
+ }
+ },
+
refresh: function (frm) {
- frappe.flags.pause_job = 0;
- frappe.flags.resume_job = 0;
+ frm.trigger("setup_stock_entry");
+
let has_items = frm.doc.items && frm.doc.items.length;
+ frm.trigger("make_fields_read_only");
if (!frm.is_new() && frm.doc.__onload.work_order_closed) {
frm.disable_save();
return;
}
+ if (frm.doc.is_subcontracted) {
+ frm.trigger("make_subcontracting_po");
+ return;
+ }
+
let has_stock_entry = frm.doc.__onload && frm.doc.__onload.has_stock_entry ? true : false;
frm.toggle_enable("for_quantity", !has_stock_entry);
- if (!frm.is_new() && has_items && frm.doc.docstatus < 2) {
+ if (!frm.is_new() && !frm.doc.skip_material_transfer && has_items && frm.doc.docstatus < 2) {
let to_request = frm.doc.for_quantity > frm.doc.transferred_qty;
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
@@ -63,11 +103,11 @@ frappe.ui.form.on("Job Card", {
if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry");
- }).addClass("btn-primary");
+ });
}
}
- if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
+ if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card && !frm.doc.finished_good) {
frm.trigger("setup_corrective_job_card");
}
@@ -84,31 +124,67 @@ frappe.ui.form.on("Job Card", {
frm.trigger("toggle_operation_number");
if (
- frm.doc.docstatus == 0 &&
- !frm.is_new() &&
- (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) &&
- (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)
+ frm.doc.for_quantity + frm.doc.process_loss_qty > frm.doc.total_completed_qty &&
+ (frm.doc.skip_material_transfer ||
+ frm.doc.transferred_qty >= frm.doc.for_quantity + frm.doc.process_loss_qty ||
+ !frm.doc.finished_good)
) {
- // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
- // and if stock mvt for WIP is required
- if (frm.doc.work_order) {
- frappe.db.get_value(
- "Work Order",
- frm.doc.work_order,
- ["skip_transfer", "status"],
- (result) => {
- if (
- result.skip_transfer === 1 ||
- result.status == "In Process" ||
- frm.doc.transferred_qty > 0 ||
- !frm.doc.items.length
- ) {
- frm.trigger("prepare_timer_buttons");
- }
+ if (!frm.doc.time_logs?.length) {
+ frm.add_custom_button(__("Start Job"), () => {
+ let from_time = frappe.datetime.now_datetime();
+ if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
+ frappe.prompt(
+ {
+ fieldtype: "Table MultiSelect",
+ label: __("Select Employees"),
+ options: "Job Card Time Log",
+ fieldname: "employees",
+ },
+ (d) => {
+ frm.events.start_timer(frm, from_time, d.employees);
+ },
+ __("Assign Job to Employee")
+ );
+ } else {
+ frm.events.start_timer(frm, from_time, frm.doc.employee);
}
- );
+ });
+ } else if (frm.doc.is_paused) {
+ frm.add_custom_button(__("Resume Job"), () => {
+ frm.call({
+ method: "resume_job",
+ doc: frm.doc,
+ args: {
+ start_time: frappe.datetime.now_datetime(),
+ },
+ callback() {
+ frm.reload_doc();
+ },
+ });
+ });
} else {
- frm.trigger("prepare_timer_buttons");
+ if (frm.doc.for_quantity - frm.doc.manufactured_qty > 0) {
+ if (!frm.doc.is_paused) {
+ frm.add_custom_button(__("Pause Job"), () => {
+ frm.call({
+ method: "pause_job",
+ doc: frm.doc,
+ args: {
+ end_time: frappe.datetime.now_datetime(),
+ },
+ callback() {
+ frm.reload_doc();
+ },
+ });
+ });
+ }
+
+ frm.add_custom_button(__("Complete Job"), () => {
+ frm.trigger("complete_job_card");
+ });
+ }
+
+ frm.trigger("make_dashboard");
}
}
@@ -116,7 +192,7 @@ frappe.ui.form.on("Job Card", {
if (frm.doc.work_order) {
frappe.db.get_value("Work Order", frm.doc.work_order, "transfer_material_against").then((r) => {
- if (r.message.transfer_material_against == "Work Order") {
+ if (r.message.transfer_material_against == "Work Order" && !frm.doc.operation_row_id) {
frm.set_df_property("items", "hidden", 1);
}
});
@@ -134,6 +210,75 @@ frappe.ui.form.on("Job Card", {
}
},
+ make_subcontracting_po(frm) {
+ if (frm.doc.docstatus === 1 && frm.doc.for_quantity > frm.doc.manufactured_qty) {
+ frm.add_custom_button(__("Make Subcontracting PO"), () => {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.manufacturing.doctype.job_card.job_card.make_subcontracting_po",
+ frm: frm,
+ });
+ }).addClass("btn-primary");
+ }
+ },
+
+ start_timer(frm, start_time, employees) {
+ frm.call({
+ method: "start_timer",
+ doc: frm.doc,
+ args: {
+ start_time: start_time,
+ employees: employees,
+ },
+ callback: function (r) {
+ frm.reload_doc();
+ frm.trigger("make_dashboard");
+ },
+ });
+ },
+
+ make_finished_good(frm) {
+ let fields = [
+ {
+ fieldtype: "Float",
+ label: __("Completed Quantity"),
+ fieldname: "qty",
+ reqd: 1,
+ default: frm.doc.for_quantity - frm.doc.manufactured_qty,
+ },
+ {
+ fieldtype: "Datetime",
+ label: __("End Time"),
+ fieldname: "end_time",
+ default: frappe.datetime.now_datetime(),
+ },
+ ];
+
+ frappe.prompt(
+ fields,
+ (data) => {
+ if (data.qty <= 0) {
+ frappe.throw(__("Quantity should be greater than 0"));
+ }
+
+ frm.call({
+ method: "make_finished_good",
+ doc: frm.doc,
+ args: {
+ qty: data.qty,
+ end_time: data.end_time,
+ },
+ callback: function (r) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ },
+ __("Enter Value"),
+ __("Update"),
+ __("Set Finished Good Quantity")
+ );
+ },
+
setup_quality_inspection: function (frm) {
let quality_inspection_field = frm.get_docfield("quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function (frm) {
@@ -262,90 +407,6 @@ frappe.ui.form.on("Job Card", {
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
},
- prepare_timer_buttons: function (frm) {
- frm.trigger("make_dashboard");
-
- if (!frm.doc.started_time && !frm.doc.current_time) {
- frm.add_custom_button(__("Start Job"), () => {
- if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
- frappe.prompt(
- {
- fieldtype: "Table MultiSelect",
- label: __("Select Employees"),
- options: "Job Card Time Log",
- fieldname: "employees",
- },
- (d) => {
- frm.events.start_job(frm, "Work In Progress", d.employees);
- },
- __("Assign Job to Employee")
- );
- } else {
- frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
- }
- }).addClass("btn-primary");
- } else if (frm.doc.status == "On Hold") {
- frm.add_custom_button(__("Resume Job"), () => {
- frm.events.start_job(frm, "Resume Job", frm.doc.employee);
- }).addClass("btn-primary");
- } else {
- frm.add_custom_button(__("Pause Job"), () => {
- frm.events.complete_job(frm, "On Hold");
- });
-
- frm.add_custom_button(__("Complete Job"), () => {
- var sub_operations = frm.doc.sub_operations;
-
- let set_qty = true;
- if (sub_operations && sub_operations.length > 1) {
- set_qty = false;
- let last_op_row = sub_operations[sub_operations.length - 2];
-
- if (last_op_row.status == "Complete") {
- set_qty = true;
- }
- }
-
- if (set_qty) {
- frappe.prompt(
- {
- fieldtype: "Float",
- label: __("Completed Quantity"),
- fieldname: "qty",
- default: frm.doc.for_quantity,
- },
- (data) => {
- frm.events.complete_job(frm, "Complete", data.qty);
- },
- __("Enter Value")
- );
- } else {
- frm.events.complete_job(frm, "Complete", 0.0);
- }
- }).addClass("btn-primary");
- }
- },
-
- start_job: function (frm, status, employee) {
- const args = {
- job_card_id: frm.doc.name,
- start_time: frappe.datetime.now_datetime(),
- employees: employee,
- status: status,
- };
- frm.events.make_time_log(frm, args);
- },
-
- complete_job: function (frm, status, completed_qty) {
- const args = {
- job_card_id: frm.doc.name,
- complete_time: frappe.datetime.now_datetime(),
- status: status,
- completed_qty: completed_qty,
- };
- frm.events.make_time_log(frm, args);
- },
-
make_time_log: function (frm, args) {
frm.events.update_sub_operation(frm, args);
@@ -392,7 +453,7 @@ frappe.ui.form.on("Job Card", {
function updateStopwatch(increment) {
var hours = Math.floor(increment / 3600);
var minutes = Math.floor((increment - hours * 3600) / 60);
- var seconds = increment - hours * 3600 - minutes * 60;
+ var seconds = flt(increment - hours * 3600 - minutes * 60, 2);
$(section)
.find(".hours")
@@ -415,7 +476,7 @@ frappe.ui.form.on("Job Card", {
frm.dashboard.refresh();
const timer = `
{{row.status}}
-