From 906ac093e37694c4616cffb961b131ce9002315b Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Fri, 8 Dec 2023 14:56:10 +0100
Subject: [PATCH 01/53] feat: copy emails from lead to customer
---
erpnext/selling/doctype/customer/customer.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index efb9820016..d44532adc5 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -229,6 +229,7 @@ class Customer(TransactionBase):
if self.flags.is_new_doc:
self.link_lead_address_and_contact()
+ self.copy_communication()
self.update_customer_groups()
@@ -290,6 +291,17 @@ class Customer(TransactionBase):
linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name))
linked_doc.save(ignore_permissions=self.flags.ignore_permissions)
+ def copy_communication(self):
+ if not self.lead_name or not frappe.db.get_single_value(
+ "CRM Settings", "carry_forward_communication_and_comments"
+ ):
+ return
+
+ from erpnext.crm.utils import copy_comments, link_communications
+
+ copy_comments("Lead", self.lead_name, self)
+ link_communications("Lead", self.lead_name, self)
+
def validate_name_with_customer_group(self):
if frappe.db.exists("Customer Group", self.name):
frappe.throw(
From c648090b5d18c58af658ec4fe88114741efa60c7 Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Fri, 12 Jan 2024 18:33:06 +0530
Subject: [PATCH 02/53] fix: query for filter by party
---
.../tax_withholding_details.py | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index d045d91f52..613d6f9fca 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -340,9 +340,6 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
if filters.get("to_date"):
query = query.where(gle.posting_date <= filters.get("to_date"))
- if bank_accounts:
- query = query.where(gle.against.notin(bank_accounts))
-
if filters.get("party"):
party = [filters.get("party")]
jv_condition = gle.against.isin(party) | (
@@ -354,7 +351,14 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
(gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
)
- query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
+
+ query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
+ if bank_accounts:
+ query = query.where(
+ gle.against.notin(bank_accounts) & (gle.account.isin(tds_accounts) & jv_condition)
+ | gle.party.isin(party)
+ )
+
return query
From e9526b112d2610293411ada5a98ee847dd511306 Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Sun, 21 Jan 2024 18:04:47 +0530
Subject: [PATCH 03/53] test: journals in withholding report
---
.../test_tax_withholding_details.py | 34 +++++++++++++------
1 file changed, 24 insertions(+), 10 deletions(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
index b3f67378a9..88321e928c 100644
--- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
@@ -5,9 +5,8 @@ import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
-from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
-from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_tax_withholding_category,
@@ -17,7 +16,7 @@ from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
-class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
+class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.clear_old_entries()
@@ -27,13 +26,25 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
def test_tax_withholding_for_customers(self):
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
+ jv = make_journal_entry(
+ "TCS - _TC",
+ "Debtors - _TC",
+ 1000,
+ "_Test Cost Center - _TC",
+ save=False,
+ )
+ jv.accounts[1].party_type = "Customer"
+ jv.accounts[1].party = "_Test Customer"
+ jv.submit()
+
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
+ [jv.name, "TCS", 0.075, 1000, -1000, 1000],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
- [si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
+ [si.name, "TCS", 0.075, 1000, 0.525, 1000.525],
]
self.check_expected_values(result, expected_values)
@@ -41,12 +52,15 @@ class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase):
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
- self.assertEqual(voucher.ref_no, voucher_expected_values[0])
- self.assertEqual(voucher.section_code, voucher_expected_values[1])
- self.assertEqual(voucher.rate, voucher_expected_values[2])
- self.assertEqual(voucher.base_total, voucher_expected_values[3])
- self.assertAlmostEqual(voucher.tax_amount, voucher_expected_values[4])
- self.assertAlmostEqual(voucher.grand_total, voucher_expected_values[5])
+ voucher_actual_values = (
+ voucher.ref_no,
+ voucher.section_code,
+ voucher.rate,
+ voucher.base_total,
+ voucher.tax_amount,
+ voucher.grand_total,
+ )
+ self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
def tearDown(self):
self.clear_old_entries()
From ddecbeba756b741950f4233a3355b07add285880 Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Tue, 30 Jan 2024 12:39:40 +0530
Subject: [PATCH 04/53] fix: test JV totals using back calculation logic
---
.../test_tax_withholding_details.py | 43 ++++++++++++++-----
1 file changed, 32 insertions(+), 11 deletions(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
index 88321e928c..6825b4dcf6 100644
--- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
@@ -26,23 +26,15 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
def test_tax_withholding_for_customers(self):
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
- jv = make_journal_entry(
- "TCS - _TC",
- "Debtors - _TC",
- 1000,
- "_Test Cost Center - _TC",
- save=False,
- )
- jv.accounts[1].party_type = "Customer"
- jv.accounts[1].party = "_Test Customer"
- jv.submit()
+ jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
- [jv.name, "TCS", 0.075, 1000, -1000, 1000],
+ # Check for JV totals using back calculation logic
+ [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.525, 1000.525],
]
@@ -123,3 +115,32 @@ def create_tcs_payment_entry():
)
payment_entry.submit()
return payment_entry
+
+
+def create_tcs_journal_entry():
+ jv = frappe.new_doc("Journal Entry")
+ jv.posting_date = today()
+ jv.company = "_Test Company"
+ jv.set(
+ "accounts",
+ [
+ {
+ "account": "Debtors - _TC",
+ "party_type": "Customer",
+ "party": "_Test Customer",
+ "credit_in_account_currency": 10000,
+ },
+ {
+ "account": "Debtors - _TC",
+ "party_type": "Customer",
+ "party": "_Test Customer",
+ "debit_in_account_currency": 9992.5,
+ },
+ {
+ "account": "TCS - _TC",
+ "debit_in_account_currency": 7.5,
+ },
+ ],
+ )
+ jv.insert()
+ return jv.submit()
From 1ff473b615fddfffb928fcb67fa79de3efe6671b Mon Sep 17 00:00:00 2001
From: RitvikSardana
Date: Wed, 31 Jan 2024 11:34:20 +0530
Subject: [PATCH 05/53] fix: add ignore_permissions flag while creating a
payment entry
---
.../accounts/doctype/journal_entry/journal_entry.py | 6 ++++--
.../accounts/doctype/payment_entry/payment_entry.py | 13 +++++++++----
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index f722c907be..689dbe376b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -1155,7 +1155,9 @@ class JournalEntry(AccountsController):
@frappe.whitelist()
-def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
+def get_default_bank_cash_account(
+ company, account_type=None, mode_of_payment=None, account=None, ignore_permissions=False
+):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
if mode_of_payment:
@@ -1193,7 +1195,7 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
return frappe._dict(
{
"account": account,
- "balance": get_balance_on(account),
+ "balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
"account_currency": account_details.account_currency,
"account_type": account_details.account_type,
}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 7e88b6b7c9..309f072d27 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -2220,6 +2220,7 @@ def get_payment_entry(
party_type=None,
payment_type=None,
reference_date=None,
+ ignore_permissions=False,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2242,14 +2243,14 @@ def get_payment_entry(
)
# bank or cash
- bank = get_bank_cash_account(doc, bank_account)
+ bank = get_bank_cash_account(doc, bank_account, ignore_permissions=ignore_permissions)
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
if party_type in ["Customer", "Supplier"] and not bank:
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
if party_bank_account:
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
- bank = get_bank_cash_account(doc, account)
+ bank = get_bank_cash_account(doc, account, ignore_permissions=ignore_permissions)
paid_amount, received_amount = set_paid_amount_and_received_amount(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
@@ -2389,9 +2390,13 @@ def update_accounting_dimensions(pe, doc):
pe.set(dimension, doc.get(dimension))
-def get_bank_cash_account(doc, bank_account):
+def get_bank_cash_account(doc, bank_account, ignore_permissions=False):
bank = get_default_bank_cash_account(
- doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
+ doc.company,
+ "Bank",
+ mode_of_payment=doc.get("mode_of_payment"),
+ account=bank_account,
+ ignore_permissions=ignore_permissions,
)
if not bank:
From 951023f434a36ef03f874b3dcbd4f995168b7b5a Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Wed, 31 Jan 2024 11:32:17 +0530
Subject: [PATCH 06/53] perf: timeout for auto material request through reorder
level
---
erpnext/controllers/selling_controller.py | 2 +-
.../doctype/stock_entry/test_stock_entry.py | 42 +++++
erpnext/stock/get_item_details.py | 7 +-
erpnext/stock/reorder_item.py | 163 +++++++++++++-----
4 files changed, 172 insertions(+), 42 deletions(-)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 8c438420ad..dc49023149 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -599,7 +599,7 @@ class SellingController(StockController):
if self.doctype in ["Sales Order", "Quotation"]:
for item in self.items:
item.gross_profit = flt(
- ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item)
+ ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item)
)
def set_customer_address(self):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 23dacc8343..9f3435ec31 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -1785,6 +1785,48 @@ class TestStockEntry(FrappeTestCase):
self.assertRaises(frappe.ValidationError, se1.cancel)
+ def test_auto_reorder_level(self):
+ from erpnext.stock.reorder_item import reorder_item
+
+ item_doc = make_item(
+ "Test Auto Reorder Item - 001",
+ properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1},
+ uoms=[{"uom": "Nos", "conversion_factor": 5}],
+ )
+
+ if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}):
+ item_doc.append(
+ "reorder_levels",
+ {
+ "warehouse_reorder_level": 0,
+ "warehouse_reorder_qty": 10,
+ "warehouse": "_Test Warehouse - _TC",
+ "material_request_type": "Purchase",
+ },
+ )
+
+ item_doc.save(ignore_permissions=True)
+
+ frappe.db.set_single_value("Stock Settings", "auto_indent", 1)
+
+ mr_list = reorder_item()
+
+ frappe.db.set_single_value("Stock Settings", "auto_indent", 0)
+ mrs = frappe.get_all(
+ "Material Request Item",
+ fields=["qty", "stock_uom", "stock_qty"],
+ filters={"item_code": item_doc.name, "uom": "Nos"},
+ )
+
+ for mri in mrs:
+ self.assertEqual(mri.stock_uom, "Kg")
+ self.assertEqual(mri.stock_qty, 10)
+ self.assertEqual(mri.qty, 2)
+
+ for mr in mr_list:
+ mr.cancel()
+ mr.delete()
+
def make_serialized_item(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index ebcdd11bf1..693ccaa317 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -86,7 +86,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
get_party_item_code(args, item, out)
- set_valuation_rate(out, args)
+ if args.get("doctype") in ["Sales Order", "Quotation"]:
+ set_valuation_rate(out, args)
update_party_blanket_order(args, out)
@@ -269,7 +270,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if not item:
item = frappe.get_doc("Item", args.get("item_code"))
- if item.variant_of and not item.taxes:
+ if (
+ item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of})
+ ):
item.update_template_tables()
item_defaults = get_item_defaults(item.name, args.company)
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 276531ab5c..59f8b20b41 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -34,73 +34,157 @@ def _reorder_item():
erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]
)
- items_to_consider = frappe.db.sql_list(
- """select name from `tabItem` item
- where is_stock_item=1 and has_variants=0
- and disabled=0
- and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s)
- and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name)
- or (variant_of is not null and variant_of != ''
- and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of))
- )""",
- {"today": nowdate()},
- )
+ items_to_consider = get_items_for_reorder()
if not items_to_consider:
return
item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider)
- def add_to_material_request(
- item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None
- ):
- if warehouse not in warehouse_company:
+ def add_to_material_request(**kwargs):
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ if kwargs.warehouse not in warehouse_company:
# a disabled warehouse
return
- reorder_level = flt(reorder_level)
- reorder_qty = flt(reorder_qty)
+ reorder_level = flt(kwargs.reorder_level)
+ reorder_qty = flt(kwargs.reorder_qty)
# projected_qty will be 0 if Bin does not exist
- if warehouse_group:
- projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group))
+ if kwargs.warehouse_group:
+ projected_qty = flt(
+ item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group)
+ )
else:
- projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
+ projected_qty = flt(
+ item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)
+ )
if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
deficiency = reorder_level - projected_qty
if deficiency > reorder_qty:
reorder_qty = deficiency
- company = warehouse_company.get(warehouse) or default_company
+ company = warehouse_company.get(kwargs.warehouse) or default_company
- material_requests[material_request_type].setdefault(company, []).append(
- {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty}
+ material_requests[kwargs.material_request_type].setdefault(company, []).append(
+ {
+ "item_code": kwargs.item_code,
+ "warehouse": kwargs.warehouse,
+ "reorder_qty": reorder_qty,
+ "item_details": kwargs.item_details,
+ }
)
- for item_code in items_to_consider:
- item = frappe.get_doc("Item", item_code)
+ for item_code, reorder_levels in items_to_consider.items():
+ for d in reorder_levels:
+ if d.has_variants:
+ continue
- if item.variant_of and not item.get("reorder_levels"):
- item.update_template_tables()
-
- if item.get("reorder_levels"):
- for d in item.get("reorder_levels"):
- add_to_material_request(
- item_code,
- d.warehouse,
- d.warehouse_reorder_level,
- d.warehouse_reorder_qty,
- d.material_request_type,
- warehouse_group=d.warehouse_group,
- )
+ add_to_material_request(
+ item_code=item_code,
+ warehouse=d.warehouse,
+ reorder_level=d.warehouse_reorder_level,
+ reorder_qty=d.warehouse_reorder_qty,
+ material_request_type=d.material_request_type,
+ warehouse_group=d.warehouse_group,
+ item_details=frappe._dict(
+ {
+ "item_code": item_code,
+ "name": item_code,
+ "item_name": d.item_name,
+ "item_group": d.item_group,
+ "brand": d.brand,
+ "description": d.description,
+ "stock_uom": d.stock_uom,
+ "purchase_uom": d.purchase_uom,
+ }
+ ),
+ )
if material_requests:
return create_material_request(material_requests)
+def get_items_for_reorder() -> dict[str, list]:
+ reorder_table = frappe.qb.DocType("Item Reorder")
+ item_table = frappe.qb.DocType("Item")
+
+ query = (
+ frappe.qb.from_(reorder_table)
+ .inner_join(item_table)
+ .on(reorder_table.parent == item_table.name)
+ .select(
+ reorder_table.warehouse,
+ reorder_table.warehouse_group,
+ reorder_table.material_request_type,
+ reorder_table.warehouse_reorder_level,
+ reorder_table.warehouse_reorder_qty,
+ item_table.name,
+ item_table.stock_uom,
+ item_table.purchase_uom,
+ item_table.description,
+ item_table.item_name,
+ item_table.item_group,
+ item_table.brand,
+ item_table.variant_of,
+ item_table.has_variants,
+ )
+ .where(
+ (item_table.disabled == 0)
+ & (item_table.is_stock_item == 1)
+ & (
+ (item_table.end_of_life.isnull())
+ | (item_table.end_of_life > nowdate())
+ | (item_table.end_of_life == "0000-00-00")
+ )
+ )
+ )
+
+ data = query.run(as_dict=True)
+ itemwise_reorder = frappe._dict({})
+ for d in data:
+ itemwise_reorder.setdefault(d.name, []).append(d)
+
+ itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder)
+
+ return itemwise_reorder
+
+
+def get_reorder_levels_for_variants(itemwise_reorder):
+ item_table = frappe.qb.DocType("Item")
+
+ query = (
+ frappe.qb.from_(item_table)
+ .select(
+ item_table.name,
+ item_table.variant_of,
+ )
+ .where(
+ (item_table.disabled == 0)
+ & (item_table.is_stock_item == 1)
+ & (
+ (item_table.end_of_life.isnull())
+ | (item_table.end_of_life > nowdate())
+ | (item_table.end_of_life == "0000-00-00")
+ )
+ & (item_table.variant_of.notnull())
+ )
+ )
+
+ variants_item = query.run(as_dict=True)
+ for row in variants_item:
+ if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of):
+ itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, []))
+
+ return itemwise_reorder
+
+
def get_item_warehouse_projected_qty(items_to_consider):
item_warehouse_projected_qty = {}
+ items_to_consider = list(items_to_consider.keys())
for item_code, warehouse, projected_qty in frappe.db.sql(
"""select item_code, warehouse, projected_qty
@@ -164,7 +248,7 @@ def create_material_request(material_requests):
for d in items:
d = frappe._dict(d)
- item = frappe.get_doc("Item", d.item_code)
+ item = d.get("item_details")
uom = item.stock_uom
conversion_factor = 1.0
@@ -190,6 +274,7 @@ def create_material_request(material_requests):
"item_code": d.item_code,
"schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
"qty": qty,
+ "conversion_factor": conversion_factor,
"uom": uom,
"stock_uom": item.stock_uom,
"warehouse": d.warehouse,
From c077eda64e8b57011c42fbf7b4fe41af9ab2bc2f Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Thu, 1 Feb 2024 14:53:01 +0530
Subject: [PATCH 07/53] refactor: move ignore ERR filters from SOA to General
Ledger
---
.../process_statement_of_accounts.py | 16 ++--------------
.../report/general_ledger/general_ledger.js | 6 ++++++
.../report/general_ledger/general_ledger.py | 13 +++++++++++++
3 files changed, 21 insertions(+), 14 deletions(-)
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index c03b18a871..083c8fce18 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -120,18 +120,6 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
- err_journals = None
- if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
- err_journals = frappe.db.get_all(
- "Journal Entry",
- filters={
- "company": doc.company,
- "docstatus": 1,
- "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
- },
- as_list=True,
- )
-
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -144,8 +132,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
- if err_journals:
- filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
+ if doc.ignore_exchange_rate_revaluation_journals:
+ filters.update({"ignore_err": True})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 79b5e4d9ec..b7b9d34e00 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -203,8 +203,14 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check"
+ },
+ {
+ "fieldname": "ignore_err",
+ "label": __("Ignore Exchange Rate Revaluation Journals"),
+ "fieldtype": "Check"
}
+
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 110ec758fc..cea3a7b57e 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -241,6 +241,19 @@ def get_conditions(filters):
if filters.get("against_voucher_no"):
conditions.append("against_voucher=%(against_voucher_no)s")
+ if filters.get("ignore_err"):
+ err_journals = frappe.db.get_all(
+ "Journal Entry",
+ filters={
+ "company": filters.get("company"),
+ "docstatus": 1,
+ "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
+ },
+ as_list=True,
+ )
+ if err_journals:
+ filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
+
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
From affca3a519c626a2c3273a82f00ee1579853b0e1 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Thu, 1 Feb 2024 15:03:02 +0530
Subject: [PATCH 08/53] test: ignore_err filter out in General Ledger
---
.../general_ledger/test_general_ledger.py | 104 +++++++++++++++++-
1 file changed, 103 insertions(+), 1 deletion(-)
diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py
index a8c362e78c..c3ed7f2a23 100644
--- a/erpnext/accounts/report/general_ledger/test_general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py
@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from frappe.utils import today
+from frappe.utils import flt, today
from erpnext.accounts.report.general_ledger.general_ledger import execute
@@ -148,3 +148,105 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[2]["credit"], 900)
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
+
+ def test_ignore_exchange_rate_journals_filter(self):
+ # create a new account with USD currency
+ account_name = "Test Debtors USD"
+ company = "_Test Company"
+ account = frappe.get_doc(
+ {
+ "account_name": account_name,
+ "is_group": 0,
+ "company": company,
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "account_currency": "USD",
+ "parent_account": "Accounts Receivable - _TC",
+ "account_type": "Receivable",
+ "doctype": "Account",
+ }
+ )
+ account.insert(ignore_if_duplicate=True)
+ # create a JV to debit 1000 USD at 75 exchange rate
+ jv = frappe.new_doc("Journal Entry")
+ jv.posting_date = today()
+ jv.company = company
+ jv.multi_currency = 1
+ jv.cost_center = "_Test Cost Center - _TC"
+ jv.set(
+ "accounts",
+ [
+ {
+ "account": account.name,
+ "party_type": "Customer",
+ "party": "_Test Customer",
+ "debit_in_account_currency": 1000,
+ "credit_in_account_currency": 0,
+ "exchange_rate": 75,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ {
+ "account": "Cash - _TC",
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 75000,
+ "cost_center": "_Test Cost Center - _TC",
+ },
+ ],
+ )
+ jv.save()
+ jv.submit()
+
+ revaluation = frappe.new_doc("Exchange Rate Revaluation")
+ revaluation.posting_date = today()
+ revaluation.company = company
+ accounts = revaluation.get_accounts_data()
+ revaluation.extend("accounts", accounts)
+ row = revaluation.accounts[0]
+ row.new_exchange_rate = 83
+ row.new_balance_in_base_currency = flt(
+ row.new_exchange_rate * flt(row.balance_in_account_currency)
+ )
+ row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
+ revaluation.set_total_gain_loss()
+ revaluation = revaluation.save().submit()
+
+ # post journal entry for Revaluation doc
+ frappe.db.set_value(
+ "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
+ )
+ revaluation_jv = revaluation.make_jv_for_revaluation()
+ revaluation_jv.cost_center = "_Test Cost Center - _TC"
+ for acc in revaluation_jv.get("accounts"):
+ acc.cost_center = "_Test Cost Center - _TC"
+ revaluation_jv.save()
+ revaluation_jv.submit()
+
+ # With ignore_err enabled
+ columns, data = execute(
+ frappe._dict(
+ {
+ "company": company,
+ "from_date": today(),
+ "to_date": today(),
+ "account": [account.name],
+ "group_by": "Group by Voucher (Consolidated)",
+ "ignore_err": True,
+ }
+ )
+ )
+ self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data]))
+
+ # Without ignore_err enabled
+ columns, data = execute(
+ frappe._dict(
+ {
+ "company": company,
+ "from_date": today(),
+ "to_date": today(),
+ "account": [account.name],
+ "group_by": "Group by Voucher (Consolidated)",
+ "ignore_err": False,
+ }
+ )
+ )
+ self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data]))
From beff566c8267104b97f7e4b80c36715e4eb91832 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Thu, 1 Feb 2024 15:46:57 +0530
Subject: [PATCH 09/53] refactor(test): use party with USD billing currency
---
erpnext/accounts/report/general_ledger/test_general_ledger.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py
index c3ed7f2a23..75f94309bc 100644
--- a/erpnext/accounts/report/general_ledger/test_general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py
@@ -179,7 +179,7 @@ class TestGeneralLedger(FrappeTestCase):
{
"account": account.name,
"party_type": "Customer",
- "party": "_Test Customer",
+ "party": "_Test Customer USD",
"debit_in_account_currency": 1000,
"credit_in_account_currency": 0,
"exchange_rate": 75,
From e9fe10c6f13424630a2bfc7cfef1e1faeebab30d Mon Sep 17 00:00:00 2001
From: Divyam Mistry <73271406+divyam-mistry@users.noreply.github.com>
Date: Thu, 1 Feb 2024 16:15:43 +0530
Subject: [PATCH 10/53] fix: fetch/change tax template on basis of
base_net_rate instead of net_rate (#39448)
fix: change tax template on basis of base_net_rate instead of net_rate
Co-authored-by: Deepesh Garg
---
erpnext/controllers/taxes_and_totals.py | 1 +
erpnext/public/js/controllers/transaction.js | 3 ++-
erpnext/stock/get_item_details.py | 4 ++--
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 8cb1a0e861..3d7a94778b 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -98,6 +98,7 @@ class calculate_taxes_and_totals(object):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
"net_rate": item.net_rate or item.rate,
+ "base_net_rate": item.base_net_rate or item.base_rate,
"tax_category": self.doc.get("tax_category"),
"posting_date": self.doc.get("posting_date"),
"bill_date": self.doc.get("bill_date"),
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 4ea834bcd6..26795f7b98 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -502,6 +502,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
project: item.project || me.frm.doc.project,
qty: item.qty || 1,
net_rate: item.rate,
+ base_net_rate: item.base_net_rate,
stock_qty: item.stock_qty,
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
@@ -1902,7 +1903,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
- item_rates[item.name] = item.net_rate;
+ item_rates[item.name] = item.base_net_rate;
item_tax_templates[item.name] = item.item_tax_template;
}
});
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index ebcdd11bf1..bed5285f2d 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -543,7 +543,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
args = {
"company": company,
"tax_category": tax_category,
- "net_rate": item_rates.get(item_code[1]),
+ "base_net_rate": item_rates.get(item_code[1]),
}
if item_tax_templates:
@@ -635,7 +635,7 @@ def is_within_valid_range(args, tax):
if not flt(tax.maximum_net_rate):
# No range specified, just ignore
return True
- elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate):
+ elif flt(tax.minimum_net_rate) <= flt(args.get("base_net_rate")) <= flt(tax.maximum_net_rate):
return True
return False
From 78483e2ee6fdef9e36b63408f6796f0a1e78dac1 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Thu, 1 Feb 2024 16:05:27 +0530
Subject: [PATCH 11/53] refactor: use pop up to inform of possible data issue
and leave a comment in communcation trail as well
---
erpnext/accounts/doctype/account/account.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 651599dafd..3f11798c2a 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -118,6 +118,7 @@ class Account(NestedSet):
self.validate_balance_must_be_debit_or_credit()
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
+ self.validate_receivable_payable_account_type()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -188,6 +189,24 @@ class Account(NestedSet):
"Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
)
+ def validate_receivable_payable_account_type(self):
+ doc_before_save = self.get_doc_before_save()
+ receivable_payable_types = ["Receivable", "Payable"]
+ if (
+ doc_before_save
+ and doc_before_save.account_type in receivable_payable_types
+ and doc_before_save.account_type != self.account_type
+ ):
+ # check for ledger entries
+ if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1):
+ msg = _(
+ "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
+ ).format(
+ frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
+ )
+ frappe.msgprint(msg)
+ self.add_comment("Comment", msg)
+
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()
From d78a1e78148a3702990ae347a2b1c31b580c308e Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Thu, 1 Feb 2024 19:29:06 +0530
Subject: [PATCH 12/53] fix: incorrect landed cost voucher amount
---
.../doctype/landed_cost_voucher/landed_cost_voucher.py | 7 +++++++
.../stock/doctype/purchase_receipt/purchase_receipt.py | 10 +++++-----
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index b4f7708e0b..dec75066ec 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -149,6 +149,13 @@ class LandedCostVoucher(Document):
self.get("items")[item_count - 1].applicable_charges += diff
def validate_applicable_charges_for_item(self):
+ if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1:
+ frappe.throw(
+ _(
+ "Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher."
+ )
+ )
+
based_on = self.distribute_charges_based_on.lower()
if based_on != "distribute manually":
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index fcb7a6d510..c2890d8d86 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -1360,16 +1360,16 @@ def get_item_account_wise_additional_cost(purchase_document):
for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
+ based_on_field = None
# Use amount field for total item cost for manually cost distributed LCVs
- if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually":
- based_on_field = "amount"
- else:
+ if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually":
based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
total_item_cost = 0
- for item in landed_cost_voucher_doc.items:
- total_item_cost += item.get(based_on_field)
+ if based_on_field:
+ for item in landed_cost_voucher_doc.items:
+ total_item_cost += item.get(based_on_field)
for item in landed_cost_voucher_doc.items:
if item.receipt_document == purchase_document:
From 1e15a3cc151d750f6b00a52c4a9a595436e465ba Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Fri, 2 Feb 2024 13:07:26 +0530
Subject: [PATCH 13/53] fix: out of range for valuation_rate column in SE
---
erpnext/stock/stock_ledger.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 45764f3ec0..e88b1921fa 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -897,9 +897,12 @@ class update_entries_after(object):
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
- self.wh_data.qty_after_transaction += doc.total_qty
+ precision = doc.precision("total_qty")
+ self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
if self.wh_data.qty_after_transaction:
- self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
+ self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
+ self.wh_data.qty_after_transaction, precision
+ )
def validate_negative_stock(self, sle):
"""
From 7c6a5a0f23b948953815870b726c30b0fd076338 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Fri, 2 Feb 2024 13:18:52 +0530
Subject: [PATCH 14/53] fix: remove pricing rule
---
erpnext/controllers/accounts_controller.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index a0569ec63e..e2b0ee5fe4 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -693,7 +693,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
- ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
+ ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
From 6d87cfeb8d87b6faed8a7238cd8cf97df2acd99d Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Fri, 2 Feb 2024 18:08:33 +0530
Subject: [PATCH 15/53] fix: Percentage handling in queries (#39692)
* fix: Percentage handling in queries
* test: Account with percent sign
* chore: add test records
---
erpnext/accounts/doctype/account/test_account.py | 14 ++++++++++++++
erpnext/accounts/utils.py | 10 +++++-----
2 files changed, 19 insertions(+), 5 deletions(-)
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index eb3e00b388..7d0869b600 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
+from frappe.utils import nowdate
from erpnext.accounts.doctype.account.account import (
InvalidAccountMergeError,
@@ -324,6 +325,19 @@ class TestAccount(unittest.TestCase):
acc.account_currency = "USD"
self.assertRaises(frappe.ValidationError, acc.save)
+ def test_account_balance(self):
+ from erpnext.accounts.utils import get_balance_on
+
+ if not frappe.db.exists("Account", "Test Percent Account %5 - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Test Percent Account %5"
+ acc.parent_account = "Tax Assets - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+
+ balance = get_balance_on(account="Test Percent Account %5 - _TC", date=nowdate())
+ self.assertEqual(balance, 0)
+
def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 30700d09b8..64bc39a77b 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -237,7 +237,7 @@ def get_balance_on(
)
else:
- cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
+ cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center),))
if account:
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
@@ -258,7 +258,7 @@ def get_balance_on(
if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
in_account_currency = False
else:
- cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
+ cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
if account_type:
accounts = frappe.db.get_all(
@@ -278,11 +278,11 @@ def get_balance_on(
if party_type and party:
cond.append(
"""gle.party_type = %s and gle.party = %s """
- % (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))
+ % (frappe.db.escape(party_type), frappe.db.escape(party))
)
if company:
- cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
+ cond.append("""gle.company = %s """ % (frappe.db.escape(company)))
if account or (party_type and party) or account_type:
precision = get_currency_precision()
@@ -348,7 +348,7 @@ def get_count_on(account, fieldname, date):
% (acc.lft, acc.rgt)
)
else:
- cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
+ cond.append("""gle.account = %s """ % (frappe.db.escape(account),))
entries = frappe.db.sql(
"""
From 2caa2d677c09793f7097c3e76ebb4180a3f2a336 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Fri, 2 Feb 2024 17:45:50 +0530
Subject: [PATCH 16/53] refactor: ensure unique accounts for each Bank
Account's
---
.../accounts/doctype/bank_account/bank_account.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index ace4bb193d..df4bd5655a 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -9,6 +9,7 @@ from frappe.contacts.address_and_contact import (
load_address_and_contact,
)
from frappe.model.document import Document
+from frappe.utils import comma_and, get_link_to_form
class BankAccount(Document):
@@ -52,6 +53,17 @@ class BankAccount(Document):
def validate(self):
self.validate_company()
self.validate_iban()
+ self.validate_account()
+
+ def validate_account(self):
+ if self.account:
+ if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
+ frappe.throw(
+ _("'{0}' account is already used by {1}. Use another account.").format(
+ frappe.bold(self.account),
+ frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
+ )
+ )
def validate_company(self):
if self.is_company_account and not self.company:
From 93259cab1d65c2e0c92be4c7eff3c67e75afa5d9 Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Sat, 3 Feb 2024 04:42:28 +0100
Subject: [PATCH 17/53] fix(Bank Statement Import): scheduler not needed in dev
mode (#39678)
---
.../doctype/bank_statement_import/bank_statement_import.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 1a4747c55b..30e564c803 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -80,7 +80,8 @@ class BankStatementImport(DataImport):
from frappe.utils.background_jobs import is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
- if is_scheduler_inactive() and not frappe.flags.in_test:
+ run_now = frappe.flags.in_test or frappe.conf.developer_mode
+ if is_scheduler_inactive() and not run_now:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
job_id = f"bank_statement_import::{self.name}"
@@ -97,7 +98,7 @@ class BankStatementImport(DataImport):
google_sheets_url=self.google_sheets_url,
bank=self.bank,
template_options=self.template_options,
- now=frappe.conf.developer_mode or frappe.flags.in_test,
+ now=run_now,
)
return True
From a9a2ec81de73cde0995c837e12cd5dc79a584841 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Sat, 3 Feb 2024 10:58:31 +0530
Subject: [PATCH 18/53] refactor(test): generate uniq GL acc and Bank acc for
each test case
---
.../bank_transaction/test_bank_transaction.py | 72 ++++++++++++-------
1 file changed, 46 insertions(+), 26 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 7bb3f4183b..1fe3608f56 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -32,8 +32,16 @@ class TestBankTransaction(FrappeTestCase):
frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile()
- add_transactions()
- add_vouchers()
+
+ # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
+ uniq_identifier = frappe.generate_hash(length=10)
+ gl_account = create_gl_account("_Test Bank " + uniq_identifier)
+ bank_account = create_bank_account(
+ gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
+ )
+
+ add_transactions(bank_account=bank_account)
+ add_vouchers(gl_account=gl_account)
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
@@ -219,7 +227,9 @@ def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
-def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
+def create_bank_account(
+ bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account"
+):
try:
frappe.get_doc(
{
@@ -231,21 +241,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
pass
try:
- frappe.get_doc(
+ bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
- "account_name": "Checking Account",
+ "account_name": bank_account_name,
"bank": bank_name,
- "account": account_name,
+ "account": gl_account,
}
).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError:
pass
+ return bank_account.name
-def add_transactions():
- create_bank_account()
+def create_gl_account(gl_account_name="_Test Bank - _TC"):
+ gl_account = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "company": "_Test Company",
+ "parent_account": "Current Assets - _TC",
+ "account_type": "Bank",
+ "is_group": 0,
+ "account_name": gl_account_name,
+ }
+ ).insert()
+ return gl_account.name
+
+
+def add_transactions(bank_account="_Test Bank - _TC"):
doc = frappe.get_doc(
{
"doctype": "Bank Transaction",
@@ -253,7 +277,7 @@ def add_transactions():
"date": "2018-10-23",
"deposit": 1200,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -265,7 +289,7 @@ def add_transactions():
"date": "2018-10-23",
"deposit": 1700,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -277,7 +301,7 @@ def add_transactions():
"date": "2018-10-26",
"withdrawal": 690,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -289,7 +313,7 @@ def add_transactions():
"date": "2018-10-27",
"deposit": 3900,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
@@ -301,13 +325,13 @@ def add_transactions():
"date": "2018-10-27",
"withdrawal": 109080,
"currency": "INR",
- "bank_account": "Checking Account - Citi Bank",
+ "bank_account": bank_account,
}
).insert()
doc.submit()
-def add_vouchers():
+def add_vouchers(gl_account="_Test Bank - _TC"):
try:
frappe.get_doc(
{
@@ -323,7 +347,7 @@ def add_vouchers():
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
@@ -342,14 +366,14 @@ def add_vouchers():
pass
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200)
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Herr G Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()
pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700)
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Herr G Nov 18"
pe.reference_date = "2018-11-01"
pe.insert()
@@ -380,10 +404,10 @@ def add_vouchers():
pass
pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
- pi.cash_bank_account = "_Test Bank - _TC"
+ pi.cash_bank_account = gl_account
pi.insert()
pi.submit()
- pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account)
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.paid_amount = 690
@@ -392,7 +416,7 @@ def add_vouchers():
pe.submit()
si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900)
- pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account)
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
pe.insert()
@@ -415,16 +439,12 @@ def add_vouchers():
if not frappe.db.get_value(
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
):
- mode_of_payment.append(
- "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
- )
+ mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
- si.append(
- "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
- )
+ si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
si.insert()
si.submit()
From 322cdbaccf0b8697000aae4e56efa659a34fa8e5 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Sat, 3 Feb 2024 12:08:01 +0530
Subject: [PATCH 19/53] refactor(test): make use of test fixtures in Payment
Order
---
.../payment_order/test_payment_order.py | 32 ++++++++++++-------
1 file changed, 20 insertions(+), 12 deletions(-)
diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py
index 0dcb1794b9..60f288e1f0 100644
--- a/erpnext/accounts/doctype/payment_order/test_payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py
@@ -4,9 +4,13 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate
-from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
+from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
+ create_bank_account,
+ create_gl_account,
+)
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_payment_entry,
make_payment_order,
@@ -14,28 +18,32 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
-class TestPaymentOrder(unittest.TestCase):
+class TestPaymentOrder(FrappeTestCase):
def setUp(self):
- create_bank_account()
+ # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
+ uniq_identifier = frappe.generate_hash(length=10)
+ self.gl_account = create_gl_account("_Test Bank " + uniq_identifier)
+ self.bank_account = create_bank_account(
+ gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier
+ )
def tearDown(self):
- for bt in frappe.get_all("Payment Order"):
- doc = frappe.get_doc("Payment Order", bt.name)
- doc.cancel()
- doc.delete()
+ frappe.db.rollback()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry(
- "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
+ "Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account
)
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
- payment_entry.party_bank_account = "Checking Account - Citi Bank"
+ payment_entry.party_bank_account = self.bank_account
payment_entry.insert()
payment_entry.submit()
- doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
+ doc = create_payment_order_against_payment_entry(
+ payment_entry, "Payment Entry", self.bank_account
+ )
reference_doc = doc.get("references")[0]
self.assertEqual(reference_doc.reference_name, payment_entry.name)
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
@@ -43,13 +51,13 @@ class TestPaymentOrder(unittest.TestCase):
self.assertEqual(reference_doc.amount, 250)
-def create_payment_order_against_payment_entry(ref_doc, order_type):
+def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account):
payment_order = frappe.get_doc(
dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
- company_bank_account="Checking Account - Citi Bank",
+ company_bank_account=bank_account,
)
)
doc = make_payment_order(ref_doc.name, payment_order)
From d9a72c1e614a8d103ecb8eb13db26cff18981a52 Mon Sep 17 00:00:00 2001
From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Date: Sat, 3 Feb 2024 13:05:41 +0530
Subject: [PATCH 20/53] feat: reference for POS SI payments (#39523)
* feat: reference field in SI payment
* fix: document link for pos si
* refactor: pos invoice queries
---
.../doctype/bank_clearance/bank_clearance.py | 89 ++++++++++++-------
.../sales_invoice_payment.json | 11 ++-
.../sales_invoice_payment.py | 1 +
3 files changed, 67 insertions(+), 34 deletions(-)
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 4b97619f29..8a505a8dee 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -5,7 +5,9 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
+from pypika import Order
import erpnext
@@ -179,39 +181,62 @@ def get_payment_entries_for_bank_clearance(
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
- pos_sales_invoices = frappe.db.sql(
- """
- select
- "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
- si.posting_date, si.customer as against_account, sip.clearance_date,
- account.account_currency, 0 as credit
- from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
- where
- sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
- and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
- order by
- si.posting_date ASC, si.name DESC
- """,
- {"account": account, "from": from_date, "to": to_date},
- as_dict=1,
- )
+ si_payment = frappe.qb.DocType("Sales Invoice Payment")
+ si = frappe.qb.DocType("Sales Invoice")
+ acc = frappe.qb.DocType("Account")
- pos_purchase_invoices = frappe.db.sql(
- """
- select
- "Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
- pi.posting_date, pi.supplier as against_account, pi.clearance_date,
- account.account_currency, 0 as debit
- from `tabPurchase Invoice` pi, `tabAccount` account
- where
- pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
- and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
- order by
- pi.posting_date ASC, pi.name DESC
- """,
- {"account": account, "from": from_date, "to": to_date},
- as_dict=1,
- )
+ pos_sales_invoices = (
+ frappe.qb.from_(si_payment)
+ .inner_join(si)
+ .on(si_payment.parent == si.name)
+ .inner_join(acc)
+ .on(si_payment.account == acc.name)
+ .select(
+ ConstantColumn("Sales Invoice").as_("payment_document"),
+ si.name.as_("payment_entry"),
+ si_payment.reference_no.as_("cheque_number"),
+ si_payment.amount.as_("debit"),
+ si.posting_date,
+ si.customer.as_("against_account"),
+ si_payment.clearance_date,
+ acc.account_currency,
+ ConstantColumn(0).as_("credit"),
+ )
+ .where(
+ (si.docstatus == 1)
+ & (si_payment.account == account)
+ & (si.posting_date >= from_date)
+ & (si.posting_date <= to_date)
+ )
+ .orderby(si.posting_date)
+ .orderby(si.name, order=Order.desc)
+ ).run(as_dict=True)
+
+ pi = frappe.qb.DocType("Purchase Invoice")
+
+ pos_purchase_invoices = (
+ frappe.qb.from_(pi)
+ .inner_join(acc)
+ .on(pi.cash_bank_account == acc.name)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("payment_document"),
+ pi.name.as_("payment_entry"),
+ pi.paid_amount.as_("credit"),
+ pi.posting_date,
+ pi.supplier.as_("against_account"),
+ pi.clearance_date,
+ acc.account_currency,
+ ConstantColumn(0).as_("debit"),
+ )
+ .where(
+ (pi.docstatus == 1)
+ & (pi.cash_bank_account == account)
+ & (pi.posting_date >= from_date)
+ & (pi.posting_date <= to_date)
+ )
+ .orderby(pi.posting_date)
+ .orderby(pi.name, order=Order.desc)
+ ).run(as_dict=True)
entries = (
list(payment_entries)
diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
index 5ab46b7fd5..bd59f65dd4 100644
--- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
+++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json
@@ -8,6 +8,7 @@
"default",
"mode_of_payment",
"amount",
+ "reference_no",
"column_break_3",
"account",
"type",
@@ -75,11 +76,16 @@
"hidden": 1,
"label": "Default",
"read_only": 1
+ },
+ {
+ "fieldname": "reference_no",
+ "fieldtype": "Data",
+ "label": "Reference No"
}
],
"istable": 1,
"links": [],
- "modified": "2020-08-03 12:45:39.986598",
+ "modified": "2024-01-23 16:20:06.436979",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Payment",
@@ -87,5 +93,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py
index 57d0142406..e460a01155 100644
--- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py
+++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.py
@@ -23,6 +23,7 @@ class SalesInvoicePayment(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
+ reference_no: DF.Data | None
type: DF.ReadOnly | None
# end: auto-generated types
From c81d597ca510706cf3ca7230f21b1630e7515bf5 Mon Sep 17 00:00:00 2001
From: Vishnu VS
Date: Sun, 4 Feb 2024 12:17:18 +0530
Subject: [PATCH 21/53] fix(work order): resolve type error during job card
creation (#39713)
fix: type error
---
erpnext/manufacturing/doctype/work_order/work_order.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index aa7bc5bf76..39beb361de 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1511,14 +1511,14 @@ def get_serial_nos_for_work_order(work_order, production_item):
def validate_operation_data(row):
- if row.get("qty") <= 0:
+ if flt(row.get("qty")) <= 0:
frappe.throw(
_("Quantity to Manufacture can not be zero for the operation {0}").format(
frappe.bold(row.get("operation"))
)
)
- if row.get("qty") > row.get("pending_qty"):
+ if flt(row.get("qty")) > flt(row.get("pending_qty")):
frappe.throw(
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
frappe.bold(row.get("operation")),
From 9fafc83632c4e5c73186bda038b892b7a43d16b8 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Sun, 4 Feb 2024 10:42:31 +0530
Subject: [PATCH 22/53] fix: use old serial / batch fields to make serial batch
bundle
---
.../pos_invoice_item/pos_invoice_item.json | 33 ++-
.../pos_invoice_item/pos_invoice_item.py | 1 +
.../purchase_invoice/purchase_invoice.py | 1 +
.../purchase_invoice_item.json | 49 ++--
.../purchase_invoice_item.py | 1 +
.../doctype/sales_invoice/sales_invoice.py | 1 +
.../sales_invoice_item.json | 35 ++-
.../sales_invoice_item/sales_invoice_item.py | 1 +
.../asset_capitalization.py | 1 +
.../asset_capitalization_stock_item.json | 31 +-
.../asset_capitalization_stock_item.py | 1 +
erpnext/controllers/stock_controller.py | 48 +++
erpnext/public/js/controllers/transaction.js | 40 ++-
erpnext/public/js/utils/barcode_scanner.js | 274 ++++--------------
.../doctype/sales_order/sales_order.py | 8 +-
.../doctype/delivery_note/delivery_note.py | 2 +
.../delivery_note_item.json | 30 +-
.../delivery_note_item/delivery_note_item.py | 1 +
.../doctype/packed_item/packed_item.json | 33 ++-
.../stock/doctype/packed_item/packed_item.py | 1 +
erpnext/stock/doctype/pick_list/pick_list.js | 1 -
.../stock/doctype/pick_list/pick_list.json | 4 +-
erpnext/stock/doctype/pick_list/pick_list.py | 150 +++++++---
.../pick_list_item/pick_list_item.json | 32 +-
.../doctype/pick_list_item/pick_list_item.py | 1 +
.../purchase_receipt/purchase_receipt.py | 1 +
.../purchase_receipt_item.json | 21 +-
.../purchase_receipt_item.py | 1 +
.../serial_and_batch_bundle.py | 5 +-
.../stock/doctype/stock_entry/stock_entry.py | 1 +
.../stock_entry_detail.json | 32 +-
.../stock_entry_detail/stock_entry_detail.py | 1 +
.../stock_reconciliation.py | 1 +
.../stock_reconciliation_item.json | 33 ++-
.../stock_reconciliation_item.py | 2 +
.../stock_settings/stock_settings.json | 9 +-
.../doctype/stock_settings/stock_settings.py | 2 +
erpnext/stock/serial_batch_bundle.py | 61 ++++
.../subcontracting_receipt.py | 1 +
.../subcontracting_receipt_item.json | 36 ++-
.../subcontracting_receipt_item.py | 1 +
.../subcontracting_receipt_supplied_item.json | 33 ++-
.../subcontracting_receipt_supplied_item.py | 1 +
43 files changed, 652 insertions(+), 370 deletions(-)
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 5a281aaa4f..ad2889d0a0 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -80,13 +80,16 @@
"target_warehouse",
"quality_inspection",
"serial_and_batch_bundle",
- "batch_no",
+ "use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
- "serial_no",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
+ "section_break_tlhi",
+ "serial_no",
+ "column_break_ciit",
+ "batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -628,13 +631,13 @@
"options": "Quality Inspection"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"hidden": 1,
"label": "Batch No",
"options": "Batch",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "col_break5",
@@ -649,14 +652,14 @@
"print_hide": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"in_list_view": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
- "oldfieldtype": "Small Text",
- "read_only": 1
+ "oldfieldtype": "Small Text"
},
{
"fieldname": "item_tax_rate",
@@ -824,17 +827,33 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_tlhi",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_ciit",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:33:22.585715",
+ "modified": "2024-02-04 16:36:25.665743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
index e2a62f1336..55a577b0c5 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.py
@@ -82,6 +82,7 @@ class POSInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index c4e09b47f1..c68ff838dd 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -696,6 +696,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_using_old_serial_batch_fields()
self.update_stock_ledger()
if self.is_old_subcontracting_flow:
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 26984d96ef..3ee4214ae7 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -62,16 +62,19 @@
"rm_supp_cost",
"warehouse_section",
"warehouse",
- "from_warehouse",
- "quality_inspection",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"col_br_wh",
+ "from_warehouse",
+ "quality_inspection",
"rejected_warehouse",
"rejected_serial_and_batch_bundle",
- "batch_no",
+ "section_break_rqbe",
+ "serial_no",
"rejected_serial_no",
+ "column_break_vbbb",
+ "batch_no",
"manufacture_details",
"manufacturer",
"column_break_13",
@@ -440,13 +443,11 @@
"print_hide": 1
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
- "hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -454,21 +455,18 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
- "hidden": 1,
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
"label": "Rejected Serial No",
"no_copy": 1,
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "accounting",
@@ -891,7 +889,7 @@
"label": "Apply TDS"
},
{
- "depends_on": "eval:parent.update_stock == 1",
+ "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -901,7 +899,7 @@
"search_index": 1
},
{
- "depends_on": "eval:parent.update_stock == 1",
+ "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -916,16 +914,31 @@
"options": "Asset"
},
{
- "depends_on": "eval:parent.update_stock === 1",
+ "depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
+ "fieldname": "section_break_rqbe",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_vbbb",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2024-01-21 19:46:25.537861",
+ "modified": "2024-02-04 14:11:52.742228",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
index e48d22379a..ccbc34749d 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
@@ -88,6 +88,7 @@ class PurchaseInvoiceItem(Document):
stock_uom_rate: DF.Currency
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link | None
weight_per_unit: DF.Float
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index c381b8a910..abc0694a63 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -447,6 +447,7 @@ class SalesInvoice(SellingController):
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
# this sequence because outstanding may get -ve
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index ec9e792d7d..d06c7861da 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -83,14 +83,17 @@
"quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle",
- "batch_no",
- "incoming_rate",
+ "use_serial_batch_fields",
"col_break5",
"allow_zero_valuation_rate",
- "serial_no",
+ "incoming_rate",
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
+ "section_break_eoec",
+ "serial_no",
+ "column_break_ytgd",
+ "batch_no",
"edit_references",
"sales_order",
"so_detail",
@@ -600,12 +603,11 @@
"options": "Quality Inspection"
},
{
+ "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
- "hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -621,13 +623,12 @@
"print_hide": 1
},
{
+ "depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
- "hidden": 1,
"label": "Serial No",
"oldfieldname": "serial_no",
- "oldfieldtype": "Small Text",
- "read_only": 1
+ "oldfieldtype": "Small Text"
},
{
"fieldname": "item_group",
@@ -891,6 +892,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -904,12 +906,27 @@
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
+ "fieldname": "section_break_eoec",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_ytgd",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-12-29 13:03:14.121298",
+ "modified": "2024-02-04 11:52:16.106541",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
index 80f67748f4..c71d08e7f7 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
@@ -86,6 +86,7 @@ class SalesInvoiceItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 5e251a5658..c9ed806fe4 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -126,6 +126,7 @@ class AssetCapitalization(StockController):
self.create_target_asset()
def on_submit(self):
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.update_target_asset()
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
index 26e1c3c270..d301454be8 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
@@ -18,9 +18,12 @@
"amount",
"batch_and_serial_no_section",
"serial_and_batch_bundle",
+ "use_serial_batch_fields",
"column_break_13",
- "batch_no",
+ "section_break_bfqc",
"serial_no",
+ "column_break_mbuv",
+ "batch_no",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
@@ -39,13 +42,13 @@
"reqd": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"options": "Batch",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "section_break_6",
@@ -102,12 +105,12 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Serial No",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "item_code",
@@ -148,18 +151,34 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_bfqc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_mbuv",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-04-06 01:10:17.947952",
+ "modified": "2024-02-04 16:41:09.239762",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization Stock Item",
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
index 122cbb600d..d2b075c3e6 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py
@@ -27,6 +27,7 @@ class AssetCapitalizationStockItem(Document):
serial_no: DF.SmallText | None
stock_qty: DF.Float
stock_uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link
# end: auto-generated types
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 11e9f9f441..17d3369e9d 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -126,6 +126,54 @@ class StockController(AccountsController):
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
+ def make_bundle_using_old_serial_batch_fields(self):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ for row in self.items:
+ if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
+ continue
+
+ if not row.use_serial_batch_fields and (
+ row.serial_no or row.batch_no or row.get("rejected_serial_no")
+ ):
+ frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
+
+ if row.use_serial_batch_fields and (
+ not row.serial_and_batch_bundle or not row.get("rejected_serial_and_batch_bundle")
+ ):
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": row.stock_qty,
+ "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward",
+ "company": self.company,
+ "is_rejected": 1 if row.get("rejected_warehouse") else 0,
+ "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
+ "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ "use_serial_batch_fields": row.use_serial_batch_fields,
+ }
+ ).make_serial_and_batch_bundle()
+
+ if sn_doc.is_rejected:
+ row.rejected_serial_and_batch_bundle = sn_doc.name
+ row.db_set("rejected_serial_and_batch_bundle", sn_doc.name)
+ else:
+ row.serial_and_batch_bundle = sn_doc.name
+ row.db_set("serial_and_batch_bundle", sn_doc.name)
+
+ def set_use_serial_batch_fields(self):
+ if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
+ for row in self.items:
+ row.use_serial_batch_fields = 1
+
def get_gl_entries(
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
):
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 26795f7b98..18b963dd7b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -7,6 +7,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
super.setup();
let me = this;
+ this.set_fields_onload_for_line_item();
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frappe.flags.hide_serial_batch_dialog = true;
@@ -105,6 +106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) {
+ debugger
var item = frappe.get_doc(cdt, cdn);
if (!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
@@ -118,6 +120,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.from_warehouse = frm.doc.set_from_warehouse;
}
+ if (item.docstatus === 0
+ && frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
+ && cint(frappe.user_defaults?.use_serial_batch_fields) === 1
+ ) {
+ frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
+ }
+
erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
}
});
@@ -222,7 +231,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
};
});
}
+ }
+ set_fields_onload_for_line_item() {
+ if (this.frm.is_new && this.frm.doc?.items) {
+ this.frm.doc.items.forEach(item => {
+ if (item.docstatus === 0
+ && frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
+ && cint(frappe.user_defaults?.use_serial_batch_fields) === 1
+ ) {
+ frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
+ }
+ })
+ }
}
toggle_enable_for_stock_uom(field) {
@@ -462,6 +483,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.doc.doctype === 'Delivery Note') {
show_batch_dialog = 1;
}
+
+ if (show_batch_dialog && item.use_serial_batch_fields === 1) {
+ show_batch_dialog = 0;
+ }
+
item.barcode = null;
@@ -1242,20 +1268,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}
- sync_bundle_data() {
- let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
-
- if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
- const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
- barcode_scanner.sync_bundle_data();
- barcode_scanner.remove_item_from_localstorage();
- }
- }
-
- before_save(doc) {
- this.sync_bundle_data();
- }
-
service_start_date(frm, cdt, cdn) {
var child = locals[cdt][cdn];
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index aacab0fe6c..4d1c0c1ad3 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -1,12 +1,15 @@
erpnext.utils.BarcodeScanner = class BarcodeScanner {
constructor(opts) {
this.frm = opts.frm;
+ // frappe.flags.trigger_from_barcode_scanner is used for custom scripts
// field from which to capture input of scanned data
this.scan_field_name = opts.scan_field_name || "scan_barcode";
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];
this.barcode_field = opts.barcode_field || "barcode";
+ this.serial_no_field = opts.serial_no_field || "serial_no";
+ this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
@@ -105,53 +108,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false;
}
- if (serial_no) {
- this.is_duplicate_serial_no(row, item_code, serial_no)
- .then((is_duplicate) => {
- if (!is_duplicate) {
- this.run_serially_tasks(row, data, resolve);
- } else {
- this.clean_up();
- reject();
- return;
- }
- });
- } else {
- this.run_serially_tasks(row, data, resolve);
+ if (this.is_duplicate_serial_no(row, serial_no)) {
+ this.clean_up();
+ reject();
+ return;
}
-
+ frappe.run_serially([
+ () => this.set_selector_trigger_flag(data),
+ () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
+ this.show_scan_message(row.idx, row.item_code, qty);
+ }),
+ () => this.set_barcode_uom(row, uom),
+ () => this.set_serial_no(row, serial_no),
+ () => this.set_batch_no(row, batch_no),
+ () => this.set_barcode(row, barcode),
+ () => this.clean_up(),
+ () => this.revert_selector_flag(),
+ () => resolve(row)
+ ]);
});
}
- run_serially_tasks(row, data, resolve) {
- const {item_code, barcode, batch_no, serial_no, uom} = data;
+ // batch and serial selector is reduandant when all info can be added by scan
+ // this flag on item row is used by transaction.js to avoid triggering selector
+ set_selector_trigger_flag(data) {
+ const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
- frappe.run_serially([
- () => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
- () => this.set_barcode(row, barcode),
- () => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
- this.show_scan_message(row.idx, row.item_code, qty);
- }),
- () => this.set_barcode_uom(row, uom),
- () => this.clean_up(),
- () => {
- if (row.serial_and_batch_bundle && !this.frm.is_new()) {
- this.frm.save();
- }
+ const require_selecting_batch = has_batch_no && !batch_no;
+ const require_selecting_serial = has_serial_no && !serial_no;
- frappe.flags.trigger_from_barcode_scanner = false;
- },
- () => resolve(row),
- ]);
+ if (!(require_selecting_batch || require_selecting_serial)) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ }
+
+ revert_selector_flag() {
+ frappe.flags.hide_serial_batch_dialog = false;
+ frappe.flags.trigger_from_barcode_scanner = false;
}
set_item(row, item_code, barcode, batch_no, serial_no) {
return new Promise(resolve => {
const increment = async (value = 1) => {
- const item_data = {item_code: item_code};
- item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
+ const item_data = {item_code: item_code, use_serial_batch_fields: 1.0};
frappe.flags.trigger_from_barcode_scanner = true;
+ item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
await frappe.model.set_value(row.doctype, row.name, item_data);
return value;
};
@@ -160,6 +162,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value).then((value) => resolve(value));
});
+ } else if (this.frm.has_items) {
+ this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
} else {
increment().then((value) => resolve(value));
}
@@ -182,8 +186,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.model.set_value(row.doctype, row.name, item_data);
frappe.run_serially([
+ () => this.set_batch_no(row, this.dialog.get_value("batch_no")),
() => this.set_barcode(row, this.dialog.get_value("barcode")),
- () => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
+ () => this.set_serial_no(row, this.dialog.get_value("serial_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
@@ -337,144 +342,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
- async set_serial_and_batch(row, item_code, serial_no, batch_no) {
- if (this.frm.is_new() || !row.serial_and_batch_bundle) {
- this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
- } else if(row.serial_and_batch_bundle) {
- frappe.call({
- method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
- args: {
- bundle_id: row.serial_and_batch_bundle,
- serial_no: serial_no,
- batch_no: batch_no,
- },
- })
- }
- }
+ async set_serial_no(row, serial_no) {
+ if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
+ const existing_serial_nos = row[this.serial_no_field];
+ let new_serial_nos = "";
- get_key_for_localstorage() {
- let parts = this.frm.doc.name.split("-");
- return parts[parts.length - 1] + this.frm.doc.doctype;
- }
-
- update_localstorage_scanned_data() {
- let docname = this.frm.doc.name
- if (localStorage[docname]) {
- let items = JSON.parse(localStorage[docname]);
- let existing_items = this.frm.doc.items.map(d => d.item_code);
- if (!existing_items.length) {
- localStorage.removeItem(docname);
- return;
+ if (!!existing_serial_nos) {
+ new_serial_nos = existing_serial_nos + "\n" + serial_no;
+ } else {
+ new_serial_nos = serial_no;
}
-
- for (let item_code in items) {
- if (!existing_items.includes(item_code)) {
- delete items[item_code];
- }
- }
-
- localStorage[docname] = JSON.stringify(items);
+ await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
}
}
- async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
- let docname = this.frm.doc.name
-
- let entries = JSON.parse(localStorage.getItem(docname));
- if (!entries) {
- entries = {};
- }
-
- let key = item_code;
- if (!entries[key]) {
- entries[key] = [];
- }
-
- let existing_row = [];
- if (!serial_no && batch_no) {
- existing_row = entries[key].filter((e) => e.batch_no === batch_no);
- if (existing_row.length) {
- existing_row[0].qty += 1;
- }
- } else if (serial_no) {
- existing_row = entries[key].filter((e) => e.serial_no === serial_no);
- if (existing_row.length) {
- frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
- }
- }
-
- if (!existing_row.length) {
- entries[key].push({
- "serial_no": serial_no,
- "batch_no": batch_no,
- "qty": 1
- });
- }
-
- localStorage.setItem(docname, JSON.stringify(entries));
-
- // Auto remove from localstorage after 1 hour
- setTimeout(() => {
- localStorage.removeItem(docname);
- }, 3600000)
- }
-
- remove_item_from_localstorage() {
- let docname = this.frm.doc.name;
- if (localStorage[docname]) {
- localStorage.removeItem(docname);
- }
- }
-
- async sync_bundle_data() {
- let docname = this.frm.doc.name;
-
- if (localStorage[docname]) {
- let entries = JSON.parse(localStorage[docname]);
- if (entries) {
- for (let entry in entries) {
- let row = this.frm.doc.items.filter((item) => {
- if (item.item_code === entry) {
- return true;
- }
- })[0];
-
- if (row) {
- this.create_serial_and_batch_bundle(row, entries, entry)
- .then(() => {
- if (!entries) {
- localStorage.removeItem(docname);
- }
- });
- }
- }
- }
- }
- }
-
- async create_serial_and_batch_bundle(row, entries, key) {
- frappe.call({
- method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
- args: {
- entries: entries[key],
- child_row: row,
- doc: this.frm.doc,
- warehouse: row.warehouse,
- do_not_save: 1
- },
- callback: function(r) {
- row.serial_and_batch_bundle = r.message.name;
- delete entries[key];
- }
- })
- }
-
async set_barcode_uom(row, uom) {
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
}
}
+ async set_batch_no(row, batch_no) {
+ if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
+ await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
+ }
+ }
+
async set_barcode(row, barcode) {
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
@@ -490,58 +383,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}
- async is_duplicate_serial_no(row, item_code, serial_no) {
- let is_duplicate = false;
- const promise = new Promise((resolve, reject) => {
- if (this.frm.is_new() || !row.serial_and_batch_bundle) {
- is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
- if (is_duplicate) {
- this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
- }
+ is_duplicate_serial_no(row, serial_no) {
+ const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
- resolve(is_duplicate);
- } else if (row.serial_and_batch_bundle) {
- this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
- if (r.message) {
- this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
- }
-
- is_duplicate = r.message;
- resolve(is_duplicate);
- })
- }
- });
-
- return await promise;
- }
-
- check_duplicate_serial_no_in_db(row, serial_no, response) {
- frappe.call({
- method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
- args: {
- serial_no: serial_no,
- bundle_id: row.serial_and_batch_bundle
- },
- callback(r) {
- response(r);
- }
- });
- }
-
- check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
- let docname = this.frm.doc.name
- let entries = JSON.parse(localStorage.getItem(docname));
-
- if (!entries) {
- return false;
+ if (is_duplicate) {
+ this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
-
- let existing_row = [];
- if (entries[item_code]) {
- existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
- }
-
- return existing_row.length;
+ return is_duplicate;
}
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
@@ -587,4 +435,4 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
show_alert(msg, indicator, duration=3) {
frappe.show_alert({message: msg, indicator: indicator}, duration);
}
-};
+};
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 79f24d1160..9661bac8ad 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -906,6 +906,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
+ target.run_method("set_use_serial_batch_fields")
if source.company_address:
target.update({"company_address": source.company_address})
@@ -1026,6 +1027,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
+ target.run_method("set_use_serial_batch_fields")
if source.company_address:
target.update({"company_address": source.company_address})
@@ -1608,7 +1610,11 @@ def create_pick_list(source_name, target_doc=None):
"Sales Order",
source_name,
{
- "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}},
+ "Sales Order": {
+ "doctype": "Pick List",
+ "field_map": {"set_warehouse": "parent_warehouse"},
+ "validation": {"docstatus": ["=", 1]},
+ },
"Sales Order Item": {
"doctype": "Pick List Item",
"field_map": {"parent": "sales_order", "name": "sales_order_item"},
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 58990d4838..4eacbc1541 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -398,6 +398,8 @@ class DeliveryNote(SellingController):
self.check_credit_limit()
elif self.issue_credit_note:
self.make_return_invoice()
+
+ self.make_bundle_using_old_serial_batch_fields()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index a44b9ac44b..247672fe12 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -80,8 +80,11 @@
"section_break_40",
"pick_serial_and_batch",
"serial_and_batch_bundle",
+ "use_serial_batch_fields",
"column_break_eaoe",
+ "section_break_qyjv",
"serial_no",
+ "column_break_rxvc",
"batch_no",
"available_qty_section",
"actual_batch_qty",
@@ -850,6 +853,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -859,6 +863,7 @@
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
@@ -874,27 +879,40 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
- "hidden": 1,
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
- "hidden": 1,
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_qyjv",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_rxvc",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-14 18:37:38.638144",
+ "modified": "2024-02-04 14:10:31.750340",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
index c11c4103e5..b76f742972 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
@@ -82,6 +82,7 @@ class DeliveryNoteItem(Document):
target_warehouse: DF.Link | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
weight_per_unit: DF.Float
weight_uom: DF.Link | None
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 5dd8934d43..0b006ab363 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -20,9 +20,12 @@
"uom",
"section_break_9",
"pick_serial_and_batch",
- "serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"column_break_11",
+ "serial_and_batch_bundle",
+ "section_break_bgys",
+ "serial_no",
+ "column_break_qlha",
"batch_no",
"actual_batch_qty",
"section_break_13",
@@ -118,10 +121,10 @@
"fieldtype": "Section Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
"fieldname": "column_break_11",
@@ -131,8 +134,7 @@
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
- "options": "Batch",
- "read_only": 1
+ "options": "Batch"
},
{
"fieldname": "section_break_13",
@@ -259,6 +261,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -267,16 +270,32 @@
"print_hide": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_bgys",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_qlha",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-04-28 13:16:38.460806",
+ "modified": "2024-02-04 16:30:44.263964",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index ed667c2b99..c115e33e17 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -47,6 +47,7 @@ class PackedItem(Document):
serial_no: DF.Text | None
target_warehouse: DF.Link | None
uom: DF.Link | None
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index afd6ce8138..aa0e125496 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -16,7 +16,6 @@ frappe.ui.form.on('Pick List', {
frm.set_query('parent_warehouse', () => {
return {
filters: {
- 'is_group': 1,
'company': frm.doc.company
}
};
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index 7259dc00a8..bd84aadef7 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -51,7 +51,7 @@
"description": "Items under this warehouse will be suggested",
"fieldname": "parent_warehouse",
"fieldtype": "Link",
- "label": "Parent Warehouse",
+ "label": "Warehouse",
"options": "Warehouse"
},
{
@@ -188,7 +188,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2023-01-24 10:33:43.244476",
+ "modified": "2024-02-01 16:17:44.877426",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 758448af79..f2edeea56d 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
-from frappe.utils import cint, floor, flt
+from frappe.utils import ceil, cint, floor, flt
from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import (
@@ -122,11 +122,43 @@ class PickList(Document):
def on_submit(self):
self.validate_serial_and_batch_bundle()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_status()
self.update_bundle_picked_qty()
self.update_reference_qty()
self.update_sales_order_picking_status()
+ def make_bundle_using_old_serial_batch_fields(self):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ for row in self.locations:
+ if not row.serial_no and not row.batch_no:
+ continue
+
+ if not row.use_serial_batch_fields and (row.serial_no or row.batch_no):
+ frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
+
+ if row.use_serial_batch_fields and (not row.serial_and_batch_bundle):
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": row.stock_qty,
+ "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward",
+ "company": self.company,
+ "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
+ "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ "use_serial_batch_fields": row.use_serial_batch_fields,
+ }
+ ).make_serial_and_batch_bundle()
+
+ row.serial_and_batch_bundle = sn_doc.name
+ row.db_set("serial_and_batch_bundle", sn_doc.name)
+
def on_update_after_submit(self) -> None:
if self.has_reserved_stock():
msg = _(
@@ -317,8 +349,12 @@ class PickList(Document):
self.item_location_map = frappe._dict()
from_warehouses = None
- if self.parent_warehouse:
+ if self.parent_warehouse and frappe.get_cached_value(
+ "Warehouse", self.parent_warehouse, "is_group"
+ ):
from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
+ elif self.parent_warehouse:
+ from_warehouses = [self.parent_warehouse]
# Create replica before resetting, to handle empty table on update after submit.
locations_replica = self.get("locations")
@@ -351,13 +387,13 @@ class PickList(Document):
for row in locations:
location = item_doc.as_dict()
location.update(row)
+ bundle = location.serial_and_batch_bundle or location.serial_no or location.batch_no
key = (
location.item_code,
location.warehouse,
location.uom,
- location.batch_no,
- location.serial_no,
location.sales_order_item or location.material_request_item,
+ bundle,
)
if key not in updated_locations:
@@ -645,7 +681,9 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
"qty": qty,
"stock_qty": stock_qty,
"warehouse": item_location.warehouse,
- "serial_and_batch_bundle": item_location.serial_and_batch_bundle,
+ "serial_no": item_location.serial_no,
+ "batch_no": item_location.batch_no,
+ "use_serial_batch_fields": 1,
}
)
)
@@ -673,6 +711,7 @@ def get_available_item_locations(
company,
ignore_validation=False,
picked_item_details=None,
+ consider_rejected_warehouses=False,
):
locations = []
total_picked_qty = (
@@ -681,7 +720,16 @@ def get_available_item_locations(
has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no")
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
- if has_serial_no:
+ if has_batch_no and has_serial_no:
+ locations = get_available_item_locations_for_serial_and_batched_item(
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty,
+ consider_rejected_warehouses=consider_rejected_warehouses,
+ )
+ elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty
)
@@ -724,6 +772,49 @@ def get_available_item_locations(
return locations
+def get_available_item_locations_for_serial_and_batched_item(
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty=0,
+ consider_rejected_warehouses=False,
+):
+ # Get batch nos by FIFO
+ locations = get_available_item_locations_for_batched_item(
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ consider_rejected_warehouses=consider_rejected_warehouses,
+ )
+
+ if locations:
+ sn = frappe.qb.DocType("Serial No")
+ conditions = (sn.item_code == item_code) & (sn.company == company)
+
+ for location in locations:
+ location.qty = (
+ required_qty if location.qty > required_qty else location.qty
+ ) # if extra qty in batch
+
+ serial_nos = (
+ frappe.qb.from_(sn)
+ .select(sn.name)
+ .where(
+ (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
+ )
+ .orderby(sn.purchase_date)
+ .limit(ceil(location.qty + total_picked_qty))
+ ).run(as_dict=True)
+
+ serial_nos = [sn.name for sn in serial_nos]
+ location.serial_no = serial_nos
+ location.qty = len(serial_nos)
+
+ return locations
+
+
def get_available_item_locations_for_serialized_item(
item_code, from_warehouses, required_qty, company, total_picked_qty=0
):
@@ -760,25 +851,12 @@ def get_available_item_locations_for_serialized_item(
for warehouse, serial_nos in warehouse_serial_nos_map.items():
qty = len(serial_nos)
- bundle_doc = SerialBatchCreation(
- {
- "item_code": item_code,
- "warehouse": warehouse,
- "voucher_type": "Pick List",
- "total_qty": qty * -1,
- "serial_nos": serial_nos,
- "type_of_transaction": "Outward",
- "company": company,
- "do_not_submit": True,
- }
- ).make_serial_and_batch_bundle()
-
locations.append(
{
"qty": qty,
"warehouse": warehouse,
"item_code": item_code,
- "serial_and_batch_bundle": bundle_doc.name,
+ "serial_nos": serial_nos,
}
)
@@ -808,29 +886,15 @@ def get_available_item_locations_for_batched_item(
warehouse_wise_batches[d.warehouse][d.batch_no] += d.qty
for warehouse, batches in warehouse_wise_batches.items():
- qty = sum(batches.values())
-
- bundle_doc = SerialBatchCreation(
- {
- "item_code": item_code,
- "warehouse": warehouse,
- "voucher_type": "Pick List",
- "total_qty": qty * -1,
- "batches": batches,
- "type_of_transaction": "Outward",
- "company": company,
- "do_not_submit": True,
- }
- ).make_serial_and_batch_bundle()
-
- locations.append(
- {
- "qty": qty,
- "warehouse": warehouse,
- "item_code": item_code,
- "serial_and_batch_bundle": bundle_doc.name,
- }
- )
+ for batch_no, qty in batches.items():
+ locations.append(
+ {
+ "qty": qty,
+ "warehouse": warehouse,
+ "item_code": item_code,
+ "batch_no": batch_no,
+ }
+ )
return locations
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index e8e4afc6e3..c8001fd508 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -24,8 +24,11 @@
"serial_no_and_batch_section",
"pick_serial_and_batch",
"serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"column_break_20",
+ "section_break_ecxc",
+ "serial_no",
+ "column_break_belw",
"batch_no",
"column_break_15",
"sales_order",
@@ -72,19 +75,17 @@
"read_only": 1
},
{
- "depends_on": "serial_no",
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
- "depends_on": "batch_no",
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -195,6 +196,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -204,6 +206,7 @@
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
@@ -218,11 +221,26 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_ecxc",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_belw",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2023-07-26 12:54:15.785962",
+ "modified": "2024-02-04 16:12:16.257951",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py
index 6e5a94e446..f3f6298a30 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py
@@ -37,6 +37,7 @@ class PickListItem(Document):
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
uom: DF.Link | None
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index bf6080bf23..14d5b947fa 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -369,6 +369,7 @@ class PurchaseReceipt(BuyingController):
else:
self.db_set("status", "Completed")
+ self.make_bundle_using_old_serial_batch_fields()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating ordered qty, reserved_qty_for_subcontract in bin
# depends upon updated ordered qty in PO
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 9bd692ad61..6b01047f00 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -94,6 +94,7 @@
"section_break_45",
"add_serial_batch_bundle",
"serial_and_batch_bundle",
+ "use_serial_batch_fields",
"col_break5",
"add_serial_batch_for_rejected_qty",
"rejected_serial_and_batch_bundle",
@@ -1003,6 +1004,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -1020,24 +1022,22 @@
{
"fieldname": "serial_no",
"fieldtype": "Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
"fieldname": "rejected_serial_no",
"fieldtype": "Text",
- "label": "Rejected Serial No",
- "read_only": 1
+ "label": "Rejected Serial No"
},
{
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -1045,11 +1045,13 @@
"options": "Serial and Batch Bundle"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_3vxt",
"fieldtype": "Section Break"
},
@@ -1058,6 +1060,7 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -1098,12 +1101,18 @@
"read_only": 1,
"report_hide": 1,
"search_index": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-12-25 22:32:09.801965",
+ "modified": "2024-02-04 11:48:06.653771",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
index aed8d21dae..3c6dcdca48 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
@@ -99,6 +99,7 @@ class PurchaseReceiptItem(Document):
supplier_part_no: DF.Data | None
total_weight: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link | None
weight_per_unit: DF.Float
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 9cad8f62b8..ea33c54544 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -1677,7 +1677,10 @@ def get_reserved_batches_for_sre(kwargs) -> dict:
query = query.where(sb_entry.batch_no == kwargs.batch_no)
if kwargs.warehouse:
- query = query.where(sre.warehouse == kwargs.warehouse)
+ if isinstance(kwargs.warehouse, list):
+ query = query.where(sre.warehouse.isin(kwargs.warehouse))
+ else:
+ query = query.where(sre.warehouse == kwargs.warehouse)
if kwargs.ignore_voucher_nos:
query = query.where(sre.name.notin(kwargs.ignore_voucher_nos))
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index faccfa3a3d..10e3522579 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -274,6 +274,7 @@ class StockEntry(StockController):
def on_submit(self):
self.validate_closed_subcontracting_order()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.update_work_order()
self.validate_subcontract_order()
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index bd84a2b0d9..bd11d0b861 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -47,9 +47,12 @@
"amount",
"serial_no_batch",
"add_serial_batch_bundle",
- "serial_and_batch_bundle",
+ "use_serial_batch_fields",
"col_break4",
+ "serial_and_batch_bundle",
+ "section_break_rdtg",
"serial_no",
+ "column_break_prps",
"batch_no",
"accounting",
"expense_account",
@@ -289,27 +292,27 @@
"no_copy": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
"no_copy": 1,
"oldfieldname": "serial_no",
- "oldfieldtype": "Text",
- "read_only": 1
+ "oldfieldtype": "Text"
},
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"oldfieldname": "batch_no",
"oldfieldtype": "Link",
- "options": "Batch",
- "read_only": 1
+ "options": "Batch"
},
{
"depends_on": "eval:parent.inspection_required && doc.t_warehouse",
@@ -573,24 +576,41 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_rdtg",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_prps",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-01-12 11:56:04.626103",
+ "modified": "2024-02-04 16:16:47.606270",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
index a6dd0faadf..47c443c519 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.py
@@ -63,6 +63,7 @@ class StockEntryDetail(Document):
transfer_qty: DF.Float
transferred_qty: DF.Float
uom: DF.Link
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
# end: auto-generated types
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 788ae0d3ab..cc8a7c57b3 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -99,6 +99,7 @@ class StockReconciliation(StockController):
)
def on_submit(self):
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index fc4ae6a5fa..27693d2f1b 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -19,11 +19,14 @@
"allow_zero_valuation_rate",
"serial_no_and_batch_section",
"add_serial_batch_bundle",
- "serial_and_batch_bundle",
- "batch_no",
+ "use_serial_batch_fields",
"column_break_11",
+ "serial_and_batch_bundle",
"current_serial_and_batch_bundle",
+ "section_break_lypk",
"serial_no",
+ "column_break_eefq",
+ "batch_no",
"section_break_3",
"current_qty",
"current_amount",
@@ -103,10 +106,10 @@
"label": "Serial No and Batch"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Long Text",
- "label": "Serial No",
- "read_only": 1
+ "label": "Serial No"
},
{
"fieldname": "column_break_11",
@@ -171,11 +174,11 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
- "read_only": 1,
"search_index": 1
},
{
@@ -195,6 +198,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial / Batch Bundle",
@@ -204,6 +208,7 @@
"search_index": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "current_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Current Serial / Batch Bundle",
@@ -212,6 +217,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -222,11 +228,26 @@
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_lypk",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_eefq",
+ "fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
- "modified": "2024-01-14 10:04:23.599951",
+ "modified": "2024-02-04 16:19:44.576022",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py
index c82cdf58de..1938fec32b 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.py
@@ -26,6 +26,7 @@ class StockReconciliationItem(Document):
current_valuation_rate: DF.Currency
has_item_scanned: DF.Data | None
item_code: DF.Link
+ item_group: DF.Link | None
item_name: DF.Data | None
parent: DF.Data
parentfield: DF.Data
@@ -34,6 +35,7 @@ class StockReconciliationItem(Document):
quantity_difference: DF.ReadOnly | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.LongText | None
+ use_serial_batch_fields: DF.Check
valuation_rate: DF.Currency
warehouse: DF.Link
# end: auto-generated types
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index dc2797457b..344486c46b 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -50,6 +50,7 @@
"disable_serial_no_and_batch_selector",
"use_naming_series",
"naming_series_prefix",
+ "use_serial_batch_fields",
"stock_planning_tab",
"auto_material_request",
"auto_indent",
@@ -420,6 +421,12 @@
"fieldname": "auto_reserve_stock_for_sales_order_on_purchase",
"fieldtype": "Check",
"label": "Auto Reserve Stock for Sales Order on Purchase"
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial / Batch Fields"
}
],
"icon": "icon-cog",
@@ -427,7 +434,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-01-30 14:03:52.143457",
+ "modified": "2024-02-04 12:01:31.931864",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 088c7cdfe1..c4960aa67a 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -57,6 +57,7 @@ class StockSettings(Document):
stock_uom: DF.Link | None
update_existing_price_list_rate: DF.Check
use_naming_series: DF.Check
+ use_serial_batch_fields: DF.Check
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"]
# end: auto-generated types
@@ -68,6 +69,7 @@ class StockSettings(Document):
"allow_negative_stock",
"default_warehouse",
"set_qty_in_transactions_based_on_serial_no_input",
+ "use_serial_batch_fields",
]:
frappe.db.set_default(key, self.get(key, ""))
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 4cfe5d817e..41baf59557 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -793,6 +793,9 @@ class SerialBatchCreation:
setattr(self, "actual_qty", qty)
self.__dict__["actual_qty"] = self.actual_qty
+ if not hasattr(self, "use_serial_batch_fields"):
+ setattr(self, "use_serial_batch_fields", 0)
+
def duplicate_package(self):
if not self.serial_and_batch_bundle:
return
@@ -901,9 +904,14 @@ class SerialBatchCreation:
self.batches = get_available_batches(kwargs)
def set_auto_serial_batch_entries_for_inward(self):
+ print(self.get("serial_nos"))
+
if (self.get("batches") and self.has_batch_no) or (
self.get("serial_nos") and self.has_serial_no
):
+ if self.use_serial_batch_fields and self.get("serial_nos"):
+ self.make_serial_no_if_not_exists()
+
return
self.batch_no = None
@@ -915,6 +923,59 @@ class SerialBatchCreation:
else:
self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
+ def make_serial_no_if_not_exists(self):
+ non_exists_serial_nos = []
+ for row in self.serial_nos:
+ if not frappe.db.exists("Serial No", row):
+ non_exists_serial_nos.append(row)
+
+ if non_exists_serial_nos:
+ self.make_serial_nos(non_exists_serial_nos)
+
+ def make_serial_nos(self, serial_nos):
+ serial_nos_details = []
+ batch_no = None
+ if self.batches:
+ batch_no = list(self.batches.keys())[0]
+
+ for serial_no in serial_nos:
+ serial_nos_details.append(
+ (
+ serial_no,
+ serial_no,
+ now(),
+ now(),
+ frappe.session.user,
+ frappe.session.user,
+ self.warehouse,
+ self.company,
+ self.item_code,
+ self.item_name,
+ self.description,
+ "Active",
+ batch_no,
+ )
+ )
+
+ if serial_nos_details:
+ fields = [
+ "name",
+ "serial_no",
+ "creation",
+ "modified",
+ "owner",
+ "modified_by",
+ "warehouse",
+ "company",
+ "item_code",
+ "item_name",
+ "description",
+ "status",
+ "batch_no",
+ ]
+
+ frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
+
def set_serial_batch_entries(self, doc):
if self.get("serial_nos"):
serial_no_wise_batch = frappe._dict({})
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 7c2a1f12e2..3467b8283c 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -149,6 +149,7 @@ class SubcontractingReceipt(SubcontractingController):
self.update_prevdoc_status()
self.set_subcontracting_order_status()
self.set_consumed_qty_in_subcontract_order()
+ self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
index 9bfc2fdb7a..9d36359944 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
@@ -48,11 +48,14 @@
"reference_name",
"section_break_45",
"serial_and_batch_bundle",
- "serial_no",
+ "use_serial_batch_fields",
"col_break5",
"rejected_serial_and_batch_bundle",
- "batch_no",
+ "section_break_jshh",
+ "serial_no",
"rejected_serial_no",
+ "column_break_henr",
+ "batch_no",
"manufacture_details",
"manufacturer",
"column_break_16",
@@ -311,22 +314,20 @@
"label": "Serial and Batch Details"
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
- "depends_on": "eval:!doc.is_fixed_asset",
+ "depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
"options": "Batch",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"depends_on": "eval: !parent.is_return",
@@ -478,6 +479,7 @@
"label": "Accounting Details"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -486,6 +488,7 @@
"print_hide": 1
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
@@ -546,12 +549,27 @@
"fieldtype": "Check",
"label": "Include Exploded Items",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_jshh",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_henr",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-30 12:05:51.920705",
+ "modified": "2024-02-04 16:23:30.374865",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
index d02160ece4..1a4ce5b977 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
@@ -58,6 +58,7 @@ class SubcontractingReceiptItem(Document):
subcontracting_order: DF.Link | None
subcontracting_order_item: DF.Data | None
subcontracting_receipt_item: DF.Data | None
+ use_serial_batch_fields: DF.Check
warehouse: DF.Link | None
# end: auto-generated types
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
index 90bcf4e544..1c8e9dd227 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
@@ -26,10 +26,13 @@
"current_stock",
"secbreak_3",
"serial_and_batch_bundle",
- "batch_no",
+ "use_serial_batch_fields",
"col_break4",
+ "subcontracting_order",
+ "section_break_zwnh",
"serial_no",
- "subcontracting_order"
+ "column_break_qibi",
+ "batch_no"
],
"fields": [
{
@@ -60,19 +63,19 @@
"width": "300px"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"no_copy": 1,
- "options": "Batch",
- "read_only": 1
+ "options": "Batch"
},
{
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no",
"fieldtype": "Text",
"label": "Serial No",
- "no_copy": 1,
- "read_only": 1
+ "no_copy": 1
},
{
"fieldname": "col_break1",
@@ -198,6 +201,7 @@
},
{
"columns": 2,
+ "depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"in_list_view": 1,
@@ -205,12 +209,27 @@
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "use_serial_batch_fields",
+ "fieldtype": "Check",
+ "label": "Use Serial No / Batch Fields"
+ },
+ {
+ "depends_on": "eval:doc.use_serial_batch_fields === 1",
+ "fieldname": "section_break_zwnh",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_qibi",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-03-15 13:55:08.132626",
+ "modified": "2024-02-04 16:32:17.534162",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
index 2ee55518d5..8f09197aa8 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
@@ -35,6 +35,7 @@ class SubcontractingReceiptSuppliedItem(Document):
serial_no: DF.Text | None
stock_uom: DF.Link | None
subcontracting_order: DF.Link | None
+ use_serial_batch_fields: DF.Check
# end: auto-generated types
pass
From 407045a1dee4d5d1e4f08f0e78871942ded9e709 Mon Sep 17 00:00:00 2001
From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Date: Sun, 4 Feb 2024 23:01:51 +0530
Subject: [PATCH 23/53] fix: production plan date filters for orders (#39702)
---
.../doctype/production_plan/production_plan.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 5dc5c38376..f0392be01c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1334,10 +1334,10 @@ def get_sales_orders(self):
)
date_field_mapper = {
- "from_date": self.from_date >= so.transaction_date,
- "to_date": self.to_date <= so.transaction_date,
- "from_delivery_date": self.from_delivery_date >= so_item.delivery_date,
- "to_delivery_date": self.to_delivery_date <= so_item.delivery_date,
+ "from_date": so.transaction_date >= self.from_date,
+ "to_date": so.transaction_date <= self.to_date,
+ "from_delivery_date": so_item.delivery_date >= self.from_delivery_date,
+ "to_delivery_date": so_item.delivery_date <= self.to_delivery_date,
}
for field, value in date_field_mapper.items():
From b70f3de16be169a723842a3a99c046a3809d0768 Mon Sep 17 00:00:00 2001
From: rohitwaghchaure
Date: Mon, 5 Feb 2024 11:46:39 +0530
Subject: [PATCH 24/53] perf: memory consumption for the stock balance report
(#39626)
---
.../report/stock_balance/stock_balance.py | 32 ++++++++++++-------
1 file changed, 20 insertions(+), 12 deletions(-)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index ed84a5c2d5..269323810b 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -90,8 +90,7 @@ class StockBalanceReport(object):
self.opening_data.setdefault(group_by_key, entry)
def prepare_new_data(self):
- if not self.sle_entries:
- return
+ self.item_warehouse_map = self.get_item_warehouse_map()
if self.filters.get("show_stock_ageing_data"):
self.filters["show_warehouse_wise_stock"] = True
@@ -99,7 +98,8 @@ class StockBalanceReport(object):
_func = itemgetter(1)
- self.item_warehouse_map = self.get_item_warehouse_map()
+ del self.sle_entries
+
sre_details = self.get_sre_reserved_qty_details()
variant_values = {}
@@ -143,15 +143,22 @@ class StockBalanceReport(object):
item_warehouse_map = {}
self.opening_vouchers = self.get_opening_vouchers()
- for entry in self.sle_entries:
- group_by_key = self.get_group_by_key(entry)
- if group_by_key not in item_warehouse_map:
- self.initialize_data(item_warehouse_map, group_by_key, entry)
+ if self.filters.get("show_stock_ageing_data"):
+ self.sle_entries = self.sle_query.run(as_dict=True)
- self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
+ with frappe.db.unbuffered_cursor():
+ if not self.filters.get("show_stock_ageing_data"):
+ self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True)
- if self.opening_data.get(group_by_key):
- del self.opening_data[group_by_key]
+ for entry in self.sle_entries:
+ group_by_key = self.get_group_by_key(entry)
+ if group_by_key not in item_warehouse_map:
+ self.initialize_data(item_warehouse_map, group_by_key, entry)
+
+ self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key)
+
+ if self.opening_data.get(group_by_key):
+ del self.opening_data[group_by_key]
for group_by_key, entry in self.opening_data.items():
if group_by_key not in item_warehouse_map:
@@ -252,7 +259,8 @@ class StockBalanceReport(object):
.where(
(table.docstatus == 1)
& (table.company == self.filters.company)
- & ((table.to_date <= self.from_date))
+ & (table.to_date <= self.from_date)
+ & (table.status == "Completed")
)
.orderby(table.to_date, order=Order.desc)
.limit(1)
@@ -305,7 +313,7 @@ class StockBalanceReport(object):
if self.filters.get("company"):
query = query.where(sle.company == self.filters.get("company"))
- self.sle_entries = query.run(as_dict=True)
+ self.sle_query = query
def apply_inventory_dimensions_filters(self, query, sle) -> str:
inventory_dimension_fields = self.get_inventory_dimension_fields()
From 7a04f0f7bab1bc36774d41b236e8c4421a6305bd Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Mon, 5 Feb 2024 12:35:26 +0530
Subject: [PATCH 25/53] fix: update company in serial no doc
---
erpnext/stock/serial_batch_bundle.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 4cfe5d817e..78df755d74 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -283,6 +283,7 @@ class SerialBatchBundle:
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
else "Inactive",
)
+ .set(sn_table.company, self.sle.company)
.where(sn_table.name.isin(serial_nos))
).run()
From 25c2b79864ba96b43388a235b34c5b55e2898a8a Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Mon, 5 Feb 2024 13:03:21 +0530
Subject: [PATCH 26/53] fix: precision for tds amount
---
.../tax_withholding_details/test_tax_withholding_details.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
index 6825b4dcf6..af55ba6639 100644
--- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
@@ -36,7 +36,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
- [si.name, "TCS", 0.075, 1000, 0.525, 1000.525],
+ [si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
]
self.check_expected_values(result, expected_values)
From b834ed10d6b6faa1f76e1343ca8b347ca393b7dd Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 5 Feb 2024 14:05:01 +0530
Subject: [PATCH 27/53] perf: Move dimension validation out of GL Entry doctype
(#39730)
---
erpnext/accounts/doctype/gl_entry/gl_entry.py | 46 +------------------
erpnext/accounts/general_ledger.py | 42 +++++++++++++++++
2 files changed, 43 insertions(+), 45 deletions(-)
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 777a5bb91c..def2838b75 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -13,16 +13,9 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
-from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
- get_dimension_filter_map,
-)
from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
-from erpnext.exceptions import (
- InvalidAccountCurrency,
- InvalidAccountDimensionError,
- MandatoryAccountDimensionError,
-)
+from erpnext.exceptions import InvalidAccountCurrency
exclude_from_linked_with = True
@@ -98,7 +91,6 @@ class GLEntry(Document):
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
- self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
@@ -208,42 +200,6 @@ class GLEntry(Document):
)
)
- def validate_allowed_dimensions(self):
- dimension_filter_map = get_dimension_filter_map()
- for key, value in dimension_filter_map.items():
- dimension = key[0]
- account = key[1]
-
- if self.account == account:
- if value["is_mandatory"] and not self.get(dimension):
- frappe.throw(
- _("{0} is mandatory for account {1}").format(
- frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
- ),
- MandatoryAccountDimensionError,
- )
-
- if value["allow_or_restrict"] == "Allow":
- if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
- frappe.throw(
- _("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)),
- frappe.bold(frappe.unscrub(dimension)),
- frappe.bold(self.account),
- ),
- InvalidAccountDimensionError,
- )
- else:
- if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
- frappe.throw(
- _("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)),
- frappe.bold(frappe.unscrub(dimension)),
- frappe.bold(self.account),
- ),
- InvalidAccountDimensionError,
- )
-
def check_pl_account(self):
if (
self.is_opening == "Yes"
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 1c8ac2f164..2e82886755 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -13,9 +13,13 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
+from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
+ get_dimension_filter_map,
+)
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
+from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
def make_gl_entries(
@@ -355,6 +359,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
process_debit_credit_difference(gl_map)
+ dimension_filter_map = get_dimension_filter_map()
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
@@ -362,6 +367,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
for entry in gl_map:
+ validate_allowed_dimensions(entry, dimension_filter_map)
make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -700,3 +706,39 @@ def set_as_cancel(voucher_type, voucher_no):
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no),
)
+
+
+def validate_allowed_dimensions(gl_entry, dimension_filter_map):
+ for key, value in dimension_filter_map.items():
+ dimension = key[0]
+ account = key[1]
+
+ if gl_entry.account == account:
+ if value["is_mandatory"] and not gl_entry.get(dimension):
+ frappe.throw(
+ _("{0} is mandatory for account {1}").format(
+ frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account)
+ ),
+ MandatoryAccountDimensionError,
+ )
+
+ if value["allow_or_restrict"] == "Allow":
+ if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(gl_entry.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(gl_entry.account),
+ ),
+ InvalidAccountDimensionError,
+ )
+ else:
+ if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(gl_entry.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(gl_entry.account),
+ ),
+ InvalidAccountDimensionError,
+ )
From 6e6c818084ee3fbbb2d44a58acb5c78c9f5b61ac Mon Sep 17 00:00:00 2001
From: Deepesh Garg
Date: Mon, 5 Feb 2024 14:10:42 +0530
Subject: [PATCH 28/53] feat: Period-wise closing entries for TB (#39712)
---
erpnext/accounts/report/trial_balance/trial_balance.js | 10 ++++++++--
erpnext/accounts/report/trial_balance/trial_balance.py | 4 ++--
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js
index 2c4c762073..5374ac16d1 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.js
+++ b/erpnext/accounts/report/trial_balance/trial_balance.js
@@ -78,8 +78,14 @@ frappe.query_reports["Trial Balance"] = {
"options": erpnext.get_presentation_currency_list()
},
{
- "fieldname": "with_period_closing_entry",
- "label": __("Period Closing Entry"),
+ "fieldname": "with_period_closing_entry_for_opening",
+ "label": __("With Period Closing Entry For Opening Balances"),
+ "fieldtype": "Check",
+ "default": 1
+ },
+ {
+ "fieldname": "with_period_closing_entry_for_current_period",
+ "label": __("Period Closing Entry For Current Period"),
"fieldtype": "Check",
"default": 1
},
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index 8b7f0bbc00..2ff0eff662 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -116,7 +116,7 @@ def get_data(filters):
max_rgt,
filters,
gl_entries_by_account,
- ignore_closing_entries=not flt(filters.with_period_closing_entry),
+ ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period),
ignore_opening_entries=True,
)
@@ -249,7 +249,7 @@ def get_opening_balance(
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
- if not flt(filters.with_period_closing_entry):
+ if not flt(filters.with_period_closing_entry_for_opening):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:
From 5ce5c352e4ab2293b3f5b5dac9bc1a2a1912e620 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Mon, 5 Feb 2024 11:25:25 +0530
Subject: [PATCH 29/53] fix: disable no-copy for blanket order in PO
---
.../doctype/purchase_order_item/purchase_order_item.json | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
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 5a24cc2e92..e3e8def7ff 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -545,7 +545,6 @@
"fieldname": "blanket_order",
"fieldtype": "Link",
"label": "Blanket Order",
- "no_copy": 1,
"options": "Blanket Order"
},
{
@@ -553,7 +552,6 @@
"fieldname": "blanket_order_rate",
"fieldtype": "Currency",
"label": "Blanket Order Rate",
- "no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -917,7 +915,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-24 13:24:41.298416",
+ "modified": "2024-02-05 11:23:24.859435",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
From 61ded697a7384d8ef133a42424d8a14763bb6061 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Mon, 5 Feb 2024 11:42:17 +0530
Subject: [PATCH 30/53] fix: update BO Ordered Quantity on PO Close/Open
---
erpnext/buying/doctype/purchase_order/purchase_order.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 4efbb270e9..656ee9b91c 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -457,6 +457,7 @@ class PurchaseOrder(BuyingController):
self.update_ordered_qty()
self.update_reserved_qty_for_subcontract()
self.update_subcontracting_order_status()
+ self.update_blanket_order()
self.notify_update()
clear_doctype_notifications(self)
From 27d6c8b6d52ada292eef5c42506e95bcf933eec8 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Mon, 5 Feb 2024 14:08:57 +0530
Subject: [PATCH 31/53] test: BO on PO Close/Open
---
.../purchase_order/test_purchase_order.py | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 5405799b4e..a30de68a00 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -822,6 +822,30 @@ class TestPurchaseOrder(FrappeTestCase):
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
+ def test_blanket_order_on_po_close_and_open(self):
+ # Step - 1: Create Blanket Order
+ bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10)
+
+ # Step - 2: Create Purchase Order
+ po = create_purchase_order(
+ item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name
+ )
+
+ bo.load_from_db()
+ self.assertEqual(bo.items[0].ordered_qty, 5)
+
+ # Step - 3: Close Purchase Order
+ po.update_status("Closed")
+
+ bo.load_from_db()
+ self.assertEqual(bo.items[0].ordered_qty, 0)
+
+ # Step - 4: Re-Open Purchase Order
+ po.update_status("Re-open")
+
+ bo.load_from_db()
+ self.assertEqual(bo.items[0].ordered_qty, 5)
+
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
create_payment_terms_template,
@@ -1148,6 +1172,7 @@ def create_purchase_order(**args):
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),
"against_blanket_order": args.against_blanket_order,
+ "against_blanket": args.against_blanket,
"material_request": args.material_request,
"material_request_item": args.material_request_item,
},
From c1e869f040fd4c6ec2e234db71c3f80be50e4524 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Mon, 5 Feb 2024 12:40:26 +0530
Subject: [PATCH 32/53] test: fixed test cases
---
.../asset_capitalization_stock_item.json | 2 +-
erpnext/controllers/stock_controller.py | 27 +++++-
erpnext/public/js/controllers/transaction.js | 4 +-
.../delivery_note/test_delivery_note.py | 5 +-
.../doctype/packed_item/packed_item.json | 4 +-
erpnext/stock/doctype/pick_list/pick_list.py | 44 +++++-----
.../stock/doctype/pick_list/test_pick_list.py | 41 ++++-----
.../pick_list_item/pick_list_item.json | 4 +-
.../test_serial_and_batch_bundle.py | 6 ++
.../doctype/stock_entry/stock_entry_utils.py | 5 ++
.../doctype/stock_entry/test_stock_entry.py | 83 ++-----------------
.../stock_entry_detail.json | 2 +-
.../test_stock_ledger_entry.py | 4 +
.../stock_reconciliation_item.json | 6 +-
.../subcontracting_receipt_item.json | 4 +-
.../subcontracting_receipt_supplied_item.json | 2 +-
16 files changed, 100 insertions(+), 143 deletions(-)
diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
index d301454be8..8eda441781 100644
--- a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
+++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json
@@ -151,7 +151,7 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 17d3369e9d..9eed6a4f04 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -130,7 +130,15 @@ class StockController(AccountsController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
- for row in self.items:
+ # To handle test cases
+ if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields:
+ return
+
+ table_name = "items"
+ if self.doctype == "Asset Capitalization":
+ table_name = "stock_items"
+
+ for row in self.get(table_name):
if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"):
continue
@@ -140,7 +148,7 @@ class StockController(AccountsController):
frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle"))
if row.use_serial_batch_fields and (
- not row.serial_and_batch_bundle or not row.get("rejected_serial_and_batch_bundle")
+ not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
):
sn_doc = SerialBatchCreation(
{
@@ -164,10 +172,21 @@ class StockController(AccountsController):
if sn_doc.is_rejected:
row.rejected_serial_and_batch_bundle = sn_doc.name
- row.db_set("rejected_serial_and_batch_bundle", sn_doc.name)
+ row.db_set(
+ {
+ "rejected_serial_and_batch_bundle": sn_doc.name,
+ "rejected_serial_no": "",
+ }
+ )
else:
row.serial_and_batch_bundle = sn_doc.name
- row.db_set("serial_and_batch_bundle", sn_doc.name)
+ row.db_set(
+ {
+ "serial_and_batch_bundle": sn_doc.name,
+ "serial_no": "",
+ "batch_no": "",
+ }
+ )
def set_use_serial_batch_fields(self):
if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 18b963dd7b..ba53cf86f4 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -732,10 +732,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.serial_no = item.serial_no.replace(/,/g, '\n');
item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield);
- if (!doc.is_return && cint(frappe.user_defaults.set_qty_in_transactions_based_on_serial_no_input)) {
+ if (!doc.is_return) {
setTimeout(() => {
me.update_qty(cdt, cdn);
- }, 10000);
+ }, 3000);
}
}
}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0f12f38195..b19361ca9f 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -200,7 +200,6 @@ class TestDeliveryNote(FrappeTestCase):
},
)
- frappe.flags.ignore_serial_batch_bundle_validation = True
serial_nos = [
"OSN-1",
"OSN-2",
@@ -239,6 +238,8 @@ class TestDeliveryNote(FrappeTestCase):
)
se_doc.items[0].serial_no = "\n".join(serial_nos)
+
+ frappe.flags.use_serial_and_batch_fields = True
se_doc.submit()
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
@@ -294,6 +295,8 @@ class TestDeliveryNote(FrappeTestCase):
self.assertTrue(serial_no in serial_nos)
self.assertFalse(serial_no in returned_serial_nos1)
+ frappe.flags.use_serial_and_batch_fields = False
+
def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 0b006ab363..1daf6791d4 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -261,7 +261,7 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -270,7 +270,7 @@
"print_hide": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index f2edeea56d..e2edb20510 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -147,12 +147,11 @@ class PickList(Document):
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": row.stock_qty,
- "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward",
+ "type_of_transaction": "Outward",
"company": self.company,
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
"batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
"batch_no": row.batch_no,
- "use_serial_batch_fields": row.use_serial_batch_fields,
}
).make_serial_and_batch_bundle()
@@ -188,6 +187,7 @@ class PickList(Document):
{"is_cancelled": 1, "voucher_no": ""},
)
+ frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel()
row.db_set("serial_and_batch_bundle", None)
def on_update(self):
@@ -349,18 +349,13 @@ class PickList(Document):
self.item_location_map = frappe._dict()
from_warehouses = None
- if self.parent_warehouse and frappe.get_cached_value(
- "Warehouse", self.parent_warehouse, "is_group"
- ):
+ if self.parent_warehouse:
from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
- elif self.parent_warehouse:
- from_warehouses = [self.parent_warehouse]
# Create replica before resetting, to handle empty table on update after submit.
locations_replica = self.get("locations")
# reset
- self.remove_serial_and_batch_bundle()
self.delete_key("locations")
updated_locations = frappe._dict()
for item_doc in items:
@@ -387,13 +382,13 @@ class PickList(Document):
for row in locations:
location = item_doc.as_dict()
location.update(row)
- bundle = location.serial_and_batch_bundle or location.serial_no or location.batch_no
key = (
location.item_code,
location.warehouse,
location.uom,
+ location.batch_no,
+ location.serial_no,
location.sales_order_item or location.material_request_item,
- bundle,
)
if key not in updated_locations:
@@ -675,13 +670,17 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
if not stock_qty:
break
+ serial_nos = None
+ if item_location.serial_nos:
+ serial_nos = "\n".join(item_location.serial_nos[0 : cint(stock_qty)])
+
locations.append(
frappe._dict(
{
"qty": qty,
"stock_qty": stock_qty,
"warehouse": item_location.warehouse,
- "serial_no": item_location.serial_no,
+ "serial_no": serial_nos,
"batch_no": item_location.batch_no,
"use_serial_batch_fields": 1,
}
@@ -711,7 +710,6 @@ def get_available_item_locations(
company,
ignore_validation=False,
picked_item_details=None,
- consider_rejected_warehouses=False,
):
locations = []
total_picked_qty = (
@@ -727,7 +725,6 @@ def get_available_item_locations(
required_qty,
company,
total_picked_qty,
- consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
@@ -778,7 +775,6 @@ def get_available_item_locations_for_serial_and_batched_item(
required_qty,
company,
total_picked_qty=0,
- consider_rejected_warehouses=False,
):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(
@@ -786,7 +782,6 @@ def get_available_item_locations_for_serial_and_batched_item(
from_warehouses,
required_qty,
company,
- consider_rejected_warehouses=consider_rejected_warehouses,
)
if locations:
@@ -804,12 +799,12 @@ def get_available_item_locations_for_serial_and_batched_item(
.where(
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
)
- .orderby(sn.purchase_date)
+ .orderby(sn.creation)
.limit(ceil(location.qty + total_picked_qty))
).run(as_dict=True)
serial_nos = [sn.name for sn in serial_nos]
- location.serial_no = serial_nos
+ location.serial_nos = serial_nos
location.qty = len(serial_nos)
return locations
@@ -848,6 +843,7 @@ def get_available_item_locations_for_serialized_item(
picked_qty -= 1
locations = []
+
for warehouse, serial_nos in warehouse_serial_nos_map.items():
qty = len(serial_nos)
@@ -888,12 +884,14 @@ def get_available_item_locations_for_batched_item(
for warehouse, batches in warehouse_wise_batches.items():
for batch_no, qty in batches.items():
locations.append(
- {
- "qty": qty,
- "warehouse": warehouse,
- "item_code": item_code,
- "batch_no": batch_no,
- }
+ frappe._dict(
+ {
+ "qty": qty,
+ "warehouse": warehouse,
+ "item_code": item_code,
+ "batch_no": batch_no,
+ }
+ )
)
return locations
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 322b0b46ba..cffd0d2820 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -217,6 +217,8 @@ class TestPickList(FrappeTestCase):
)
pick_list.save()
+ pick_list.submit()
+
self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item")
self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
self.assertEqual(pick_list.locations[0].qty, 5)
@@ -239,7 +241,7 @@ class TestPickList(FrappeTestCase):
pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)
pr1.load_from_db()
- oldest_batch_no = pr1.items[0].batch_no
+ oldest_batch_no = get_batch_from_bundle(pr1.items[0].serial_and_batch_bundle)
pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0)
@@ -302,6 +304,8 @@ class TestPickList(FrappeTestCase):
}
)
pick_list.set_item_locations()
+ pick_list.submit()
+ pick_list.reload()
self.assertEqual(
get_batch_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_batch_no
@@ -310,6 +314,7 @@ class TestPickList(FrappeTestCase):
get_serial_nos_from_bundle(pick_list.locations[0].serial_and_batch_bundle), oldest_serial_nos
)
+ pick_list.cancel()
pr1.cancel()
pr2.cancel()
@@ -671,29 +676,22 @@ class TestPickList(FrappeTestCase):
so = make_sales_order(item_code=item, qty=25.0, rate=100)
pl = create_pick_list(so.name)
+ pl.submit()
# pick half the qty
for loc in pl.locations:
self.assertEqual(loc.qty, 25.0)
self.assertTrue(loc.serial_and_batch_bundle)
- data = frappe.get_all(
- "Serial and Batch Entry",
- fields=["qty", "batch_no"],
- filters={"parent": loc.serial_and_batch_bundle},
- )
-
- for d in data:
- self.assertEqual(d.batch_no, "PICKLT-000001")
- self.assertEqual(d.qty, 25.0 * -1)
-
pl.save()
pl.submit()
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
- pl = create_pick_list(so1.name)
+ pl1 = create_pick_list(so1.name)
+ pl1.submit()
+
# pick half the qty
- for loc in pl.locations:
- self.assertEqual(loc.qty, 10.0)
+ for loc in pl1.locations:
+ self.assertEqual(loc.qty, 5.0)
self.assertTrue(loc.serial_and_batch_bundle)
data = frappe.get_all(
@@ -709,8 +707,7 @@ class TestPickList(FrappeTestCase):
elif d.batch_no == "PICKLT-000002":
self.assertEqual(d.qty, 5.0 * -1)
- pl.save()
- pl.submit()
+ pl1.cancel()
pl.cancel()
def test_picklist_for_serial_item(self):
@@ -723,6 +720,7 @@ class TestPickList(FrappeTestCase):
so = make_sales_order(item_code=item, qty=25.0, rate=100)
pl = create_pick_list(so.name)
+ pl.submit()
picked_serial_nos = []
# pick half the qty
for loc in pl.locations:
@@ -736,13 +734,11 @@ class TestPickList(FrappeTestCase):
picked_serial_nos = [d.serial_no for d in data]
self.assertEqual(len(picked_serial_nos), 25)
- pl.save()
- pl.submit()
-
so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
- pl = create_pick_list(so1.name)
+ pl1 = create_pick_list(so1.name)
+ pl1.submit()
# pick half the qty
- for loc in pl.locations:
+ for loc in pl1.locations:
self.assertEqual(loc.qty, 10.0)
self.assertTrue(loc.serial_and_batch_bundle)
@@ -756,8 +752,7 @@ class TestPickList(FrappeTestCase):
for d in data:
self.assertTrue(d.serial_no not in picked_serial_nos)
- pl.save()
- pl.submit()
+ pl1.cancel()
pl.cancel()
def test_picklist_with_bundles(self):
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
index c8001fd508..962fa9f09d 100644
--- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -196,7 +196,7 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -206,7 +206,7 @@
"search_index": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 0d453fb841..f430943708 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -136,6 +136,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
def test_old_batch_valuation(self):
frappe.flags.ignore_serial_batch_bundle_validation = True
+ frappe.flags.use_serial_and_batch_fields = True
batch_item_code = "Old Batch Item Valuation 1"
make_item(
batch_item_code,
@@ -240,6 +241,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
bundle_doc.submit()
frappe.flags.ignore_serial_batch_bundle_validation = False
+ frappe.flags.use_serial_and_batch_fields = False
def test_old_serial_no_valuation(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -259,6 +261,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
)
frappe.flags.ignore_serial_batch_bundle_validation = True
+ frappe.flags.use_serial_and_batch_fields = True
serial_no_id = "Old Serial No 1"
if not frappe.db.exists("Serial No", serial_no_id):
@@ -320,6 +323,9 @@ class TestSerialandBatchBundle(FrappeTestCase):
for row in bundle_doc.entries:
self.assertEqual(flt(row.stock_value_difference, 2), -100.00)
+ frappe.flags.ignore_serial_batch_bundle_validation = False
+ frappe.flags.use_serial_and_batch_fields = False
+
def test_batch_not_belong_to_serial_no(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index 83bfaa0094..0f67e47ad9 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -92,6 +92,9 @@ def make_stock_entry(**args):
else:
args.qty = cint(args.qty)
+ if args.serial_no or args.batch_no:
+ args.use_serial_batch_fields = True
+
# purpose
if not args.purpose:
if args.source and args.target:
@@ -162,6 +165,7 @@ def make_stock_entry(**args):
)
args.serial_no = serial_number
+
s.append(
"items",
{
@@ -177,6 +181,7 @@ def make_stock_entry(**args):
"batch_no": args.batch_no,
"cost_center": args.cost_center,
"expense_account": args.expense_account,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
},
)
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 23dacc8343..a2088c3fef 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -680,6 +680,7 @@ class TestStockEntry(FrappeTestCase):
def test_serial_move(self):
se = make_serialized_item()
serial_no = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)[0]
+ frappe.flags.use_serial_and_batch_fields = True
se = frappe.copy_doc(test_records[0])
se.purpose = "Material Transfer"
@@ -700,6 +701,7 @@ class TestStockEntry(FrappeTestCase):
self.assertTrue(
frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC"
)
+ frappe.flags.use_serial_and_batch_fields = False
def test_serial_cancel(self):
se, serial_nos = self.test_serial_by_series()
@@ -999,6 +1001,8 @@ class TestStockEntry(FrappeTestCase):
do_not_save=True,
)
+ frappe.flags.use_serial_and_batch_fields = True
+
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Inward",
@@ -1035,84 +1039,7 @@ class TestStockEntry(FrappeTestCase):
s2.submit()
s2.cancel()
-
- # def test_retain_sample(self):
- # from erpnext.stock.doctype.batch.batch import get_batch_qty
- # from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-
- # create_warehouse("Test Warehouse for Sample Retention")
- # frappe.db.set_value(
- # "Stock Settings",
- # None,
- # "sample_retention_warehouse",
- # "Test Warehouse for Sample Retention - _TC",
- # )
-
- # test_item_code = "Retain Sample Item"
- # if not frappe.db.exists("Item", test_item_code):
- # item = frappe.new_doc("Item")
- # item.item_code = test_item_code
- # item.item_name = "Retain Sample Item"
- # item.description = "Retain Sample Item"
- # item.item_group = "All Item Groups"
- # item.is_stock_item = 1
- # item.has_batch_no = 1
- # item.create_new_batch = 1
- # item.retain_sample = 1
- # item.sample_quantity = 4
- # item.save()
-
- # receipt_entry = frappe.new_doc("Stock Entry")
- # receipt_entry.company = "_Test Company"
- # receipt_entry.purpose = "Material Receipt"
- # receipt_entry.append(
- # "items",
- # {
- # "item_code": test_item_code,
- # "t_warehouse": "_Test Warehouse - _TC",
- # "qty": 40,
- # "basic_rate": 12,
- # "cost_center": "_Test Cost Center - _TC",
- # "sample_quantity": 4,
- # },
- # )
- # receipt_entry.set_stock_entry_type()
- # receipt_entry.insert()
- # receipt_entry.submit()
-
- # retention_data = move_sample_to_retention_warehouse(
- # receipt_entry.company, receipt_entry.get("items")
- # )
- # retention_entry = frappe.new_doc("Stock Entry")
- # retention_entry.company = retention_data.company
- # retention_entry.purpose = retention_data.purpose
- # retention_entry.append(
- # "items",
- # {
- # "item_code": test_item_code,
- # "t_warehouse": "Test Warehouse for Sample Retention - _TC",
- # "s_warehouse": "_Test Warehouse - _TC",
- # "qty": 4,
- # "basic_rate": 12,
- # "cost_center": "_Test Cost Center - _TC",
- # "batch_no": get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle),
- # },
- # )
- # retention_entry.set_stock_entry_type()
- # retention_entry.insert()
- # retention_entry.submit()
-
- # qty_in_usable_warehouse = get_batch_qty(
- # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle), "_Test Warehouse - _TC", "_Test Item"
- # )
- # qty_in_retention_warehouse = get_batch_qty(
- # get_batch_from_bundle(receipt_entry.get("items")[0].serial_and_batch_bundle),
- # "Test Warehouse for Sample Retention - _TC",
- # "_Test Item",
- # )
-
- # self.assertEqual(qty_in_usable_warehouse, 36)
- # self.assertEqual(qty_in_retention_warehouse, 4)
+ frappe.flags.use_serial_and_batch_fields = False
def test_quality_check(self):
item_code = "_Test Item For QC"
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index bd11d0b861..c7b3daab82 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -582,7 +582,7 @@
"label": "Add Serial / Batch No"
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index d8a3f2e33c..c0999532d0 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -482,6 +482,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
(item, warehouses[0], batches[1], 1, 200),
(item, warehouses[0], batches[0], 1, 200),
]
+
+ frappe.flags.use_serial_and_batch_fields = True
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
sle_details = fetch_sle_details_for_doc_list(dns, ["stock_value_difference"])
svd_list = [-1 * d["stock_value_difference"] for d in sle_details]
@@ -494,6 +496,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
"Incorrect 'Incoming Rate' values fetched for DN items",
)
+ frappe.flags.use_serial_and_batch_fields = False
+
def test_batchwise_item_valuation_stock_reco(self):
item, warehouses, batches = setup_item_valuation_test()
state = {"stock_value": 0.0, "qty": 0.0}
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index 27693d2f1b..734225972c 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -198,7 +198,7 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial / Batch Bundle",
@@ -208,7 +208,7 @@
"search_index": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "current_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Current Serial / Batch Bundle",
@@ -217,7 +217,7 @@
"read_only": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
index 9d36359944..f9e0a0b591 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
@@ -479,7 +479,7 @@
"label": "Accounting Details"
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Serial and Batch Bundle",
@@ -488,7 +488,7 @@
"print_hide": 1
},
{
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
index 1c8e9dd227..957b6a2a65 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
@@ -201,7 +201,7 @@
},
{
"columns": 2,
- "depends_on": "eval:doc.use_serial_batch_fields === 0",
+ "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"in_list_view": 1,
From 1fa62333773a59feb5a0a39d274ebcae362ed286 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Mon, 5 Feb 2024 19:26:31 +0530
Subject: [PATCH 33/53] perf: timeout while submitting the purchase receipt
entry
---
erpnext/buying/doctype/purchase_order/purchase_order.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index b830e7d204..32b73dd482 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -640,6 +640,7 @@ class PurchaseOrder(BuyingController):
update_sco_status(sco, "Closed" if self.status == "Closed" else None)
+@frappe.request_cache
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""
From ee14faaa39bcb0fb8d813aee7e00eedc8d3a38c1 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Mon, 5 Feb 2024 21:53:25 +0530
Subject: [PATCH 34/53] fix: show warehouse title field in sales docs
---
erpnext/controllers/queries.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index e234eec1a6..c46ef50f58 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -729,17 +729,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
conditions, bin_conditions = [], []
filter_dict = get_doctype_wise_filters(filters)
- query = """select `tabWarehouse`.name,
+ warehouse_field = "name"
+ meta = frappe.get_meta("Warehouse")
+ if meta.get("show_title_field_in_link") and meta.get("title_field"):
+ searchfield = meta.get("title_field")
+ warehouse_field = meta.get("title_field")
+
+ query = """select `tabWarehouse`.`{warehouse_field}`,
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
from `tabWarehouse` left join `tabBin`
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
where
`tabWarehouse`.`{key}` like {txt}
{fcond} {mcond}
- order by ifnull(`tabBin`.actual_qty, 0) desc
+ order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc
limit
{page_len} offset {start}
""".format(
+ warehouse_field=warehouse_field,
bin_conditions=get_filters_cond(
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
),
From 56e5611337faf174771122b45a772a52105ce515 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 6 Feb 2024 11:31:11 +0530
Subject: [PATCH 35/53] chore: update CI badges (#39753)
---
README.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 710187ad2f..4f65ceb70b 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@
ERP made simple
-[](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
-[](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
+[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
[](https://www.codetriage.com/frappe/erpnext)
[](https://codecov.io/gh/frappe/erpnext)
[](https://hub.docker.com/r/frappe/erpnext-worker)
From 798a0510e695ed0556c2ba8713f968a21fe49ef1 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 6 Feb 2024 13:34:28 +0530
Subject: [PATCH 36/53] refactor: change server_action args (#39756)
args are now flat, no need to accept them as dict
ref: https://github.com/frappe/frappe/pull/24782
---
erpnext/selling/doctype/customer/customer.py | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 415216c480..3744922a1a 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -572,15 +572,14 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
@frappe.whitelist()
-def send_emails(args):
- args = json.loads(args)
- subject = _("Credit limit reached for customer {0}").format(args.get("customer"))
+def send_emails(customer, customer_outstanding, credit_limit, credit_controller_users_list):
+ if isinstance(credit_controller_users_list, str):
+ credit_controller_users_list = json.loads(credit_controller_users_list)
+ subject = _("Credit limit reached for customer {0}").format(customer)
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
- args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit")
- )
- frappe.sendmail(
- recipients=args.get("credit_controller_users_list"), subject=subject, message=message
+ customer, customer_outstanding, credit_limit
)
+ frappe.sendmail(recipients=credit_controller_users_list, subject=subject, message=message)
def get_customer_outstanding(
From e55611497baee4a71764edde31dcbd8abb582e89 Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Tue, 6 Feb 2024 13:56:41 +0530
Subject: [PATCH 37/53] fix: duplicate for tax category in map
---
.../report/tax_withholding_details/tax_withholding_details.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index 4a80dd0339..60713a10c1 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -295,7 +295,7 @@ def get_tds_docs(filters):
tds_accounts = {}
for tds_acc in _tds_accounts:
# if it turns out not to be the only tax withholding category, then don't include in the map
- if tds_accounts.get(tds_acc["account"]):
+ if tds_acc["account"] in tds_accounts:
tds_accounts[tds_acc["account"]] = None
else:
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
From 3a26823c62bda2984c5238a52b0f9902c483b983 Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Tue, 6 Feb 2024 14:01:06 +0530
Subject: [PATCH 38/53] refactor: fetch rate once tax category is set
---
.../tax_withholding_details/tax_withholding_details.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index 60713a10c1..a5a5cb3afe 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -63,16 +63,14 @@ def get_result(
tax_amount += entry.credit - entry.debit
# infer tax withholding category from the account if it's the single account for this category
tax_withholding_category = tds_accounts.get(entry.account)
- rate = tax_rate_map.get(tax_withholding_category)
# or else the consolidated value from the voucher document
if not tax_withholding_category:
- # or else from the party default
tax_withholding_category = tax_category_map.get(name)
- rate = tax_rate_map.get(tax_withholding_category)
+ # or else from the party default
if not tax_withholding_category:
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
- rate = tax_rate_map.get(tax_withholding_category)
+ rate = tax_rate_map.get(tax_withholding_category)
if net_total_map.get(name):
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
From 01650120d40babc0992a0673498b1eda689fd615 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 6 Feb 2024 13:31:36 +0530
Subject: [PATCH 39/53] test: test case to check use serial / batch fields
feature
---
erpnext/controllers/stock_controller.py | 17 +++-
.../delivery_note/test_delivery_note.py | 5 +-
.../purchase_receipt/test_purchase_receipt.py | 92 ++++++++++++++++++-
.../serial_and_batch_bundle.py | 6 +-
erpnext/stock/doctype/serial_no/serial_no.py | 4 +-
.../stock_reconciliation.js | 1 +
.../stock_reconciliation.py | 74 ++++++++++++++-
.../test_stock_reconciliation.py | 5 +-
.../stock_settings/stock_settings.json | 2 +-
erpnext/stock/utils.py | 51 ++++------
10 files changed, 208 insertions(+), 49 deletions(-)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 9eed6a4f04..74c835c745 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -21,6 +21,9 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_type_of_transaction,
+)
from erpnext.stock.stock_ledger import get_items_to_be_repost
@@ -150,6 +153,13 @@ class StockController(AccountsController):
if row.use_serial_batch_fields and (
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
):
+ if self.doctype == "Stock Reconciliation":
+ qty = row.qty
+ type_of_transaction = "Inward"
+ else:
+ qty = row.stock_qty
+ type_of_transaction = get_type_of_transaction(self, row)
+
sn_doc = SerialBatchCreation(
{
"item_code": row.item_code,
@@ -159,14 +169,15 @@ class StockController(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
- "qty": row.stock_qty,
- "type_of_transaction": "Inward" if row.stock_qty > 0 else "Outward",
+ "qty": qty,
+ "type_of_transaction": type_of_transaction,
"company": self.company,
"is_rejected": 1 if row.get("rejected_warehouse") else 0,
"serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None,
- "batches": frappe._dict({row.batch_no: row.stock_qty}) if row.batch_no else None,
+ "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None,
"batch_no": row.batch_no,
"use_serial_batch_fields": row.use_serial_batch_fields,
+ "do_not_submit": True,
}
).make_serial_and_batch_bundle()
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index b19361ca9f..459e7e7c4f 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1566,7 +1566,7 @@ def create_delivery_note(**args):
dn.return_against = args.return_against
bundle_id = None
- if args.get("batch_no") or args.get("serial_no"):
+ if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
type_of_transaction = args.type_of_transaction or "Outward"
if dn.is_return:
@@ -1608,6 +1608,9 @@ def create_delivery_note(**args):
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"target_warehouse": args.target_warehouse,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else None,
+ "batch_no": args.batch_no if args.use_serial_batch_fields else None,
},
)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 65c08c164c..2d209220de 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2230,6 +2230,93 @@ class TestPurchaseReceipt(FrappeTestCase):
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
+ def test_use_serial_batch_fields_for_serial_nos(self):
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+
+ item_code = make_item(
+ "_Test Use Serial Fields Item Serial Item",
+ properties={"has_serial_no": 1, "serial_no_series": "SNU-TSFISI-.#####"},
+ ).name
+
+ serial_nos = [
+ "SNU-TSFISI-000011",
+ "SNU-TSFISI-000012",
+ "SNU-TSFISI-000013",
+ "SNU-TSFISI-000014",
+ "SNU-TSFISI-000015",
+ ]
+
+ pr = make_purchase_receipt(
+ item_code=item_code,
+ qty=5,
+ serial_no="\n".join(serial_nos),
+ use_serial_batch_fields=1,
+ rate=100,
+ )
+
+ self.assertEqual(pr.items[0].use_serial_batch_fields, 1)
+ self.assertFalse(pr.items[0].serial_no)
+ self.assertTrue(pr.items[0].serial_and_batch_bundle)
+
+ sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle)
+
+ for row in sbb_doc.entries:
+ self.assertTrue(row.serial_no in serial_nos)
+
+ serial_nos.remove("SNU-TSFISI-000015")
+
+ sr = create_stock_reconciliation(
+ item_code=item_code,
+ serial_no="\n".join(serial_nos),
+ qty=4,
+ warehouse=pr.items[0].warehouse,
+ use_serial_batch_fields=1,
+ do_not_submit=True,
+ )
+ sr.reload()
+
+ serial_nos = get_serial_nos(sr.items[0].current_serial_no)
+ self.assertEqual(len(serial_nos), 5)
+ self.assertEqual(sr.items[0].current_qty, 5)
+
+ new_serial_nos = get_serial_nos(sr.items[0].serial_no)
+ self.assertEqual(len(new_serial_nos), 4)
+ self.assertEqual(sr.items[0].qty, 4)
+ self.assertEqual(sr.items[0].use_serial_batch_fields, 1)
+ self.assertFalse(sr.items[0].current_serial_and_batch_bundle)
+ self.assertFalse(sr.items[0].serial_and_batch_bundle)
+ self.assertTrue(sr.items[0].current_serial_no)
+ sr.submit()
+
+ sr.reload()
+ self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
+ self.assertTrue(sr.items[0].serial_and_batch_bundle)
+
+ serial_no_status = frappe.db.get_value("Serial No", "SNU-TSFISI-000015", "status")
+
+ self.assertTrue(serial_no_status != "Active")
+
+ dn = create_delivery_note(
+ item_code=item_code,
+ qty=4,
+ serial_no="\n".join(new_serial_nos),
+ use_serial_batch_fields=1,
+ )
+
+ self.assertTrue(dn.items[0].serial_and_batch_bundle)
+ self.assertEqual(dn.items[0].qty, 4)
+ doc = frappe.get_doc("Serial and Batch Bundle", dn.items[0].serial_and_batch_bundle)
+ for row in doc.entries:
+ self.assertTrue(row.serial_no in new_serial_nos)
+
+ for sn in new_serial_nos:
+ serial_no_status = frappe.db.get_value("Serial No", sn, "status")
+ self.assertTrue(serial_no_status != "Active")
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
@@ -2399,7 +2486,7 @@ def make_purchase_receipt(**args):
uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM"
bundle_id = None
- if args.get("batch_no") or args.get("serial_no"):
+ if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
batches = {}
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -2441,6 +2528,9 @@ def make_purchase_receipt(**args):
"cost_center": args.cost_center
or frappe.get_cached_value("Company", pr.company, "cost_center"),
"asset_location": args.location or "Test Location",
+ "use_serial_batch_fields": args.use_serial_batch_fields or 0,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else "",
+ "batch_no": args.batch_no if args.use_serial_batch_fields else "",
},
)
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index ea33c54544..eb4df29db8 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -1117,7 +1117,7 @@ def parse_serial_nos(data):
if isinstance(data, list):
return data
- return [s.strip() for s in cstr(data).strip().upper().replace(",", "\n").split("\n") if s.strip()]
+ return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()]
@frappe.whitelist()
@@ -1256,7 +1256,7 @@ def create_serial_batch_no_ledgers(
def get_type_of_transaction(parent_doc, child_row):
- type_of_transaction = child_row.type_of_transaction
+ type_of_transaction = child_row.get("type_of_transaction")
if parent_doc.get("doctype") == "Stock Entry":
type_of_transaction = "Outward" if child_row.s_warehouse else "Inward"
@@ -1384,6 +1384,8 @@ def get_available_serial_nos(kwargs):
filters = {"item_code": kwargs.item_code}
+ # ignore_warehouse is used for backdated stock transactions
+ # There might be chances that the serial no not exists in the warehouse during backdated stock transactions
if not kwargs.get("ignore_warehouse"):
filters["warehouse"] = ("is", "set")
if kwargs.warehouse:
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 122664c2dd..5f4f3931a7 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -151,9 +151,7 @@ def get_serial_nos(serial_no):
if isinstance(serial_no, list):
return serial_no
- return [
- s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip()
- ]
+ return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()]
def clean_serial_no_string(serial_no: str) -> str:
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 8e9dcb0fc5..ba7f9c58a8 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -198,6 +198,7 @@ frappe.ui.form.on("Stock Reconciliation", {
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.valuation_rate);
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
+ frappe.model.set_value(cdt, cdn, "use_serial_batch_fields", r.message.use_serial_batch_fields);
if (frm.doc.purpose == "Stock Reconciliation" && !frm.doc.scan_mode) {
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index cc8a7c57b3..ce08615ed5 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -99,6 +99,7 @@ class StockReconciliation(StockController):
)
def on_submit(self):
+ self.make_bundle_for_current_qty()
self.make_bundle_using_old_serial_batch_fields()
self.update_stock_ledger()
self.make_gl_entries()
@@ -117,9 +118,52 @@ class StockReconciliation(StockController):
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
+ def make_bundle_for_current_qty(self):
+ from erpnext.stock.serial_batch_bundle import SerialBatchCreation
+
+ for row in self.items:
+ if not row.use_serial_batch_fields:
+ continue
+
+ if row.current_serial_and_batch_bundle:
+ continue
+
+ if row.current_qty and (row.current_serial_no or row.batch_no):
+ sn_doc = SerialBatchCreation(
+ {
+ "item_code": row.item_code,
+ "warehouse": row.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "voucher_detail_no": row.name,
+ "qty": row.qty,
+ "type_of_transaction": "Outward",
+ "company": self.company,
+ "is_rejected": 0,
+ "serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None,
+ "batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None,
+ "batch_no": row.batch_no,
+ "do_not_submit": True,
+ }
+ ).make_serial_and_batch_bundle()
+
+ row.current_serial_and_batch_bundle = sn_doc.name
+ row.db_set(
+ {
+ "current_serial_and_batch_bundle": sn_doc.name,
+ "current_serial_no": "",
+ "batch_no": "",
+ }
+ )
+
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
+ if not save and item.use_serial_batch_fields:
+ continue
+
if voucher_detail_no and voucher_detail_no != item.name:
continue
@@ -230,6 +274,9 @@ class StockReconciliation(StockController):
def set_new_serial_and_batch_bundle(self):
for item in self.items:
+ if item.use_serial_batch_fields:
+ continue
+
if not item.qty:
continue
@@ -292,8 +339,10 @@ class StockReconciliation(StockController):
inventory_dimensions_dict=inventory_dimensions_dict,
)
- if (item.qty is None or item.qty == item_dict.get("qty")) and (
- item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")
+ if (
+ (item.qty is None or item.qty == item_dict.get("qty"))
+ and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
+ and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
):
return False
else:
@@ -304,6 +353,11 @@ class StockReconciliation(StockController):
if item.valuation_rate is None:
item.valuation_rate = item_dict.get("rate")
+ if item_dict.get("serial_nos"):
+ item.current_serial_no = item_dict.get("serial_nos")
+ if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty:
+ item.serial_no = item.current_serial_no
+
item.current_qty = item_dict.get("qty")
item.current_valuation_rate = item_dict.get("rate")
self.calculate_difference_amount(item, item_dict)
@@ -1136,9 +1190,16 @@ def get_stock_balance_for(
has_serial_no = bool(item_dict.get("has_serial_no"))
has_batch_no = bool(item_dict.get("has_batch_no"))
+ use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
+
if not batch_no and has_batch_no:
# Not enough information to fetch data
- return {"qty": 0, "rate": 0, "serial_nos": None}
+ return {
+ "qty": 0,
+ "rate": 0,
+ "serial_nos": None,
+ "use_serial_batch_fields": use_serial_batch_fields,
+ }
# TODO: fetch only selected batch's values
data = get_stock_balance(
@@ -1161,7 +1222,12 @@ def get_stock_balance_for(
get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0
)
- return {"qty": qty, "rate": rate, "serial_nos": serial_nos}
+ return {
+ "qty": qty,
+ "rate": rate,
+ "serial_nos": serial_nos,
+ "use_serial_batch_fields": use_serial_batch_fields,
+ }
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 0bbfed40d8..479a74af7a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -1094,7 +1094,7 @@ def create_stock_reconciliation(**args):
)
bundle_id = None
- if args.batch_no or args.serial_no:
+ if not args.use_serial_batch_fields and (args.batch_no or args.serial_no):
batches = frappe._dict({})
if args.batch_no:
batches[args.batch_no] = args.qty
@@ -1125,7 +1125,10 @@ def create_stock_reconciliation(**args):
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty,
"valuation_rate": args.rate,
+ "serial_no": args.serial_no if args.use_serial_batch_fields else None,
+ "batch_no": args.batch_no if args.use_serial_batch_fields else None,
"serial_and_batch_bundle": bundle_id,
+ "use_serial_batch_fields": args.use_serial_batch_fields,
},
)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index 344486c46b..c6982831b7 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -423,7 +423,7 @@
"label": "Auto Reserve Stock for Sales Order on Purchase"
},
{
- "default": "0",
+ "default": "1",
"fieldname": "use_serial_batch_fields",
"fieldtype": "Check",
"label": "Use Serial / Batch Fields"
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 76af5d7e3e..9eac172aa7 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -11,6 +11,9 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
import erpnext
+from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ get_available_serial_nos,
+)
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
@@ -125,7 +128,21 @@ def get_stock_balance(
if with_valuation_rate:
if with_serial_no:
- serial_nos = get_serial_nos_data_after_transactions(args)
+ serial_no_details = get_available_serial_nos(
+ frappe._dict(
+ {
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": posting_date,
+ "posting_time": posting_time,
+ "ignore_warehouse": 1,
+ }
+ )
+ )
+
+ serial_nos = ""
+ if serial_no_details:
+ serial_nos = "\n".join(d.serial_no for d in serial_no_details)
return (
(last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
@@ -140,38 +157,6 @@ def get_stock_balance(
return last_entry.qty_after_transaction if last_entry else 0.0
-def get_serial_nos_data_after_transactions(args):
-
- serial_nos = set()
- args = frappe._dict(args)
- sle = frappe.qb.DocType("Stock Ledger Entry")
-
- stock_ledger_entries = (
- frappe.qb.from_(sle)
- .select("serial_no", "actual_qty")
- .where(
- (sle.item_code == args.item_code)
- & (sle.warehouse == args.warehouse)
- & (
- CombineDatetime(sle.posting_date, sle.posting_time)
- < CombineDatetime(args.posting_date, args.posting_time)
- )
- & (sle.is_cancelled == 0)
- )
- .orderby(sle.posting_date, sle.posting_time, sle.creation)
- .run(as_dict=1)
- )
-
- for stock_ledger_entry in stock_ledger_entries:
- changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
- if stock_ledger_entry.actual_qty > 0:
- serial_nos.update(changed_serial_no)
- else:
- serial_nos.difference_update(changed_serial_no)
-
- return "\n".join(serial_nos)
-
-
def get_serial_nos_data(serial_nos):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
From 9383f5da94790f4752477f7a8544408624cc64dc Mon Sep 17 00:00:00 2001
From: Gursheen Anand
Date: Tue, 6 Feb 2024 15:03:12 +0530
Subject: [PATCH 40/53] test: single account for multiple tax categories
---
.../tax_withholding_details.py | 2 +-
.../test_tax_withholding_details.py | 40 +++++++++++++------
2 files changed, 29 insertions(+), 13 deletions(-)
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index 8edf07134e..0e3acd7b24 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -410,7 +410,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"paid_amount_after_tax",
"base_paid_amount",
],
- "Journal Entry": ["tax_withholding_category", "total_amount"],
+ "Journal Entry": ["total_amount"],
}
entries = frappe.get_all(
diff --git a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
index af55ba6639..7515616b0b 100644
--- a/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/test_tax_withholding_details.py
@@ -5,8 +5,8 @@ import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
-from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_tax_withholding_category,
@@ -21,9 +21,10 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
- create_tcs_category()
def test_tax_withholding_for_customers(self):
+ create_tax_category(cumulative_threshold=300)
+ frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
jv = create_tcs_journal_entry()
@@ -40,6 +41,25 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
]
self.check_expected_values(result, expected_values)
+ def test_single_account_for_multiple_categories(self):
+ create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
+ inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
+ inv_1.tax_withholding_category = "TDS - 1"
+ inv_1.submit()
+
+ create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
+ inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
+ inv_2.tax_withholding_category = "TDS - 2"
+ inv_2.submit()
+ result = execute(
+ frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
+ )[1]
+ expected_values = [
+ [inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
+ [inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
+ ]
+ self.check_expected_values(result, expected_values)
+
def check_expected_values(self, result, expected_values):
for i in range(len(result)):
voucher = frappe._dict(result[i])
@@ -73,24 +93,20 @@ def create_tax_accounts():
).insert(ignore_if_duplicate=True)
-def create_tcs_category():
+def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
to_date = fiscal_year[2]
- tax_category = create_tax_withholding_category(
- category_name="TCS",
- rate=0.075,
+ create_tax_withholding_category(
+ category_name=category,
+ rate=rate,
from_date=from_date,
to_date=to_date,
- account="TCS - _TC",
- cumulative_threshold=300,
+ account=account,
+ cumulative_threshold=cumulative_threshold,
)
- customer = frappe.get_doc("Customer", "_Test Customer")
- customer.tax_withholding_category = "TCS"
- customer.save()
-
def create_tcs_payment_entry():
payment_entry = create_payment_entry(
From 9636458ae1baeb0ba43cbf5cfd586a9929ccdacb Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 6 Feb 2024 15:51:46 +0530
Subject: [PATCH 41/53] ci: codecov changes (#39760)
- don't report coverage on individual PRs, instead do it daily
- update tokens for reporting
---
.github/workflows/server-tests-mariadb.yml | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index ccdfc8c109..1e5125eaf7 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -31,6 +31,9 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
+ env:
+ NODE_ENV: "production"
+ WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
@@ -117,11 +120,11 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
- run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --with-coverage --total-builds 4 --build-number ${{ matrix.container }}'
+ run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 4 --build-number ${{ matrix.container }}'
env:
TYPE: server
- CI_BUILD_ID: ${{ github.run_id }}
- ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
+ CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
+
- name: Show bench output
if: ${{ always() }}
@@ -129,6 +132,7 @@ jobs:
- name: Upload coverage data
uses: actions/upload-artifact@v3
+ if: github.event_name != 'pull_request'
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
@@ -137,6 +141,7 @@ jobs:
name: Coverage Wrap Up
needs: test
runs-on: ubuntu-latest
+ if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Clone
uses: actions/checkout@v2
@@ -148,5 +153,6 @@ jobs:
uses: codecov/codecov-action@v2
with:
name: MariaDB
+ token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
From fb330d1b5a8b2620d23c5a899c26ea240966b20c Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 6 Feb 2024 19:13:48 +0530
Subject: [PATCH 42/53] fix: remove file from the disk after the completion of
reposting
---
.../repost_item_valuation/repost_item_valuation.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 4f45210fb9..4878de8331 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -281,6 +281,7 @@ def repost(doc):
repost_gl_entries(doc)
doc.set_status("Completed")
+ remove_attached_file(doc.name)
except Exception as e:
if frappe.flags.in_test:
@@ -309,6 +310,13 @@ def repost(doc):
frappe.db.commit()
+def remove_attached_file(docname):
+ if file_name := frappe.db.get_value(
+ "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
+ ):
+ frappe.delete_doc("File", file_name)
+
+
def repost_sl_entries(doc):
if doc.based_on == "Transaction":
repost_future_sle(
From 0e5b4e5f07d15fe04855f1c836c5412d3644035a Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Tue, 6 Feb 2024 19:21:51 +0530
Subject: [PATCH 43/53] fix: set rate for PO created against BO
---
erpnext/manufacturing/doctype/blanket_order/blanket_order.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
index 6a72c4fdaf..dcf122c4c3 100644
--- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
+++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py
@@ -90,6 +90,7 @@ def make_order(source_name):
def update_item(source, target, source_parent):
target_qty = source.get("qty") - source.get("ordered_qty")
target.qty = target_qty if flt(target_qty) >= 0 else 0
+ target.rate = source.get("rate")
item = get_item_defaults(target.item_code, source_parent.company)
if item:
target.item_name = item.get("item_name")
@@ -111,6 +112,10 @@ def make_order(source_name):
},
},
)
+
+ if target_doc.doctype == "Purchase Order":
+ target_doc.set_missing_values()
+
return target_doc
From 76b57a433886c3a38b9c4883db8652aabe8d32d8 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 6 Feb 2024 19:22:56 +0530
Subject: [PATCH 44/53] test: test case to check removed attached file
---
.../repost_item_valuation.py | 2 +-
.../test_repost_item_valuation.py | 35 +++++++++++++++++++
2 files changed, 36 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 4878de8331..31fc2cab6a 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -314,7 +314,7 @@ def remove_attached_file(docname):
if file_name := frappe.db.get_value(
"File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name"
):
- frappe.delete_doc("File", file_name)
+ frappe.delete_doc("File", file_name, delete_permanently=True)
def repost_sl_entries(doc):
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index 5b76e442f4..f96a612dcb 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -420,3 +420,38 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
self.assertRaises(frappe.ValidationError, riv.save)
doc.cancel()
+
+ def test_remove_attached_file(self):
+ item_code = make_item("_Test Remove Attached File Item", properties={"is_stock_item": 1})
+
+ make_purchase_receipt(
+ item_code=item_code,
+ qty=1,
+ rate=100,
+ )
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code,
+ qty=1,
+ rate=100,
+ posting_date=add_days(today(), days=-1),
+ )
+
+ if docname := frappe.db.exists("Repost Item Valuation", {"voucher_no": pr1.name}):
+ self.assertFalse(
+ frappe.db.get_value(
+ "File",
+ {"attached_to_doctype": "Repost Item Valuation", "attached_to_name": docname},
+ "name",
+ )
+ )
+ else:
+ repost_entries = create_item_wise_repost_entries(pr1.doctype, pr1.name)
+ for entry in repost_entries:
+ self.assertFalse(
+ frappe.db.get_value(
+ "File",
+ {"attached_to_doctype": "Repost Item Valuation", "attached_to_name": entry.name},
+ "name",
+ )
+ )
From 32ccf3524ad973b1bc8c5c7eefa4c79f5d8b4444 Mon Sep 17 00:00:00 2001
From: Rohit Waghchaure
Date: Tue, 6 Feb 2024 22:48:13 +0530
Subject: [PATCH 45/53] fix: do not throw validation for cancelled sle
---
erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 277ca01320..04441f0e8b 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -93,6 +93,9 @@ class StockLedgerEntry(Document):
self.validate_inventory_dimension_negative_stock()
def validate_inventory_dimension_negative_stock(self):
+ if self.is_cancelled:
+ return
+
extra_cond = ""
kwargs = {}
From 50f54d983d5bec92fab13fbd5226fe4cf2e62dd2 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Wed, 7 Feb 2024 16:06:28 +0530
Subject: [PATCH 46/53] fix: add permissions to SRE
---
.../stock_reservation_entry.json | 86 ++++++++++++++++++-
1 file changed, 85 insertions(+), 1 deletion(-)
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
index 76cedd4b1e..bf5ea741e3 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.json
@@ -315,7 +315,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-10-19 16:41:16.545416",
+ "modified": "2024-02-07 16:05:17.772098",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reservation Entry",
@@ -335,6 +335,90 @@
"share": 1,
"submit": 1,
"write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Sales User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
}
],
"sort_field": "modified",
From 0549535603cae6a7afbd3375c8dd62b517af3c6e Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Wed, 7 Feb 2024 16:32:23 +0530
Subject: [PATCH 47/53] refactor: cancel Cr/Dr JE's on Sales/Purchase return
cancel
---
erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index e2b0ee5fe4..308a134ded 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1476,6 +1476,24 @@ class AccountsController(TransactionBase):
x.update({dim.fieldname: self.get(dim.fieldname)})
reconcile_against_document(lst, active_dimensions=active_dimensions)
+ def cancel_system_generated_credit_debit_notes(self):
+ # Cancel 'Credit/Debit' Note Journal Entries, if found.
+ if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
+ voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
+ journals = frappe.db.get_all(
+ "Journal Entry",
+ filters={
+ "is_system_generated": 1,
+ "reference_type": self.doctype,
+ "reference_name": self.name,
+ "voucher_type": voucher_type,
+ "docstatus": 1,
+ },
+ pluck="name",
+ )
+ for x in journals:
+ frappe.get_doc("Journal Entry", x).cancel()
+
def on_cancel(self):
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
remove_from_bank_transaction,
@@ -1488,6 +1506,8 @@ class AccountsController(TransactionBase):
remove_from_bank_transaction(self.doctype, self.name)
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
+ self.cancel_system_generated_credit_debit_notes()
+
# Cancel Exchange Gain/Loss Journal before unlinking
cancel_exchange_gain_loss_journal(self)
From 31a8c3bdc45f0e32a51e43260db8484e5f17aa75 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Wed, 7 Feb 2024 17:21:29 +0530
Subject: [PATCH 48/53] test: Invoice status on Cr/Dr note cancellation
---
.../test_payment_reconciliation.py | 60 +++++++++++++++++++
1 file changed, 60 insertions(+)
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index d7a73f0ce7..89240ac0d8 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -591,6 +591,66 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
+ def test_invoice_status_after_cr_note_cancellation(self):
+ # This test case is made after the 'always standalone Credit/Debit notes' feature is introduced
+ transaction_date = nowdate()
+ amount = 100
+
+ si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
+
+ cr_note = self.create_sales_invoice(
+ qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+ )
+ cr_note.is_return = 1
+ cr_note.return_against = si.name
+ cr_note = cr_note.save().submit()
+
+ pr = self.create_payment_reconciliation()
+
+ pr.get_unreconciled_entries()
+ invoices = [x.as_dict() for x in pr.get("invoices")]
+ payments = [x.as_dict() for x in pr.get("payments")]
+ pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+ pr.reconcile()
+
+ pr.get_unreconciled_entries()
+ self.assertEqual(pr.get("invoices"), [])
+ self.assertEqual(pr.get("payments"), [])
+
+ journals = frappe.db.get_all(
+ "Journal Entry",
+ filters={
+ "is_system_generated": 1,
+ "docstatus": 1,
+ "voucher_type": "Credit Note",
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ },
+ pluck="name",
+ )
+ self.assertEqual(len(journals), 1)
+
+ # assert status outstanding
+ si.reload()
+ self.assertEqual(si.status, "Credit Note Issued")
+ self.assertEqual(si.outstanding_amount, 0)
+
+ cr_note.reload()
+ cr_note.cancel()
+ # 'Credit Note' Journal should be auto cancelled
+ journals = frappe.db.get_all(
+ "Journal Entry",
+ filters={
+ "is_system_generated": 1,
+ "docstatus": 1,
+ "voucher_type": "Credit Note",
+ "reference_type": si.doctype,
+ "reference_name": si.name,
+ },
+ pluck="name",
+ )
+ self.assertEqual(len(journals), 0)
+
def test_cr_note_partial_against_invoice(self):
transaction_date = nowdate()
amount = 100
From 33efe0d12d85484e551dc9ebebf8840d427ecc67 Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Wed, 7 Feb 2024 17:33:55 +0530
Subject: [PATCH 49/53] refactor(test): assert Invoice status as well
---
.../payment_reconciliation/test_payment_reconciliation.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 89240ac0d8..fb75a0f7ca 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -630,7 +630,7 @@ class TestPaymentReconciliation(FrappeTestCase):
)
self.assertEqual(len(journals), 1)
- # assert status outstanding
+ # assert status and outstanding
si.reload()
self.assertEqual(si.status, "Credit Note Issued")
self.assertEqual(si.outstanding_amount, 0)
@@ -650,6 +650,10 @@ class TestPaymentReconciliation(FrappeTestCase):
pluck="name",
)
self.assertEqual(len(journals), 0)
+ # assert status and outstanding
+ si.reload()
+ self.assertEqual(si.status, "Unpaid")
+ self.assertEqual(si.outstanding_amount, 100)
def test_cr_note_partial_against_invoice(self):
transaction_date = nowdate()
From a8ebc94a366e8f15e9bbeab3da064cf5f22dd1b9 Mon Sep 17 00:00:00 2001
From: s-aga-r
Date: Wed, 7 Feb 2024 18:13:34 +0530
Subject: [PATCH 50/53] fix: incorrect planned qty in PP
---
.../doctype/production_plan/production_plan.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index f0392be01c..6e9d1fcfd8 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -312,9 +312,10 @@ class ProductionPlan(Document):
so_item.parent,
so_item.item_code,
so_item.warehouse,
- (
- (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor
- ).as_("pending_qty"),
+ so_item.qty,
+ so_item.work_order_qty,
+ so_item.delivered_qty,
+ so_item.conversion_factor,
so_item.description,
so_item.name,
so_item.bom_no,
@@ -337,6 +338,11 @@ class ProductionPlan(Document):
items = items_query.run(as_dict=True)
+ for item in items:
+ item.pending_qty = (
+ flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor
+ )
+
pi = frappe.qb.DocType("Packed Item")
packed_items_query = (
From 294f562fb918b49fd950a7e810e445d17f5206d9 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 7 Feb 2024 18:30:35 +0530
Subject: [PATCH 51/53] build: Pin frappe requirements to ^16.0.0 (#39782)
---
erpnext/hooks.py | 1 -
erpnext/setup/install.py | 21 ---------------------
pyproject.toml | 3 +++
3 files changed, 3 insertions(+), 22 deletions(-)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 14b7656491..308e6ca011 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -42,7 +42,6 @@ setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wiza
before_install = [
"erpnext.setup.install.check_setup_wizard_not_completed",
- "erpnext.setup.install.check_frappe_version",
]
after_install = "erpnext.setup.install.after_install"
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 6239864c23..527f742841 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -2,14 +2,12 @@
# License: GNU General Public License v3. See license.txt
-import click
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.utils import cint
-import erpnext
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
@@ -42,25 +40,6 @@ You can reinstall this site (after saving your data) using: bench --site [sitena
frappe.throw(message) # nosemgrep
-def check_frappe_version():
- def major_version(v: str) -> str:
- return v.split(".")[0]
-
- frappe_version = major_version(frappe.__version__)
- erpnext_version = major_version(erpnext.__version__)
-
- if frappe_version == erpnext_version:
- return
-
- click.secho(
- f"You're attempting to install ERPNext version {erpnext_version} with Frappe version {frappe_version}. "
- "This is not supported and will result in broken install. Switch to correct branch before installing.",
- fg="red",
- )
-
- raise SystemExit(1)
-
-
def set_single_defaults():
for dt in (
"Accounts Settings",
diff --git a/pyproject.toml b/pyproject.toml
index 604aa44585..8a0f12c5f5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,3 +39,6 @@ force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
indent = "\t"
+
+[tool.bench.frappe-dependencies]
+frappe = ">=16.0.0-dev,<17.0.0"
From 2f676ced5c712823c5737f40230ec8b1994cd2dd Mon Sep 17 00:00:00 2001
From: ruthra kumar
Date: Wed, 7 Feb 2024 19:59:33 +0530
Subject: [PATCH 52/53] refactor(test): Forex Credit Note cancellation against
Invoice
---
.../controllers/tests/test_accounts_controller.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py
index fad216d5a4..d2a3574f28 100644
--- a/erpnext/controllers/tests/test_accounts_controller.py
+++ b/erpnext/controllers/tests/test_accounts_controller.py
@@ -1108,18 +1108,18 @@ class TestAccountsController(FrappeTestCase):
cr_note.reload()
cr_note.cancel()
- # Exchange Gain/Loss Journal should've been created.
+ # with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller
+ # JE(Credit Note) will be cancelled once the parent is cancelled
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
- self.assertNotEqual(exc_je_for_si, [])
- self.assertEqual(len(exc_je_for_si), 1)
+ self.assertEqual(exc_je_for_si, [])
+ self.assertEqual(len(exc_je_for_si), 0)
self.assertEqual(len(exc_je_for_cr), 0)
- # The Credit Note JE is still active and is referencing the sales invoice
- # So, outstanding stays the same
+ # No references, full outstanding
si.reload()
- self.assertEqual(si.outstanding_amount, 1)
- self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
+ self.assertEqual(si.outstanding_amount, 2)
+ self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
def test_40_cost_center_from_payment_entry(self):
"""
From 2c8e4c1ab3bde6a9ab37a41d2801c447d76903ef Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Thu, 8 Feb 2024 13:54:08 +0530
Subject: [PATCH 53/53] fix: do not consider rejected warehouses in pick list
(backport #39539) (#39804)
* fix: do not consider rejected warehouses in pick list (#39539)
* fix: do not picked rejected materials
* test: test case for pick list without rejected materials
(cherry picked from commit f6725e43425043eaba7dcdd3cf3768a857a39ee6)
# Conflicts:
# erpnext/selling/doctype/sales_order/test_sales_order.py
# erpnext/stock/doctype/pick_list/pick_list.json
# erpnext/stock/doctype/pick_list/pick_list.py
* chore: fix conflicts
* chore: fix conflicts
* chore: fix conflicts
* chore: fixed test case
---------
Co-authored-by: rohitwaghchaure
---
.../doctype/sales_order/test_sales_order.py | 78 +++++++++++++++++++
.../stock/doctype/pick_list/pick_list.json | 12 ++-
erpnext/stock/doctype/pick_list/pick_list.py | 74 ++++++++++++++++--
.../stock/doctype/warehouse/warehouse.json | 10 ++-
4 files changed, 165 insertions(+), 9 deletions(-)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 5ae48ee561..b5189b8f06 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import (
WarehouseRequired,
+ create_pick_list,
make_delivery_note,
make_material_request,
make_raw_material_request,
@@ -2023,6 +2024,83 @@ class TestSalesOrder(FrappeTestCase):
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
)
+ def test_pick_list_without_rejected_materials(self):
+ serial_and_batch_item = make_item(
+ "_Test Serial and Batch Item for Rejected Materials",
+ properties={
+ "has_serial_no": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BAT-TSBIFRM-.#####",
+ "serial_no_series": "SN-TSBIFRM-.#####",
+ },
+ ).name
+
+ serial_item = make_item(
+ "_Test Serial Item for Rejected Materials",
+ properties={
+ "has_serial_no": 1,
+ "serial_no_series": "SN-TSIFRM-.#####",
+ },
+ ).name
+
+ batch_item = make_item(
+ "_Test Batch Item for Rejected Materials",
+ properties={
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BAT-TBIFRM-.#####",
+ },
+ ).name
+
+ normal_item = make_item("_Test Normal Item for Rejected Materials").name
+
+ warehouse = "_Test Warehouse - _TC"
+ rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC"
+
+ if not frappe.db.exists("Warehouse", rejected_warehouse):
+ frappe.get_doc(
+ {
+ "doctype": "Warehouse",
+ "warehouse_name": rejected_warehouse,
+ "company": "_Test Company",
+ "warehouse_group": "_Test Warehouse Group",
+ "is_rejected_warehouse": 1,
+ }
+ ).insert()
+
+ se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True)
+ for item in [serial_and_batch_item, serial_item, batch_item]:
+ se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse})
+
+ se.save()
+ se.submit()
+
+ se = make_stock_entry(
+ item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True
+ )
+ for item in [serial_and_batch_item, serial_item, batch_item]:
+ se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse})
+
+ se.save()
+ se.submit()
+
+ so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True)
+
+ for item in [serial_and_batch_item, serial_item, batch_item]:
+ so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse})
+
+ so.save()
+ so.submit()
+
+ pick_list = create_pick_list(so.name)
+
+ pick_list.save()
+ for row in pick_list.locations:
+ self.assertEqual(row.qty, 1.0)
+ self.assertFalse(row.warehouse == rejected_warehouse)
+ self.assertTrue(row.warehouse == warehouse)
+
def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index bd84aadef7..0c474342a9 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -16,6 +16,7 @@
"for_qty",
"column_break_4",
"parent_warehouse",
+ "consider_rejected_warehouses",
"get_item_locations",
"section_break_6",
"scan_barcode",
@@ -184,11 +185,18 @@
"report_hide": 1,
"reqd": 1,
"search_index": 1
+ },
+ {
+ "default": "0",
+ "description": "Enable it if users want to consider rejected materials to dispatch.",
+ "fieldname": "consider_rejected_warehouses",
+ "fieldtype": "Check",
+ "label": "Consider Rejected Warehouses"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2024-02-01 16:17:44.877426",
+ "modified": "2024-02-02 16:17:44.877426",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
@@ -260,4 +268,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index e2edb20510..98ed569af1 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -369,6 +369,7 @@ class PickList(Document):
self.item_count_map.get(item_code),
self.company,
picked_item_details=picked_items_details.get(item_code),
+ consider_rejected_warehouses=self.consider_rejected_warehouses,
),
)
@@ -710,6 +711,7 @@ def get_available_item_locations(
company,
ignore_validation=False,
picked_item_details=None,
+ consider_rejected_warehouses=False,
):
locations = []
total_picked_qty = (
@@ -725,18 +727,34 @@ def get_available_item_locations(
required_qty,
company,
total_picked_qty,
+ consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(
- item_code, from_warehouses, required_qty, company, total_picked_qty
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty,
+ consider_rejected_warehouses=consider_rejected_warehouses,
)
elif has_batch_no:
locations = get_available_item_locations_for_batched_item(
- item_code, from_warehouses, required_qty, company, total_picked_qty
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty,
+ consider_rejected_warehouses=consider_rejected_warehouses,
)
else:
locations = get_available_item_locations_for_other_item(
- item_code, from_warehouses, required_qty, company, total_picked_qty
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty,
+ consider_rejected_warehouses=consider_rejected_warehouses,
)
total_qty_available = sum(location.get("qty") for location in locations)
@@ -775,6 +793,7 @@ def get_available_item_locations_for_serial_and_batched_item(
required_qty,
company,
total_picked_qty=0,
+ consider_rejected_warehouses=False,
):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(
@@ -782,6 +801,7 @@ def get_available_item_locations_for_serial_and_batched_item(
from_warehouses,
required_qty,
company,
+ consider_rejected_warehouses=consider_rejected_warehouses,
)
if locations:
@@ -811,7 +831,12 @@ def get_available_item_locations_for_serial_and_batched_item(
def get_available_item_locations_for_serialized_item(
- item_code, from_warehouses, required_qty, company, total_picked_qty=0
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty=0,
+ consider_rejected_warehouses=False,
):
picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
@@ -828,6 +853,10 @@ def get_available_item_locations_for_serialized_item(
else:
query = query.where(Coalesce(sn.warehouse, "") != "")
+ if not consider_rejected_warehouses:
+ if rejected_warehouses := get_rejected_warehouses():
+ query = query.where(sn.warehouse.notin(rejected_warehouses))
+
serial_nos = query.run(as_list=True)
warehouse_serial_nos_map = frappe._dict()
@@ -860,7 +889,12 @@ def get_available_item_locations_for_serialized_item(
def get_available_item_locations_for_batched_item(
- item_code, from_warehouses, required_qty, company, total_picked_qty=0
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty=0,
+ consider_rejected_warehouses=False,
):
locations = []
data = get_auto_batch_nos(
@@ -875,7 +909,14 @@ def get_available_item_locations_for_batched_item(
)
warehouse_wise_batches = frappe._dict()
+ rejected_warehouses = get_rejected_warehouses()
+
for d in data:
+ if (
+ not consider_rejected_warehouses and rejected_warehouses and d.warehouse in rejected_warehouses
+ ):
+ continue
+
if d.warehouse not in warehouse_wise_batches:
warehouse_wise_batches.setdefault(d.warehouse, defaultdict(float))
@@ -898,7 +939,12 @@ def get_available_item_locations_for_batched_item(
def get_available_item_locations_for_other_item(
- item_code, from_warehouses, required_qty, company, total_picked_qty=0
+ item_code,
+ from_warehouses,
+ required_qty,
+ company,
+ total_picked_qty=0,
+ consider_rejected_warehouses=False,
):
bin = frappe.qb.DocType("Bin")
query = (
@@ -915,6 +961,10 @@ def get_available_item_locations_for_other_item(
wh = frappe.qb.DocType("Warehouse")
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
+ if not consider_rejected_warehouses:
+ if rejected_warehouses := get_rejected_warehouses():
+ query = query.where(bin.warehouse.notin(rejected_warehouses))
+
item_locations = query.run(as_dict=True)
return item_locations
@@ -1236,3 +1286,15 @@ def update_common_item_properties(item, location):
item.serial_no = location.serial_no
item.batch_no = location.batch_no
item.material_request_item = location.material_request_item
+
+
+def get_rejected_warehouses():
+ if not hasattr(frappe.local, "rejected_warehouses"):
+ frappe.local.rejected_warehouses = []
+
+ if not frappe.local.rejected_warehouses:
+ frappe.local.rejected_warehouses = frappe.get_all(
+ "Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name"
+ )
+
+ return frappe.local.rejected_warehouses
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 43b2ad2a69..7b0cade3ca 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -13,6 +13,7 @@
"column_break_3",
"is_group",
"parent_warehouse",
+ "is_rejected_warehouse",
"column_break_4",
"account",
"company",
@@ -249,13 +250,20 @@
{
"fieldname": "column_break_qajx",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "If yes, then this warehouse will be used to store rejected materials",
+ "fieldname": "is_rejected_warehouse",
+ "fieldtype": "Check",
+ "label": "Is Rejected Warehouse"
}
],
"icon": "fa fa-building",
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2023-05-29 13:10:43.333160",
+ "modified": "2024-01-24 16:27:28.299520",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",