Merge pull request #39562 from GursheenK/JV-timeout-issue
fix: enqueue JV submission when > 100 accounts
diff --git a/.github/workflows/patch_faux.yml b/.github/workflows/patch_faux.yml
new file mode 100644
index 0000000..93d88bd
--- /dev/null
+++ b/.github/workflows/patch_faux.yml
@@ -0,0 +1,22 @@
+# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
+
+name: Skipped Patch Test
+
+on:
+ pull_request:
+ paths:
+ - "**.js"
+ - "**.css"
+ - "**.md"
+ - "**.html"
+ - "**.csv"
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ name: Patch Test
+
+ steps:
+ - name: Pass skipped tests unconditionally
+ run: "echo Skipped"
diff --git a/.github/workflows/server-tests-mariadb-faux.yml b/.github/workflows/server-tests-mariadb-faux.yml
new file mode 100644
index 0000000..8334661
--- /dev/null
+++ b/.github/workflows/server-tests-mariadb-faux.yml
@@ -0,0 +1,24 @@
+# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
+
+name: Skipped Tests
+
+on:
+ pull_request:
+ paths:
+ - "**.js"
+ - "**.css"
+ - "**.md"
+ - "**.html"
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ container: [1, 2, 3, 4]
+
+ name: Python Unit Tests
+
+ steps:
+ - name: Pass skipped tests unconditionally
+ run: "echo Skipped"
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 18891e2..7bf5324 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -201,8 +201,8 @@
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in advance_payment_doctypes:
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 3a1e1ea..b1b8d5e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -87,12 +87,14 @@
"status",
"custom_remarks",
"remarks",
+ "base_in_words",
"column_break_16",
"letter_head",
"print_heading",
"bank",
"bank_account_no",
"payment_order",
+ "in_words",
"subscription_section",
"auto_repeat",
"amended_from",
@@ -747,6 +749,20 @@
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account",
"read_only": 1
+ },
+ {
+ "fieldname": "base_in_words",
+ "fieldtype": "Small Text",
+ "label": "In Words (Company Currency)",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "in_words",
+ "fieldtype": "Small Text",
+ "label": "In Words",
+ "print_hide": 1,
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index cfe0d9c..7e88b6b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -178,6 +178,7 @@
self.validate_paid_invoices()
self.ensure_supplier_is_not_blocked()
self.set_status()
+ self.set_total_in_words()
def on_submit(self):
if self.difference_amount:
@@ -786,6 +787,21 @@
self.db_set("status", self.status, update_modified=True)
+ def set_total_in_words(self):
+ from frappe.utils import money_in_words
+
+ if self.payment_type in ("Pay", "Internal Transfer"):
+ base_amount = abs(self.base_paid_amount)
+ amount = abs(self.paid_amount)
+ currency = self.paid_from_account_currency
+ elif self.payment_type == "Receive":
+ base_amount = abs(self.base_received_amount)
+ amount = abs(self.received_amount)
+ currency = self.paid_to_account_currency
+
+ self.base_in_words = money_in_words(base_amount, self.company_currency)
+ self.in_words = money_in_words(amount, currency)
+
def set_tax_withholding(self):
if self.party_type != "Supplier":
return
@@ -927,8 +943,8 @@
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
if d.reference_doctype in advance_payment_doctypes:
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
@@ -1428,8 +1444,8 @@
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc(
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index 666926f..ff2aa6d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -41,6 +41,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"label": "Company",
"options": "Company",
"reqd": 1
@@ -229,7 +230,7 @@
"is_virtual": 1,
"issingle": 1,
"links": [],
- "modified": "2023-12-14 13:38:16.264013",
+ "modified": "2024-01-18 11:56:20.234667",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 839348a..a18104e 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -170,8 +170,8 @@
self.request_phone_payment()
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
@@ -216,8 +216,8 @@
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_total_advance_paid()
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index ca031f0..1a7eef6 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -11,7 +11,6 @@
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
SalesInvoice,
- get_bank_cash_account,
get_mode_of_payment_info,
update_multi_mode_option,
)
@@ -208,7 +207,6 @@
self.validate_stock_availablility()
self.validate_return_items_qty()
self.set_status()
- self.set_account_for_mode_of_payment()
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
@@ -643,11 +641,6 @@
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
- def set_account_for_mode_of_payment(self):
- for pay in self.payments:
- if not pay.account:
- pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
-
@frappe.whitelist()
def create_payment_request(self):
for pay in self.payments:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index f85fc87..a48f5ea 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1253,6 +1253,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
+ "no_copy": 1,
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
@@ -1612,7 +1613,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2023-11-29 15:35:44.697496",
+ "modified": "2024-01-26 10:46:00.469053",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index cc19650..c381b8a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -421,7 +421,8 @@
self.calculate_taxes_and_totals()
def before_save(self):
- set_account_for_mode_of_payment(self)
+ self.set_account_for_mode_of_payment()
+ self.set_paid_amount()
def on_submit(self):
self.validate_pos_paid_amount()
@@ -712,9 +713,6 @@
):
data.sales_invoice = sales_invoice
- def on_update(self):
- self.set_paid_amount()
-
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
fields_to_check = [
@@ -745,6 +743,11 @@
self.paid_amount = paid_amount
self.base_paid_amount = base_paid_amount
+ def set_account_for_mode_of_payment(self):
+ for payment in self.payments:
+ if not payment.account:
+ payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
+
def validate_time_sheets_are_submitted(self):
for data in self.timesheets:
if data.time_sheet:
@@ -2113,12 +2116,6 @@
return make_return_doc("Sales Invoice", source_name, target_doc)
-def set_account_for_mode_of_payment(self):
- for data in self.payments:
- if not data.account:
- data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
-
-
def get_inter_company_details(doc, doctype):
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
parties = frappe.db.get_all(
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 3a70afc..e3fa5e8 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -5,7 +5,7 @@
from collections import OrderedDict
import frappe
-from frappe import _, qb, scrub
+from frappe import _, qb, query_builder, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
@@ -576,6 +576,8 @@
def get_future_payments_from_payment_entry(self):
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
+ ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
+
return (
frappe.qb.from_(pe)
.inner_join(pe_ref)
@@ -587,6 +589,11 @@
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
+ ifelse(
+ pe.payment_type == "Receive",
+ pe.source_exchange_rate * pe_ref.allocated_amount,
+ pe.target_exchange_rate * pe_ref.allocated_amount,
+ ).as_("future_amount_in_base_currency"),
)
.where(
(pe.docstatus < 2)
@@ -623,13 +630,24 @@
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
+ query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
+ query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
else:
query = query.select(
- Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
+ Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
+ "future_amount_in_base_currency"
+ )
+ )
+ query = query.select(
+ Sum(
+ jea.debit_in_account_currency
+ if self.account_type == "Payable"
+ else jea.credit_in_account_currency
+ ).as_("future_amount")
)
query = query.having(qb.Field("future_amount") > 0)
@@ -645,14 +663,19 @@
row.remaining_balance = row.outstanding
row.future_amount = 0.0
for future in self.future_payments.get((row.voucher_no, row.party), []):
- if row.remaining_balance > 0 and future.future_amount:
- if future.future_amount > row.outstanding:
+ if self.filters.in_party_currency:
+ future_amount_field = "future_amount"
+ else:
+ future_amount_field = "future_amount_in_base_currency"
+
+ if row.remaining_balance > 0 and future.get(future_amount_field):
+ if future.get(future_amount_field) > row.outstanding:
row.future_amount = row.outstanding
- future.future_amount = future.future_amount - row.outstanding
+ future[future_amount_field] = future.get(future_amount_field) - row.outstanding
row.remaining_balance = 0
else:
- row.future_amount += future.future_amount
- future.future_amount = 0
+ row.future_amount += future.get(future_amount_field)
+ future[future_amount_field] = 0
row.remaining_balance = row.outstanding - row.future_amount
row.setdefault("future_ref", []).append(
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 976935b..6ff81be 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -772,3 +772,92 @@
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
report_output = sorted(report_output, key=lambda x: x[0])
self.assertEqual(expected_data, report_output)
+
+ def test_future_payments_on_foreign_currency(self):
+ self.customer2 = (
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": "Jane Doe",
+ "type": "Individual",
+ "default_currency": "USD",
+ }
+ )
+ .insert()
+ .submit()
+ )
+
+ si = self.create_sales_invoice(do_not_submit=True)
+ si.posting_date = add_days(today(), -1)
+ si.customer = self.customer2
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.debit_to = self.debtors_usd
+ si.save().submit()
+
+ # full payment in USD
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.posting_date = add_days(today(), 1)
+ pe.base_received_amount = 7500
+ pe.received_amount = 7500
+ pe.source_exchange_rate = 75
+ pe.save().submit()
+
+ filters = frappe._dict(
+ {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "show_future_payments": True,
+ "in_party_currency": False,
+ }
+ )
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+
+ expected_data = [8000.0, 8000.0, 500.0, 7500.0]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
+ )
+
+ filters.in_party_currency = True
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [100.0, 100.0, 0.0, 100.0]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
+ )
+
+ pe.cancel()
+ # partial payment in USD on a future date
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.posting_date = add_days(today(), 1)
+ pe.base_received_amount = 6750
+ pe.received_amount = 6750
+ pe.source_exchange_rate = 75
+ pe.paid_amount = 90 # in USD
+ pe.references[0].allocated_amount = 90
+ pe.save().submit()
+
+ filters.in_party_currency = False
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [8000.0, 8000.0, 1250.0, 6750.0]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
+ )
+
+ filters.in_party_currency = True
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [100.0, 100.0, 10.0, 90.0]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
+ )
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 65b3aba..30700d0 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -240,7 +240,6 @@
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
if account:
-
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
acc.check_permission("read")
@@ -286,18 +285,22 @@
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
if account or (party_type and party) or account_type:
-
+ precision = get_currency_precision()
if in_account_currency:
- select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
+ select_field = (
+ "sum(round(debit_in_account_currency, %s)) - sum(round(credit_in_account_currency, %s))"
+ )
else:
- select_field = "sum(debit) - sum(credit)"
+ select_field = "sum(round(debit, %s)) - sum(round(credit, %s))"
+
bal = frappe.db.sql(
"""
SELECT {0}
FROM `tabGL Entry` gle
WHERE {1}""".format(
select_field, " and ".join(cond)
- )
+ ),
+ (precision, precision),
)[0][0]
# if bal is None, return 0
return flt(bal)
@@ -619,8 +622,8 @@
# Update Advance Paid in SO/PO since they might be getting unlinked
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
if jv_detail.get("reference_type") in advance_payment_doctypes:
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
@@ -696,8 +699,8 @@
# Update Advance Paid in SO/PO since they are getting unlinked
advance_payment_doctypes = frappe.get_hooks(
- "advance_payment_customer_doctypes"
- ) + frappe.get_hooks("advance_payment_supplier_doctypes")
+ "advance_payment_receivable_doctypes"
+ ) + frappe.get_hooks("advance_payment_payable_doctypes")
if existing_row.get("reference_doctype") in advance_payment_doctypes:
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 02e7a9b..673fe54 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -571,16 +571,16 @@
indicator: 'red'
});
}
- var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
- var asset_quantity = is_grouped_asset ? item.qty : 1;
- var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
+ frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => {
+ var asset_quantity = r.is_grouped_asset ? item.qty : 1;
+ var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
- frm.set_value('gross_purchase_amount', purchase_amount);
- frm.set_value('purchase_receipt_amount', purchase_amount);
- frm.set_value('asset_quantity', asset_quantity);
- frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
- if(item.asset_location) { frm.set_value('location', item.asset_location); }
-
+ frm.set_value('gross_purchase_amount', purchase_amount);
+ frm.set_value('purchase_receipt_amount', purchase_amount);
+ frm.set_value('asset_quantity', asset_quantity);
+ frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
+ if(item.asset_location) { frm.set_value('location', item.asset_location); }
+ });
},
set_depreciation_rate: function(frm, row) {
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index cc23d9d..166e8c4 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -519,14 +519,11 @@
movement.cancel()
def cancel_capitalization(self):
- asset_capitalization = frappe.db.get_value(
- "Asset Capitalization",
- {"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"},
- )
-
- if asset_capitalization:
- asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
- asset_capitalization.cancel()
+ if self.capitalized_in:
+ self.db_set("capitalized_in", None)
+ asset_capitalization = frappe.get_doc("Asset Capitalization", self.capitalized_in)
+ if asset_capitalization.docstatus == 1:
+ asset_capitalization.cancel()
def delete_depreciation_entries(self):
if self.calculate_depreciation:
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index a93af94..df4593b 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -561,6 +561,8 @@
def reverse_depreciation_entry_made_after_disposal(asset, date):
for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
+ if not asset_depr_schedule_doc:
+ continue
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date:
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index cad74df..5e251a5 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -146,6 +146,7 @@
def cancel_target_asset(self):
if self.entry_type == "Capitalization" and self.target_asset:
asset_doc = frappe.get_doc("Asset", self.target_asset)
+ frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
if asset_doc.docstatus == 1:
asset_doc.cancel()
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index afbea61..1ed719d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -22,6 +22,7 @@
get_link_to_form,
getdate,
nowdate,
+ parse_json,
today,
)
@@ -833,6 +834,37 @@
self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges")))
+ def append_taxes_from_item_tax_template(self):
+ if not frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
+ return
+
+ for row in self.items:
+ item_tax_rate = row.get("item_tax_rate")
+ if not item_tax_rate:
+ continue
+
+ if isinstance(item_tax_rate, str):
+ item_tax_rate = parse_json(item_tax_rate)
+
+ for account_head, rate in item_tax_rate.items():
+ row = self.get_tax_row(account_head)
+
+ if not row:
+ self.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": account_head,
+ "rate": 0,
+ "description": account_head,
+ },
+ )
+
+ def get_tax_row(self, account_head):
+ for row in self.taxes:
+ if row.account_head == account_head:
+ return row
+
def set_other_charges(self):
self.set("taxes", [])
self.set_taxes()
@@ -1761,9 +1793,9 @@
def set_total_advance_paid(self):
ple = frappe.qb.DocType("Payment Ledger Entry")
- if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
+ if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
party = self.customer
- if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
+ if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
party = self.supplier
advance = (
frappe.qb.from_(ple)
@@ -1829,9 +1861,9 @@
"docstatus": 1,
},
)
- if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
+ if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
new_status = "Requested" if prs else "Not Requested"
- if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
+ if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
new_status = "Initiated" if prs else "Not Initiated"
if new_status == self.advance_payment_status:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 7c63518..11e9f9f 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -6,7 +6,7 @@
from typing import List, Tuple
import frappe
-from frappe import _
+from frappe import _, bold
from frappe.utils import cint, flt, get_link_to_form, getdate
import erpnext
@@ -697,6 +697,9 @@
self.validate_in_transit_warehouses()
self.validate_multi_currency()
self.validate_packed_items()
+
+ if self.get("is_internal_supplier"):
+ self.validate_internal_transfer_qty()
else:
self.validate_internal_transfer_warehouse()
@@ -735,6 +738,116 @@
if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
frappe.throw(_("Packed Items cannot be transferred internally"))
+ def validate_internal_transfer_qty(self):
+ if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
+ return
+
+ item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
+ if not item_wise_transfer_qty:
+ return
+
+ item_wise_received_qty = self.get_item_wise_inter_received_qty()
+ precision = frappe.get_precision(self.doctype + " Item", "qty")
+
+ over_receipt_allowance = frappe.db.get_single_value(
+ "Stock Settings", "over_delivery_receipt_allowance"
+ )
+
+ parent_doctype = {
+ "Purchase Receipt": "Delivery Note",
+ "Purchase Invoice": "Sales Invoice",
+ }.get(self.doctype)
+
+ for key, transferred_qty in item_wise_transfer_qty.items():
+ recevied_qty = flt(item_wise_received_qty.get(key), precision)
+ if over_receipt_allowance:
+ transferred_qty = transferred_qty + flt(
+ transferred_qty * over_receipt_allowance / 100, precision
+ )
+
+ if recevied_qty > flt(transferred_qty, precision):
+ frappe.throw(
+ _("For Item {0} cannot be received more than {1} qty against the {2} {3}").format(
+ bold(key[1]),
+ bold(flt(transferred_qty, precision)),
+ bold(parent_doctype),
+ get_link_to_form(parent_doctype, self.get("inter_company_reference")),
+ )
+ )
+
+ def get_item_wise_inter_transfer_qty(self):
+ reference_field = "inter_company_reference"
+ if self.doctype == "Purchase Invoice":
+ reference_field = "inter_company_invoice_reference"
+
+ parent_doctype = {
+ "Purchase Receipt": "Delivery Note",
+ "Purchase Invoice": "Sales Invoice",
+ }.get(self.doctype)
+
+ child_doctype = parent_doctype + " Item"
+
+ parent_tab = frappe.qb.DocType(parent_doctype)
+ child_tab = frappe.qb.DocType(child_doctype)
+
+ query = (
+ frappe.qb.from_(parent_doctype)
+ .inner_join(child_tab)
+ .on(child_tab.parent == parent_tab.name)
+ .select(
+ child_tab.name,
+ child_tab.item_code,
+ child_tab.qty,
+ )
+ .where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
+ )
+
+ data = query.run(as_dict=True)
+ item_wise_transfer_qty = defaultdict(float)
+ for row in data:
+ item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
+
+ return item_wise_transfer_qty
+
+ def get_item_wise_inter_received_qty(self):
+ child_doctype = self.doctype + " Item"
+
+ parent_tab = frappe.qb.DocType(self.doctype)
+ child_tab = frappe.qb.DocType(child_doctype)
+
+ query = (
+ frappe.qb.from_(self.doctype)
+ .inner_join(child_tab)
+ .on(child_tab.parent == parent_tab.name)
+ .select(
+ child_tab.item_code,
+ child_tab.qty,
+ )
+ .where(parent_tab.docstatus < 2)
+ )
+
+ if self.doctype == "Purchase Invoice":
+ query = query.select(
+ child_tab.sales_invoice_item.as_("name"),
+ )
+
+ query = query.where(
+ parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference
+ )
+ else:
+ query = query.select(
+ child_tab.delivery_note_item.as_("name"),
+ )
+
+ query = query.where(parent_tab.inter_company_reference == self.inter_company_reference)
+
+ data = query.run(as_dict=True)
+ item_wise_transfer_qty = defaultdict(float)
+ for row in data:
+ item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
+
+ return item_wise_transfer_qty
+
def validate_putaway_capacity(self):
# if over receipt is attempted while 'apply putaway rule' is disabled
# and if rule was applied on the transaction, validate it.
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 9555902..65d0872 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -881,7 +881,9 @@
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
"voucher_detail_no": item.name,
- "serial_and_batch_bundle": item.serial_and_batch_bundle,
+ "serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
+ "serial_no": item.get("serial_no"),
+ "batch_no": item.get("batch_no"),
}
)
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index f33fff0..14b7656 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -481,8 +481,8 @@
communication_doctypes = ["Customer", "Supplier"]
-advance_payment_customer_doctypes = ["Sales Order"]
-advance_payment_supplier_doctypes = ["Purchase Order"]
+advance_payment_receivable_doctypes = ["Sales Order"]
+advance_payment_payable_doctypes = ["Purchase Order"]
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 6406735..f831a88 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -790,24 +790,25 @@
if (me.frm.doc.price_list_currency == company_currency) {
me.frm.set_value('plc_conversion_rate', 1.0);
}
- if (company_doc && company_doc.default_letter_head) {
- if(me.frm.fields_dict.letter_head) {
- me.frm.set_value("letter_head", company_doc.default_letter_head);
+ if (company_doc){
+ if (company_doc.default_letter_head) {
+ if(me.frm.fields_dict.letter_head) {
+ me.frm.set_value("letter_head", company_doc.default_letter_head);
+ }
+ }
+ let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
+ if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
+ selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
+ me.frm.set_value("tc_name", company_doc.default_selling_terms);
+ }
+ let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
+ "Material Request", "Purchase Receipt"];
+ // Purchase Invoice is excluded as per issue #3345
+ if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
+ buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
+ me.frm.set_value("tc_name", company_doc.default_buying_terms);
}
}
- let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
- if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
- selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
- me.frm.set_value("tc_name", company_doc.default_selling_terms);
- }
- let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
- "Material Request", "Purchase Receipt"];
- // Purchase Invoice is excluded as per issue #3345
- if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
- buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
- me.frm.set_value("tc_name", company_doc.default_buying_terms);
- }
-
frappe.run_serially([
() => me.frm.script_manager.trigger("currency"),
() => me.update_item_tax_map(),
diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js
index 17341d1..e7743e0 100644
--- a/erpnext/public/js/financial_statements.js
+++ b/erpnext/public/js/financial_statements.js
@@ -74,22 +74,24 @@
});
});
- const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
+ if (report.page){
+ const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
- report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
- var filters = report.get_values();
- frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
- });
+ report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
+ var filters = report.get_values();
+ frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
+ });
- report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
- var filters = report.get_values();
- frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
- });
+ report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
+ var filters = report.get_values();
+ frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
+ });
- report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
- var filters = report.get_values();
- frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
- });
+ report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
+ var filters = report.get_values();
+ frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
+ });
+ }
}
};
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 44a4957..80ade70 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -71,6 +71,10 @@
let warehouse = this.item?.type_of_transaction === "Outward" ?
(this.item.warehouse || this.item.s_warehouse) : "";
+ if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
+ warehouse = this.get_warehouse();
+ }
+
return {
'item_code': this.item.item_code,
'warehouse': ["=", warehouse]
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index efb9820..2f6775f 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -124,6 +124,7 @@
),
title=_("Note"),
indicator="yellow",
+ alert=True,
)
return new_customer_name
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 654f297..9827b56 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -346,8 +346,8 @@
return _make_sales_order(source_name, target_doc)
-def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False):
- customer = _make_customer(source_name, ignore_permissions, customer_group)
+def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
+ customer = _make_customer(source_name, ignore_permissions)
ordered_items = frappe._dict(
frappe.db.get_all(
"Sales Order Item",
@@ -507,50 +507,51 @@
return doclist
-def _make_customer(source_name, ignore_permissions=False, customer_group=None):
+def _make_customer(source_name, ignore_permissions=False):
quotation = frappe.db.get_value(
- "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1
+ "Quotation",
+ source_name,
+ ["order_type", "quotation_to", "party_name", "customer_name"],
+ as_dict=1,
)
- if quotation and quotation.get("party_name"):
- if not frappe.db.exists("Customer", quotation.get("party_name")):
- lead_name = quotation.get("party_name")
- customer_name = frappe.db.get_value(
- "Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True
- )
- if not customer_name:
- from erpnext.crm.doctype.lead.lead import _make_customer
+ if quotation.quotation_to == "Customer":
+ return frappe.get_doc("Customer", quotation.party_name)
- customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions)
- customer = frappe.get_doc(customer_doclist)
- customer.flags.ignore_permissions = ignore_permissions
- customer.customer_group = customer_group
+ # If the Quotation is not to a Customer, it must be to a Lead.
+ # Check if a Customer already exists for the Lead.
+ existing_customer_for_lead = frappe.db.get_value("Customer", {"lead_name": quotation.party_name})
+ if existing_customer_for_lead:
+ return frappe.get_doc("Customer", existing_customer_for_lead)
- try:
- customer.insert()
- return customer
- except frappe.NameError:
- if frappe.defaults.get_global_default("cust_master_name") == "Customer Name":
- customer.run_method("autoname")
- customer.name += "-" + lead_name
- customer.insert()
- return customer
- else:
- raise
- except frappe.MandatoryError as e:
- mandatory_fields = e.args[0].split(":")[1].split(",")
- mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
+ # If no Customer exists for the Lead, create a new Customer.
+ return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions)
- frappe.local.message_log = []
- lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
- message = (
- _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
- )
- message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
- message += _("Please create Customer from Lead {0}.").format(lead_link)
- frappe.throw(message, title=_("Mandatory Missing"))
- else:
- return customer_name
- else:
- return frappe.get_doc("Customer", quotation.get("party_name"))
+def create_customer_from_lead(lead_name, ignore_permissions=False):
+ from erpnext.crm.doctype.lead.lead import _make_customer
+
+ customer = _make_customer(lead_name, ignore_permissions=ignore_permissions)
+ customer.flags.ignore_permissions = ignore_permissions
+
+ try:
+ customer.insert()
+ return customer
+ except frappe.MandatoryError as e:
+ handle_mandatory_error(e, customer, lead_name)
+
+
+def handle_mandatory_error(e, customer, lead_name):
+ from frappe.utils import get_link_to_form
+
+ mandatory_fields = e.args[0].split(":")[1].split(",")
+ mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
+
+ frappe.local.message_log = []
+ message = (
+ _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
+ )
+ message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
+ message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name))
+
+ frappe.throw(message, title=_("Mandatory Missing"))
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 2a4855e..86c7a72 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -609,6 +609,61 @@
quotation.items[0].conversion_factor = 2.23
self.assertRaises(frappe.ValidationError, quotation.save)
+ def test_item_tax_template_for_quotation(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ if not frappe.db.exists("Account", {"account_name": "_Test Vat", "company": "_Test Company"}):
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test Vat",
+ "company": "_Test Company",
+ "account_type": "Tax",
+ "root_type": "Asset",
+ "is_group": 0,
+ "parent_account": "Tax Assets - _TC",
+ "tax_rate": 10,
+ }
+ ).insert()
+
+ if not frappe.db.exists("Item Tax Template", "Vat Template - _TC"):
+ doc = frappe.get_doc(
+ {
+ "doctype": "Item Tax Template",
+ "name": "Vat Template",
+ "title": "Vat Template",
+ "company": "_Test Company",
+ "taxes": [
+ {
+ "tax_type": "_Test Vat - _TC",
+ "tax_rate": 5,
+ }
+ ],
+ }
+ ).insert()
+
+ item_doc = make_item("_Test Item Tax Template QTN", {"is_stock_item": 1})
+ if not frappe.db.exists(
+ "Item Tax", {"parent": item_doc.name, "item_tax_template": "Vat Template - _TC"}
+ ):
+ item_doc.append("taxes", {"item_tax_template": "Vat Template - _TC"})
+ item_doc.save()
+
+ quotation = make_quotation(
+ item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1
+ )
+ self.assertFalse(quotation.taxes)
+
+ quotation.append_taxes_from_item_tax_template()
+ quotation.save()
+ self.assertTrue(quotation.taxes)
+ for row in quotation.taxes:
+ self.assertEqual(row.account_head, "_Test Vat - _TC")
+ self.assertAlmostEqual(row.base_tax_amount, quotation.total * 5 / 100)
+
+ item_doc.taxes = []
+ item_doc.save()
+
test_records = frappe.get_test_records("Quotation")
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 87aeeac..9599980 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -118,6 +118,7 @@
"oldfieldtype": "Link",
"options": "Item",
"print_width": "150px",
+ "reqd": 1,
"width": "150px"
},
{
@@ -908,7 +909,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-11-24 13:24:55.756320",
+ "modified": "2024-01-25 14:24:00.330219",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py
index 25f5b4b..fa7b9b9 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py
@@ -43,7 +43,8 @@
gross_profit: DF.Currency
image: DF.Attach | None
is_free_item: DF.Check
- item_code: DF.Link | None
+ is_stock_item: DF.Check
+ item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data
item_tax_rate: DF.Code | None
diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
index 00acc80..72b7fa2 100644
--- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
+++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py
@@ -210,7 +210,6 @@
.where(
(so.docstatus == 1)
& (so.status.isin(["To Deliver and Bill", "To Bill", "To Pay"]))
- & (so.payment_terms_template != "NULL")
& (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date])
)
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 68a3854..876b6a4 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -908,8 +908,8 @@
@frappe.whitelist()
def is_deletion_job_running(company):
job_id = generate_id_for_deletion_job(company)
- job_name = get_job(job_id).get_id() # job name will have site prefix
if is_job_enqueued(job_id):
+ job_name = get_job(job_id).get_id() # job name will have site prefix
frappe.throw(
_("A Transaction Deletion Job: {0} is already running for {1}").format(
frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company)
diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
index 319d435..844e786 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py
@@ -4,9 +4,10 @@
import unittest
import frappe
+from frappe.tests.utils import FrappeTestCase
-class TestTransactionDeletionRecord(unittest.TestCase):
+class TestTransactionDeletionRecord(FrappeTestCase):
def setUp(self):
create_company("Dunder Mifflin Paper Co")
@@ -14,7 +15,7 @@
frappe.db.rollback()
def test_doctypes_contain_company_field(self):
- tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
+ tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
for doctype in tdr.doctypes:
contains_company = False
doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"]
@@ -27,17 +28,27 @@
def test_no_of_docs_is_correct(self):
for i in range(5):
create_task("Dunder Mifflin Paper Co")
- tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
+ tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
for doctype in tdr.doctypes:
if doctype.doctype_name == "Task":
self.assertEqual(doctype.no_of_docs, 5)
def test_deletion_is_successful(self):
create_task("Dunder Mifflin Paper Co")
- create_transaction_deletion_request("Dunder Mifflin Paper Co")
+ create_transaction_deletion_doc("Dunder Mifflin Paper Co")
tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"})
self.assertEqual(tasks_containing_company, [])
+ def test_company_transaction_deletion_request(self):
+ from erpnext.setup.doctype.company.company import create_transaction_deletion_request
+
+ # don't reuse below company for other test cases
+ company = "Deep Space Exploration"
+ create_company(company)
+
+ # below call should not raise any exceptions or throw errors
+ create_transaction_deletion_request(company)
+
def create_company(company_name):
company = frappe.get_doc(
@@ -46,7 +57,7 @@
company.insert(ignore_if_duplicate=True)
-def create_transaction_deletion_request(company):
+def create_transaction_deletion_doc(company):
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
tdr.insert()
tdr.submit()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
index d4a574d..2440701 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
@@ -8,6 +8,7 @@
"Stock Entry": "delivery_note_no",
"Quality Inspection": "reference_name",
"Auto Repeat": "reference_document",
+ "Purchase Receipt": "inter_company_reference",
},
"internal_links": {
"Sales Order": ["items", "against_sales_order"],
@@ -22,6 +23,9 @@
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
{"label": _("Returns"), "items": ["Stock Entry"]},
{"label": _("Subscription"), "items": ["Auto Repeat"]},
- {"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]},
+ {
+ "label": _("Internal Transfer"),
+ "items": ["Material Request", "Purchase Order", "Purchase Receipt"],
+ },
],
}
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 3fdda2c..0f12f38 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -1597,8 +1597,8 @@
{
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty if args.get("qty") is not None else 1,
- "rate": args.rate if args.get("rate") is not None else 100,
+ "qty": args.get("qty", 1),
+ "rate": args.get("rate", 100),
"conversion_factor": 1.0,
"serial_and_batch_bundle": bundle_id,
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1,
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index d90b71a..03fe20b 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -514,6 +514,13 @@
schedule_date() {
set_schedule_date(this.frm);
}
+
+ qty(doc, cdt, cdn) {
+ var row = frappe.get_doc(cdt, cdn);
+ row.amount = flt(row.qty) * flt(row.rate);
+ frappe.model.set_value(cdt, cdn, "amount", row.amount);
+ refresh_field("amount", row.name, row.parentfield);
+ }
};
// for backward compatibility: combine new and previous states
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 57ba5bb..ee5a176 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -21,9 +21,7 @@
get_serial_nos_from_bundle,
make_serial_batch_bundle,
)
-from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
class TestPurchaseReceipt(FrappeTestCase):
@@ -735,7 +733,6 @@
po.cancel()
def test_serial_no_against_purchase_receipt(self):
- from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
item_code = "Test Manual Created Serial No"
if not frappe.db.exists("Item", item_code):
@@ -1020,6 +1017,11 @@
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
+ create_stock_reconciliation,
+ )
+ from erpnext.stock.get_item_details import get_valuation_rate
+ from erpnext.stock.utils import get_stock_balance
prepare_data_for_internal_transfer()
@@ -1034,6 +1036,22 @@
company="_Test Company with perpetual inventory",
)
+ if (
+ get_valuation_rate(
+ pr1.items[0].item_code, "_Test Company with perpetual inventory", warehouse="Stores - TCP1"
+ )
+ != 50
+ ):
+ balance = get_stock_balance(item_code=pr1.items[0].item_code, warehouse="Stores - TCP1")
+ create_stock_reconciliation(
+ item_code=pr1.items[0].item_code,
+ company="_Test Company with perpetual inventory",
+ warehouse="Stores - TCP1",
+ qty=balance,
+ rate=50,
+ do_not_save=True,
+ )
+
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
@@ -1071,7 +1089,8 @@
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
expected_gle = [
- ["Stock In Hand - TCP1", 272.5, 0.0],
+ ["Stock In Hand - TCP1", 250.0, 0.0],
+ ["Cost of Goods Sold - TCP1", 22.5, 0.0],
["_Test Account Stock In Hand - TCP1", 0.0, 250.0],
["_Test Account Shipping Charges - TCP1", 0.0, 22.5],
]
@@ -1687,7 +1706,7 @@
pr.items[0].rejected_warehouse = from_warehouse
pr.save()
- self.assertRaises(OverAllowanceError, pr.submit)
+ self.assertRaises(frappe.ValidationError, pr.submit)
# Step 5: Test Over Receipt Allowance
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
@@ -1701,6 +1720,7 @@
to_warehouse=target_warehouse,
)
+ pr.reload()
pr.submit()
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
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 63cc938..9cad8f6 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
@@ -250,6 +250,7 @@
for d in self.entries:
available_qty = 0
+
if self.has_serial_no:
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
else:
@@ -892,6 +893,13 @@
elif batch_nos:
self.set("entries", batch_nos)
+ def delete_serial_batch_entries(self):
+ SBBE = frappe.qb.DocType("Serial and Batch Entry")
+
+ frappe.qb.from_(SBBE).delete().where(SBBE.parent == self.name).run()
+
+ self.set("entries", [])
+
@frappe.whitelist()
def download_blank_csv_template(content):
@@ -1374,10 +1382,12 @@
elif kwargs.based_on == "Expiry":
order_by = "amc_expiry_date asc"
- filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")}
+ filters = {"item_code": kwargs.item_code}
- if kwargs.warehouse:
- filters["warehouse"] = kwargs.warehouse
+ if not kwargs.get("ignore_warehouse"):
+ filters["warehouse"] = ("is", "set")
+ if kwargs.warehouse:
+ filters["warehouse"] = kwargs.warehouse
# Since SLEs are not present against Reserved Stock [POS invoices, SRE], need to ignore reserved serial nos.
ignore_serial_nos = get_reserved_serial_nos(kwargs)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 6819968..788ae0d 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -156,6 +156,7 @@
"warehouse": item.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
+ "ignore_warehouse": 1,
}
)
)
@@ -780,7 +781,20 @@
current_qty = 0.0
if row.current_serial_and_batch_bundle:
- current_qty = self.get_qty_for_serial_and_batch_bundle(row)
+ current_qty = self.get_current_qty_for_serial_or_batch(row)
+ elif row.serial_no:
+ item_dict = get_stock_balance_for(
+ row.item_code,
+ row.warehouse,
+ self.posting_date,
+ self.posting_time,
+ voucher_no=self.name,
+ )
+
+ current_qty = item_dict.get("qty")
+ row.current_serial_no = item_dict.get("serial_nos")
+ row.current_valuation_rate = item_dict.get("rate")
+ val_rate = item_dict.get("rate")
elif row.batch_no:
current_qty = get_batch_qty_for_stock_reco(
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
@@ -788,15 +802,16 @@
precesion = row.precision("current_qty")
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
- val_rate = get_valuation_rate(
- row.item_code,
- row.warehouse,
- self.doctype,
- self.name,
- company=self.company,
- batch_no=row.batch_no,
- serial_and_batch_bundle=row.current_serial_and_batch_bundle,
- )
+ if not row.serial_no:
+ val_rate = get_valuation_rate(
+ row.item_code,
+ row.warehouse,
+ self.doctype,
+ self.name,
+ company=self.company,
+ batch_no=row.batch_no,
+ serial_and_batch_bundle=row.current_serial_and_batch_bundle,
+ )
row.current_valuation_rate = val_rate
row.current_qty = current_qty
@@ -842,11 +857,56 @@
return allow_negative_stock
- def get_qty_for_serial_and_batch_bundle(self, row):
+ def get_current_qty_for_serial_or_batch(self, row):
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
- precision = doc.entries[0].precision("qty")
+ current_qty = 0.0
+ if doc.has_serial_no:
+ current_qty = self.get_current_qty_for_serial_nos(doc)
+ elif doc.has_batch_no:
+ current_qty = self.get_current_qty_for_batch_nos(doc)
- current_qty = 0
+ return abs(current_qty)
+
+ def get_current_qty_for_serial_nos(self, doc):
+ serial_nos_details = get_available_serial_nos(
+ frappe._dict(
+ {
+ "item_code": doc.item_code,
+ "warehouse": doc.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_no": self.name,
+ "ignore_warehouse": 1,
+ }
+ )
+ )
+
+ if not serial_nos_details:
+ return 0.0
+
+ doc.delete_serial_batch_entries()
+ current_qty = 0.0
+ for serial_no_row in serial_nos_details:
+ current_qty += 1
+ doc.append(
+ "entries",
+ {
+ "serial_no": serial_no_row.serial_no,
+ "qty": -1,
+ "warehouse": doc.warehouse,
+ "batch_no": serial_no_row.batch_no,
+ },
+ )
+
+ doc.set_incoming_rate(save=True)
+ doc.calculate_qty_and_amount(save=True)
+ doc.db_update_all()
+
+ return current_qty
+
+ def get_current_qty_for_batch_nos(self, doc):
+ current_qty = 0.0
+ precision = doc.entries[0].precision("qty")
for d in doc.entries:
qty = (
get_batch_qty(
@@ -864,7 +924,7 @@
current_qty += qty
- return abs(current_qty)
+ return current_qty
def get_batch_qty_for_stock_reco(
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 70e9fb2..0bbfed4 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -925,6 +925,74 @@
self.assertEqual(len(serial_batch_bundle), 0)
+ def test_backdated_purchase_receipt_with_stock_reco(self):
+ item_code = self.make_item(
+ properties={
+ "is_stock_item": 1,
+ "has_serial_no": 1,
+ "serial_no_series": "TEST-SERIAL-.###",
+ }
+ ).name
+
+ warehouse = "_Test Warehouse - _TC"
+
+ # Step - 1: Create a Backdated Purchase Receipt
+
+ pr1 = make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
+ )
+ pr1.reload()
+
+ serial_nos = sorted(get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle))[:5]
+
+ # Step - 2: Create a Stock Reconciliation
+ sr1 = create_stock_reconciliation(
+ item_code=item_code,
+ warehouse=warehouse,
+ qty=5,
+ serial_no=serial_nos,
+ )
+
+ data = frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["serial_no", "actual_qty", "stock_value_difference"],
+ filters={"voucher_no": sr1.name, "is_cancelled": 0},
+ order_by="creation",
+ )
+
+ for d in data:
+ if d.actual_qty < 0:
+ self.assertEqual(d.actual_qty, -10.0)
+ self.assertAlmostEqual(d.stock_value_difference, -1000.0)
+ else:
+ self.assertEqual(d.actual_qty, 5.0)
+ self.assertAlmostEqual(d.stock_value_difference, 500.0)
+
+ # Step - 3: Create a Purchase Receipt before the first Purchase Receipt
+ make_purchase_receipt(
+ item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
+ )
+
+ data = frappe.get_all(
+ "Stock Ledger Entry",
+ fields=["serial_no", "actual_qty", "stock_value_difference"],
+ filters={"voucher_no": sr1.name, "is_cancelled": 0},
+ order_by="creation",
+ )
+
+ for d in data:
+ if d.actual_qty < 0:
+ self.assertEqual(d.actual_qty, -20.0)
+ self.assertAlmostEqual(d.stock_value_difference, -3000.0)
+ else:
+ self.assertEqual(d.actual_qty, 5.0)
+ self.assertAlmostEqual(d.stock_value_difference, 500.0)
+
+ active_serial_no = frappe.get_all(
+ "Serial No", filters={"status": "Active", "item_code": item_code}
+ )
+ self.assertEqual(len(active_serial_no), 5)
+
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js
index 6de5f00..fe6e83e 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.js
+++ b/erpnext/stock/report/stock_balance/stock_balance.js
@@ -99,7 +99,7 @@
"fieldname": 'ignore_closing_balance',
"label": __('Ignore Closing Balance'),
"fieldtype": 'Check',
- "default": 1
+ "default": 0
},
],
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 0370666..45764f3 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -9,9 +9,18 @@
import frappe
from frappe import _, scrub
from frappe.model.meta import get_field_precision
-from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum
-from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
+from frappe.utils import (
+ cint,
+ cstr,
+ flt,
+ get_link_to_form,
+ getdate,
+ now,
+ nowdate,
+ nowtime,
+ parse_json,
+)
import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
@@ -712,11 +721,10 @@
if (
sle.voucher_type == "Stock Reconciliation"
- and (
- sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
- )
+ and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
and sle.voucher_detail_no
and not self.args.get("sle_id")
+ and sle.is_cancelled == 0
):
self.reset_actual_qty_for_stock_reco(sle)
@@ -737,6 +745,23 @@
if sle.serial_and_batch_bundle:
self.calculate_valuation_for_serial_batch_bundle(sle)
+ elif sle.serial_no and not self.args.get("sle_id"):
+ # Only run in reposting
+ self.get_serialized_values(sle)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
+
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
+ self.wh_data.valuation_rate
+ )
+ elif (
+ sle.batch_no
+ and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
+ and not self.args.get("sle_id")
+ ):
+ # Only run in reposting
+ self.update_batched_values(sle)
else:
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
# assert
@@ -782,6 +807,45 @@
):
self.update_outgoing_rate_on_transaction(sle)
+ def get_serialized_values(self, sle):
+ incoming_rate = flt(sle.incoming_rate)
+ actual_qty = flt(sle.actual_qty)
+ serial_nos = cstr(sle.serial_no).split("\n")
+
+ if incoming_rate < 0:
+ # wrong incoming rate
+ incoming_rate = self.wh_data.valuation_rate
+
+ stock_value_change = 0
+ if actual_qty > 0:
+ stock_value_change = actual_qty * incoming_rate
+ else:
+ # In case of delivery/stock issue, get average purchase rate
+ # of serial nos of current entry
+ if not sle.is_cancelled:
+ outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
+ stock_value_change = -1 * outgoing_value
+ else:
+ stock_value_change = actual_qty * sle.outgoing_rate
+
+ new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
+
+ if new_stock_qty > 0:
+ new_stock_value = (
+ self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
+ ) + stock_value_change
+ if new_stock_value >= 0:
+ # calculate new valuation rate only if stock value is positive
+ # else it remains the same as that of previous entry
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
+
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
+ allow_zero_rate = self.check_if_allow_zero_valuation_rate(
+ sle.voucher_type, sle.voucher_detail_no
+ )
+ if not allow_zero_rate:
+ self.wh_data.valuation_rate = self.get_fallback_rate(sle)
+
def reset_actual_qty_for_stock_reco(self, sle):
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
@@ -795,6 +859,36 @@
if abs(sle.actual_qty) == 0.0:
sle.is_cancelled = 1
+ if sle.serial_and_batch_bundle and frappe.get_cached_value(
+ "Item", sle.item_code, "has_serial_no"
+ ):
+ self.update_serial_no_status(sle)
+
+ def update_serial_no_status(self, sle):
+ from erpnext.stock.serial_batch_bundle import get_serial_nos
+
+ serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
+ if not serial_nos:
+ return
+
+ warehouse = None
+ status = "Inactive"
+
+ if sle.actual_qty > 0:
+ warehouse = sle.warehouse
+ status = "Active"
+
+ sn_table = frappe.qb.DocType("Serial No")
+
+ query = (
+ frappe.qb.update(sn_table)
+ .set(sn_table.warehouse, warehouse)
+ .set(sn_table.status, status)
+ .where(sn_table.name.isin(serial_nos))
+ )
+
+ query.run()
+
def calculate_valuation_for_serial_batch_bundle(self, sle):
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
@@ -1171,11 +1265,12 @@
outgoing_rate = get_batch_incoming_rate(
item_code=sle.item_code,
warehouse=sle.warehouse,
- serial_and_batch_bundle=sle.serial_and_batch_bundle,
+ batch_no=sle.batch_no,
posting_date=sle.posting_date,
posting_time=sle.posting_time,
creation=sle.creation,
)
+
if outgoing_rate is None:
# This can *only* happen if qty available for the batch is zero.
# in such case fall back various other rates.
@@ -1449,11 +1544,10 @@
def get_batch_incoming_rate(
- item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
+ item_code, warehouse, batch_no, posting_date, posting_time, creation=None
):
sle = frappe.qb.DocType("Stock Ledger Entry")
- batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
posting_date, posting_time
@@ -1464,28 +1558,13 @@
== CombineDatetime(posting_date, posting_time)
) & (sle.creation < creation)
- batches = frappe.get_all(
- "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
- )
-
batch_details = (
frappe.qb.from_(sle)
- .inner_join(batch_ledger)
- .on(sle.serial_and_batch_bundle == batch_ledger.parent)
- .select(
- Sum(
- Case()
- .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
- .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
- ).as_("batch_value"),
- Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
- "batch_qty"
- ),
- )
+ .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
.where(
(sle.item_code == item_code)
& (sle.warehouse == warehouse)
- & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
+ & (sle.batch_no == batch_no)
& (sle.is_cancelled == 0)
)
.where(timestamp_condition)
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f29e7ea..76af5d7 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -289,6 +289,21 @@
in_rate = batch_obj.get_incoming_rate()
+ elif (args.get("serial_no") or "").strip() and not args.get("serial_and_batch_bundle"):
+ in_rate = get_avg_purchase_rate(args.get("serial_no"))
+ elif (
+ args.get("batch_no")
+ and frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True)
+ and not args.get("serial_and_batch_bundle")
+ ):
+ in_rate = get_batch_incoming_rate(
+ item_code=args.get("item_code"),
+ warehouse=args.get("warehouse"),
+ batch_no=args.get("batch_no"),
+ posting_date=args.get("posting_date"),
+ posting_time=args.get("posting_time"),
+ )
+
else:
valuation_method = get_valuation_method(args.get("item_code"))
previous_sle = get_previous_sle(args)
@@ -319,6 +334,38 @@
return flt(in_rate)
+def get_batch_incoming_rate(
+ item_code, warehouse, batch_no, posting_date, posting_time, creation=None
+):
+
+ sle = frappe.qb.DocType("Stock Ledger Entry")
+
+ timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
+ posting_date, posting_time
+ )
+ if creation:
+ timestamp_condition |= (
+ CombineDatetime(sle.posting_date, sle.posting_time)
+ == CombineDatetime(posting_date, posting_time)
+ ) & (sle.creation < creation)
+
+ batch_details = (
+ frappe.qb.from_(sle)
+ .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
+ .where(
+ (sle.item_code == item_code)
+ & (sle.warehouse == warehouse)
+ & (sle.batch_no == batch_no)
+ & (sle.serial_and_batch_bundle.isnull())
+ & (sle.is_cancelled == 0)
+ )
+ .where(timestamp_condition)
+ ).run(as_dict=True)
+
+ if batch_details and batch_details[0].batch_qty:
+ return batch_details[0].batch_value / batch_details[0].batch_qty
+
+
def get_avg_purchase_rate(serial_nos):
"""get average value of serial numbers"""