Merge pull request #48832 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2025-07-29 21:08:37 +05:30
committed by GitHub
72 changed files with 1157 additions and 226 deletions

23
.github/workflows/patch_faux.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
name: Skipped Patch Test
permissions: none
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"

View File

@@ -0,0 +1,25 @@
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
name: Skipped Tests
permissions: {}
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"

View File

@@ -41,6 +41,7 @@
"show_payment_schedule_in_print",
"item_price_settings_section",
"maintain_same_internal_transaction_rate",
"fetch_valuation_rate_for_internal_transaction",
"column_break_feyo",
"maintain_same_rate_action",
"role_to_override_stop_action",
@@ -622,6 +623,12 @@
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
},
{
"default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction",
"fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction"
}
],
"icon": "icon-cog",
@@ -629,7 +636,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-06-23 15:55:33.346398",
"modified": "2025-07-18 13:56:47.192437",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -48,6 +48,7 @@ class AccountsSettings(Document):
enable_immutable_ledger: DF.Check
enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_valuation_rate_for_internal_transaction: DF.Check
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check

View File

@@ -163,6 +163,7 @@ frappe.ui.form.on("Journal Entry", {
});
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
voucher_type: function (frm) {

View File

@@ -273,6 +273,7 @@ frappe.ui.form.on("Payment Entry", {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
contact_person: function (frm) {

View File

@@ -1487,13 +1487,14 @@ class PaymentEntry(AccountsController):
"voucher_no": self.name,
"voucher_detail_no": invoice.name,
}
if invoice.reconcile_effect_on:
posting_date = invoice.reconcile_effect_on
else:
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
posting_date = get_reconciliation_effect_date(
invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date
)
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)

View File

@@ -1714,6 +1714,67 @@ class TestPaymentReconciliation(FrappeTestCase):
)
self.assertEqual(len(pl_entries), 3)
def test_advance_payment_reconciliation_date_for_older_date(self):
old_settings = frappe.db.get_value(
"Company",
self.company,
[
"reconciliation_takes_effect_on",
"default_advance_paid_account",
"book_advance_payments_in_separate_party_account",
],
as_dict=True,
)
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
self.supplier = "_Test Supplier"
pi1 = self.create_purchase_invoice(qty=10, rate=100)
po = self.create_purchase_order(qty=10, rate=100)
pay = get_payment_entry(po.doctype, po.name)
pay.paid_amount = 1000
pay.save().submit()
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.default_advance_account = self.advance_payable_account
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = 100
pr.reconcile()
pay.reload()
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
# test setting of date if not available
frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None)
pay.reload()
pay.cancel()
pay.reload()
pi1.reload()
po.reload()
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
pi1.cancel()
po.cancel()
frappe.db.set_value("Company", self.company, old_settings)
def test_advance_payment_reconciliation_against_journal_for_customer(self):
frappe.db.set_value(
"Company",

View File

@@ -14,6 +14,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
}
company() {
erpnext.utils.set_letter_head(this.frm);
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
this.frm.set_value("set_warehouse", "");
this.frm.set_value("taxes_and_charges", "");

View File

@@ -135,6 +135,7 @@ frappe.ui.form.on("POS Profile", {
company: function (frm) {
frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
toggle_display_account_head: function (frm) {

View File

@@ -92,6 +92,7 @@ frappe.ui.form.on("Process Statement Of Accounts", {
frm.set_value("account", "");
frm.set_value("cost_center", "");
frm.set_value("project", "");
erpnext.utils.set_letter_head(frm);
},
report: function (frm) {
let filters = {

View File

@@ -970,7 +970,7 @@ class PurchaseInvoice(BuyingController):
self.get_provisional_accounts()
for item in self.get("items"):
if flt(item.base_net_amount):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code:
frappe.get_cached_value("Item", item.item_code, "asset_category")

View File

@@ -9,6 +9,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -17,6 +18,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_supplier()
self.create_usd_receivable_account()
self.create_item()
self.clear_old_entries()
@@ -364,13 +366,13 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
# Assert 'Advance Paid'
so.reload()
pe.reload()
self.assertEqual(so.advance_paid, 100)
self.assertEqual(so.advance_paid, 0)
self.assertEqual(len(pe.references), 0)
self.assertEqual(pe.unallocated_amount, 100)
pe.cancel()
so.reload()
self.assertEqual(so.advance_paid, 100)
self.assertEqual(so.advance_paid, 0)
def test_06_unreconcile_advance_from_payment_entry(self):
self.enable_advance_as_liability()
@@ -417,7 +419,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
so2.reload()
pe.reload()
self.assertEqual(so1.advance_paid, 150)
self.assertEqual(so2.advance_paid, 110)
self.assertEqual(so2.advance_paid, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 110)
@@ -468,3 +470,56 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
self.assertEqual(so.advance_paid, 1000)
self.disable_advance_as_liability()
def test_unreconcile_advance_from_journal_entry(self):
po = create_purchase_order(
company=self.company,
supplier=self.supplier,
item=self.item,
qty=1,
rate=100,
transaction_date=today(),
do_not_submit=False,
)
je = frappe.get_doc(
{
"doctype": "Journal Entry",
"company": self.company,
"voucher_type": "Journal Entry",
"posting_date": po.transaction_date,
"multi_currency": True,
"accounts": [
{
"account": "Creditors - _TC",
"party_type": "Supplier",
"party": po.supplier,
"debit_in_account_currency": 100,
"is_advance": "Yes",
"reference_type": po.doctype,
"reference_name": po.name,
},
{"account": "Cash - _TC", "credit_in_account_currency": 100},
],
}
)
je.save().submit()
po.reload()
self.assertEqual(po.advance_paid, 100)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": je.doctype,
"voucher_no": je.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 1)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEqual([po.name], allocations)
unreconcile.save().submit()
po.reload()
self.assertEqual(po.advance_paid, 0)

View File

@@ -86,10 +86,25 @@ class UnreconcilePayment(Document):
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
if doc.doctype in get_advance_payment_doctypes():
self.make_advance_payment_ledger(alloc)
doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
def make_advance_payment_ledger(self, alloc):
if alloc.allocated_amount > 0:
doc = frappe.new_doc("Advance Payment Ledger Entry")
doc.company = self.company
doc.voucher_type = self.voucher_type
doc.voucher_no = self.voucher_no
doc.against_voucher_type = alloc.reference_doctype
doc.against_voucher_no = alloc.reference_name
doc.amount = -1 * alloc.allocated_amount
doc.event = "Unreconcile"
doc.currency = alloc.account_currency
doc.flags.ignore_permissions = 1
doc.save()
@frappe.whitelist()
def doc_has_references(doctype: str | None = None, docname: str | None = None):

View File

@@ -14,9 +14,16 @@ erpnext.utils.add_dimensions("Cash Flow", 10);
frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
frappe.query_reports["Cash Flow"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Cash Flow"]["filters"].push(
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_opening_and_closing_balance",
label: __("Show Opening and Closing Balance"),
fieldtype: "Check",
}
);

View File

@@ -2,9 +2,13 @@
# For license information, please see license.txt
from datetime import timedelta
import frappe
from frappe import _
from frappe.utils import cstr
from frappe.query_builder import DocType
from frappe.utils import cstr, flt
from pypika import Order
from erpnext.accounts.report.financial_statements import (
get_columns,
@@ -12,6 +16,7 @@ from erpnext.accounts.report.financial_statements import (
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
set_gl_entries_by_account,
)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_net_profit_loss,
@@ -119,10 +124,20 @@ def execute(filters=None):
filters,
)
add_total_row_account(
net_change_in_cash = add_total_row_account(
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
)
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True)
if filters.show_opening_and_closing_balance:
show_opening_and_closing_balance(data, period_list, company_currency, net_change_in_cash, filters)
columns = get_columns(
filters.periodicity,
period_list,
filters.accumulated_values,
filters.company,
True,
)
chart = get_chart_data(columns, data, company_currency)
@@ -255,6 +270,137 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
out.append(total_row)
out.append({})
return total_row
def show_opening_and_closing_balance(out, period_list, currency, net_change_in_cash, filters):
opening_balance = {
"section_name": "Opening",
"section": "Opening",
"currency": currency,
}
closing_balance = {
"section_name": "Closing (Opening + Total)",
"section": "Closing (Opening + Total)",
"currency": currency,
}
opening_amount = get_opening_balance(filters.company, period_list, filters) or 0.0
running_total = opening_amount
for i, period in enumerate(period_list):
key = period["key"]
change = net_change_in_cash.get(key, 0.0)
opening_balance[key] = opening_amount if i == 0 else running_total
running_total += change
closing_balance[key] = running_total
opening_balance["total"] = opening_balance[period_list[0]["key"]]
closing_balance["total"] = closing_balance[period_list[-1]["key"]]
out.extend([opening_balance, net_change_in_cash, closing_balance, {}])
def get_opening_balance(company, period_list, filters):
from copy import deepcopy
cash_value = {}
account_types = get_cash_flow_accounts()
net_profit_loss = 0.0
local_filters = deepcopy(filters)
local_filters.start_date, local_filters.end_date = get_opening_range_using_fiscal_year(
company, period_list
)
for section in account_types:
section_name = section.get("section_name")
cash_value.setdefault(section_name, 0.0)
if section_name == "Operations":
net_profit_loss += get_net_income(company, period_list, local_filters)
for account in section.get("account_types", []):
account_type = account.get("account_type")
local_filters.account_type = account_type
amount = get_account_type_based_gl_data(company, local_filters) or 0.0
if account_type == "Depreciation":
cash_value[section_name] += amount * -1
else:
cash_value[section_name] += amount
return sum(cash_value.values()) + net_profit_loss
def get_net_income(company, period_list, filters):
gl_entries_by_account_for_income, gl_entries_by_account_for_expense = {}, {}
income, expense = 0.0, 0.0
from_date, to_date = get_opening_range_using_fiscal_year(company, period_list)
for root_type in ["Income", "Expense"]:
for root in frappe.db.sql(
"""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""",
root_type,
as_dict=1,
):
set_gl_entries_by_account(
company,
from_date,
to_date,
filters,
gl_entries_by_account_for_income
if root_type == "Income"
else gl_entries_by_account_for_expense,
root.lft,
root.rgt,
root_type=root_type,
ignore_closing_entries=True,
)
for entries in gl_entries_by_account_for_income.values():
for entry in entries:
if entry.posting_date <= to_date:
amount = (entry.debit - entry.credit) * -1
income = flt((income + amount), 2)
for entries in gl_entries_by_account_for_expense.values():
for entry in entries:
if entry.posting_date <= to_date:
amount = entry.debit - entry.credit
expense = flt((expense + amount), 2)
return income - expense
def get_opening_range_using_fiscal_year(company, period_list):
first_from_date = period_list[0]["from_date"]
previous_day = first_from_date - timedelta(days=1)
# Get the earliest fiscal year for the company
FiscalYear = DocType("Fiscal Year")
FiscalYearCompany = DocType("Fiscal Year Company")
earliest_fy = (
frappe.qb.from_(FiscalYear)
.join(FiscalYearCompany)
.on(FiscalYearCompany.parent == FiscalYear.name)
.select(FiscalYear.year_start_date)
.where(FiscalYearCompany.company == company)
.orderby(FiscalYear.year_start_date, order=Order.asc)
.limit(1)
).run(as_dict=True)
if not earliest_fy:
frappe.throw(_("Not able to find the earliest Fiscal Year for the given company."))
company_start_date = earliest_fy[0]["year_start_date"]
return company_start_date, previous_day
def get_report_summary(summary_data, currency):
report_summary = []
@@ -276,7 +422,7 @@ def get_chart_data(columns, data, currency):
for section in data
if section.get("parent_section") is None and section.get("currency")
]
datasets = datasets[:-1]
datasets = datasets[:-2]
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}

View File

@@ -275,12 +275,25 @@ class PartyLedgerSummaryReport:
if gle.posting_date < self.filters.from_date or gle.is_opening == "Yes":
self.party_data[gle.party].opening_balance += amount
else:
if amount > 0:
self.party_data[gle.party].invoiced_amount += amount
elif gle.voucher_no in self.return_invoices:
self.party_data[gle.party].return_amount -= amount
# Cache the party data reference to avoid repeated dictionary lookups
party_data = self.party_data[gle.party]
# Check if this is a direct return invoice (most specific condition first)
if gle.voucher_no in self.return_invoices:
party_data.return_amount -= amount
# Check if this entry is against a return invoice
elif gle.against_voucher in self.return_invoices:
# For entries against return invoices, positive amounts are payments
if amount > 0:
party_data.paid_amount -= amount
else:
party_data.invoiced_amount += amount
# Normal transaction logic
else:
self.party_data[gle.party].paid_amount -= amount
if amount > 0:
party_data.invoiced_amount += amount
else:
party_data.paid_amount -= amount
out = []
for party, row in self.party_data.items():
@@ -289,7 +302,7 @@ class PartyLedgerSummaryReport:
or row.invoiced_amount
or row.paid_amount
or row.return_amount
or row.closing_amount
or row.closing_balance # Fixed typo from closing_amount to closing_balance
):
total_party_adjustment = sum(
amount for amount in self.party_adjustment_details.get(party, {}).values()
@@ -313,6 +326,7 @@ class PartyLedgerSummaryReport:
gle.party,
gle.voucher_type,
gle.voucher_no,
gle.against_voucher, # For handling returned invoices (Credit/Debit Notes)
gle.debit,
gle.credit,
gle.is_opening,

View File

@@ -150,3 +150,157 @@ class TestCustomerLedgerSummary(FrappeTestCase, AccountsTestMixin):
for field in expected_after_cr_and_payment:
with self.subTest(field=field):
self.assertEqual(report[0].get(field), expected_after_cr_and_payment.get(field))
def test_journal_voucher_against_return_invoice(self):
filters = {"company": self.company, "from_date": today(), "to_date": today()}
# Create Sales Invoice of 10 qty at rate 100 (Amount: 1000.0)
si1 = self.create_sales_invoice(do_not_submit=True)
si1.save().submit()
expected = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 0,
"return_amount": 0,
"closing_balance": 1000.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create Payment Entry (Receive) for the first invoice
pe1 = self.create_payment_entry(si1.name, True)
pe1.paid_amount = 1000 # Full payment 1000.0
pe1.save().submit()
expected_after_payment = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 1000.0,
"return_amount": 0,
"closing_balance": 0.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_payment:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_payment.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create Credit Note (return invoice) for first invoice (1000.0)
cr_note = self.create_credit_note(si1.name, do_not_submit=True)
cr_note.items[0].qty = -10 # 1 item of qty 10 at rate 100 (Amount: 1000.0)
cr_note.save().submit()
expected_after_cr_note = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 1000.0,
"return_amount": 1000.0,
"closing_balance": -1000.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_cr_note:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_cr_note.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create Payment Entry for the returned amount (1000.0) - Pay the customer back
pe2 = get_payment_entry("Sales Invoice", cr_note.name, bank_account=self.cash)
pe2.insert().submit()
expected_after_cr_and_return_payment = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 0,
"return_amount": 1000.0,
"closing_balance": 0,
"currency": "INR",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_cr_and_return_payment:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_cr_and_return_payment.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create second Sales Invoice of 10 qty at rate 100 (Amount: 1000.0)
si2 = self.create_sales_invoice(do_not_submit=True)
si2.save().submit()
# Create Payment Entry (Receive) for the second invoice - payment (500.0)
pe3 = self.create_payment_entry(si2.name, True)
pe3.paid_amount = 500 # Partial payment 500.0
pe3.save().submit()
expected_after_cr_and_payment = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0.0,
"invoiced_amount": 2000.0,
"paid_amount": 500.0,
"return_amount": 1000.0,
"closing_balance": 500.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_cr_and_payment:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_cr_and_payment.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)

View File

@@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters):
def get_items(filters, additional_table_columns):
doctype = "Purchase Invoice"
pi = frappe.qb.DocType(doctype).as_("invoice")
pii = frappe.qb.DocType(f"{doctype} Item").as_("invoice_item")
pi = frappe.qb.DocType("Purchase Invoice")
pii = frappe.qb.DocType("Purchase Invoice Item")
Item = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(pi)
@@ -375,7 +375,7 @@ def get_items(filters, additional_table_columns):
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(query, filters)
query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True)

View File

@@ -394,15 +394,18 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
return query
def apply_order_by_conditions(query, filters):
def apply_order_by_conditions(doctype, query, filters):
invoice = f"`tab{doctype}`"
invoice_item = f"`tab{doctype} Item`"
if not filters.get("group_by"):
query += "order by invoice.posting_date desc, invoice_item.item_group desc"
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc"
elif filters.get("group_by") == "Invoice":
query += "order by invoice_item.parent desc"
query += f" order by {invoice_item}.parent desc"
elif filters.get("group_by") == "Item":
query += "order by invoice_item.item_code"
query += f" order by {invoice_item}.item_code"
elif filters.get("group_by") == "Item Group":
query += "order by invoice_item.item_group"
query += f" order by {invoice_item}.item_group"
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc"
@@ -412,8 +415,8 @@ def apply_order_by_conditions(query, filters):
def get_items(filters, additional_query_columns, additional_conditions=None):
doctype = "Sales Invoice"
si = frappe.qb.DocType("Sales Invoice").as_("invoice")
sii = frappe.qb.DocType("Sales Invoice Item").as_("invoice_item")
si = frappe.qb.DocType("Sales Invoice")
sii = frappe.qb.DocType("Sales Invoice Item")
sip = frappe.qb.DocType("Sales Invoice Payment")
item = frappe.qb.DocType("Item")
@@ -487,12 +490,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Sales Invoice")
match_conditions = build_match_conditions(doctype)
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(query, filters)
query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True)

View File

@@ -27,6 +27,7 @@ from frappe.utils import (
nowdate,
)
from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion
import erpnext
@@ -717,7 +718,9 @@ def update_reference_in_payment_entry(
# Update Reconciliation effect date in reference
if payment_entry.book_advance_payments_in_separate_party_account:
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
reconcile_on = get_reconciliation_effect_date(
d.against_voucher_type, d.against_voucher, payment_entry.company, payment_entry.posting_date
)
reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no:
@@ -771,20 +774,21 @@ def update_reference_in_payment_entry(
return row, update_advance_paid
def get_reconciliation_effect_date(reference, company, posting_date):
def get_reconciliation_effect_date(against_voucher_type, against_voucher, company, posting_date):
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", company, "reconciliation_takes_effect_on"
)
# default
reconcile_on = posting_date
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
if against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(
reference.against_voucher_type, reference.against_voucher, date_field
)
reconcile_on = frappe.db.get_value(against_voucher_type, against_voucher, date_field)
if getdate(reconcile_on) < getdate(posting_date):
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
@@ -2354,6 +2358,8 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
def build_qb_match_conditions(doctype, user=None) -> list:
match_filters = build_match_conditions(doctype, user, False)
criterion = []
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
if match_filters:
from frappe import qb
@@ -2362,6 +2368,12 @@ def build_qb_match_conditions(doctype, user=None) -> list:
for filter in match_filters:
for d, names in filter.items():
fieldname = d.lower().replace(" ", "_")
criterion.append(_dt[fieldname].isin(names))
field = _dt[fieldname]
cond = field.isin(names)
if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | field.isin(names)
criterion.append(cond)
return criterion

View File

@@ -249,14 +249,11 @@ class Asset(AccountsController):
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
def prepare_depreciation_data(self):
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
else:
self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
def validate_item(self):
item = frappe.get_cached_value(
@@ -314,6 +311,9 @@ class Asset(AccountsController):
)
def set_missing_values(self):
if not self.calculate_depreciation:
return
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
@@ -1300,7 +1300,6 @@ def create_new_asset_after_split(asset, split_qty):
)
new_asset.insert()
add_asset_activity(
new_asset.name,
_("Asset created after being split from Asset {0}").format(get_link_to_form("Asset", asset.name)),

View File

@@ -445,6 +445,27 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
def test_asset_splitting_without_depreciation(self):
asset = create_asset(
calculate_depreciation=0,
asset_quantity=10,
available_for_use_date="2020-01-01",
purchase_date="2020-01-01",
gross_purchase_amount=120000,
submit=1,
)
self.assertEqual(asset.asset_quantity, 10)
self.assertEqual(asset.gross_purchase_amount, 120000)
new_asset = split_asset(asset.name, 2)
asset.load_from_db()
self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(new_asset.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000)
def test_asset_splitting(self):
asset = create_asset(
calculate_depreciation=1,
@@ -1492,7 +1513,6 @@ class TestDepreciationBasics(AssetSetup):
)
self.assertSequenceEqual(gle, expected_gle)
self.assertEqual(asset.get("value_after_depreciation"), 0)
def test_expected_value_change(self):
"""

View File

@@ -74,6 +74,7 @@
"fieldname": "completion_date",
"fieldtype": "Datetime",
"label": "Completion Date",
"mandatory_depends_on": "eval:doc.repair_status==\"Completed\"",
"no_copy": 1
},
{
@@ -249,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-06-29 22:30:00.589597",
"modified": "2025-07-29 15:14:34.044564",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -287,10 +288,11 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -70,7 +70,7 @@ class AssetRepair(AccountsController):
)
def validate_dates(self):
if self.completion_date and (self.failure_date > self.completion_date):
if self.completion_date and (getdate(self.failure_date) > getdate(self.completion_date)):
frappe.throw(
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
)
@@ -303,7 +303,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"company": self.company,
@@ -322,7 +322,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"posting_date": self.completion_date,
"company": self.company,
},
item=self,
@@ -356,7 +356,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"posting_date": self.completion_date,
"company": self.company,
},
item=self,
@@ -373,7 +373,7 @@ class AssetRepair(AccountsController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"posting_date": self.completion_date,
"against_voucher_type": "Stock Entry",
"against_voucher": stock_entry.name,
"company": self.company,

View File

@@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import (
get_asset_account,
@@ -359,6 +359,7 @@ def create_asset_repair(**args):
if args.submit:
asset_repair.repair_status = "Completed"
asset_repair.completion_date = add_days(args.failure_date, 1)
asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
if args.stock_consumption:

View File

@@ -48,7 +48,6 @@ class AssetValueAdjustment(Document):
def on_submit(self):
self.make_depreciation_entry()
self.set_value_after_depreciation()
self.update_asset(self.new_asset_value)
add_asset_activity(
self.asset,
@@ -80,9 +79,6 @@ class AssetValueAdjustment(Document):
def set_difference_amount(self):
self.difference_amount = flt(self.new_asset_value - self.current_asset_value)
def set_value_after_depreciation(self):
frappe.db.set_value("Asset", self.asset, "value_after_depreciation", self.new_asset_value)
def set_current_asset_value(self):
if not self.current_asset_value and self.asset:
self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
@@ -164,12 +160,8 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
def update_asset(self, asset_value=None):
asset = frappe.get_doc("Asset", self.asset)
if not asset.calculate_depreciation:
asset.value_after_depreciation = asset_value
asset.save()
return
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
asset = self.update_asset_value_after_depreciation(difference_amount)
asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
@@ -188,19 +180,6 @@ class AssetValueAdjustment(Document):
get_link_to_form(self.get("doctype"), self.get("name")),
)
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
if asset.calculate_depreciation:
for row in asset.finance_books:
if cstr(row.finance_book) == cstr(self.finance_book):
salvage_value_adjustment = (
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
)
row.expected_value_after_useful_life += salvage_value_adjustment
row.value_after_depreciation += flt(difference_amount)
row.db_update()
asset.db_update()
make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset,
notes,
@@ -212,6 +191,23 @@ class AssetValueAdjustment(Document):
asset.save()
asset.set_status()
def update_asset_value_after_depreciation(self, difference_amount):
asset = frappe.get_doc("Asset", self.asset)
if asset.calculate_depreciation:
for row in asset.finance_books:
if cstr(row.finance_book) == cstr(self.finance_book):
salvage_value_adjustment = (
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
)
row.expected_value_after_useful_life += salvage_value_adjustment
row.value_after_depreciation = row.value_after_depreciation + flt(difference_amount)
row.db_update()
asset.value_after_depreciation += flt(difference_amount)
asset.db_update()
return asset
def get_adjusted_salvage_value_amount(self, row, difference_amount):
if row.expected_value_after_useful_life:
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100

View File

@@ -282,13 +282,13 @@ class TestAssetValueAdjustment(unittest.TestCase):
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name,
current_asset_value=54000,
current_asset_value=120000.0,
new_asset_value=50000.0,
date="2023-08-21",
)
adj_doc.submit()
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
self.assertEqual(difference_amount, -4000)
self.assertEqual(difference_amount, -70000)
asset_doc.load_from_db()
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)

View File

@@ -12,7 +12,6 @@ erpnext.buying.setup_buying_controller();
frappe.ui.form.on("Purchase Order", {
setup: function (frm) {
frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"];
if (frm.doc.is_old_subcontracting_flow) {
frm.set_query("reserve_warehouse", "supplied_items", function () {
return {
@@ -140,6 +139,10 @@ frappe.ui.form.on("Purchase Order", {
},
onload: function (frm) {
var ignore_list = ["Unreconcile Payment", "Unreconcile Payment Entries"];
frm.ignore_doctypes_on_cancel_all = Object.hasOwn(frm, "ignore_doctypes_on_cancel_all")
? frm.ignore_doctypes_on_cancel_all.concat(ignore_list)
: ignore_list;
set_schedule_date(frm);
if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today());

View File

@@ -310,9 +310,12 @@ class BuyingController(SubcontractingController):
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
total_rejected_qty = 0
for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items:
stock_and_asset_items_qty += flt(d.qty)
total_rejected_qty += flt(d.get("rejected_qty", 0))
stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx
@@ -324,12 +327,19 @@ class BuyingController(SubcontractingController):
valuation_amount_adjustment = total_valuation_amount
for i, item in enumerate(self.get("items")):
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = (
flt(item.base_net_amount) / stock_and_asset_items_amount
if stock_and_asset_items_amount
else flt(item.qty) / stock_and_asset_items_qty
)
if (
item.item_code
and (item.qty or item.get("rejected_qty"))
and item.item_code in stock_and_asset_items
):
if stock_and_asset_items_qty:
item_proportion = (
flt(item.base_net_amount) / stock_and_asset_items_amount
if stock_and_asset_items_amount
else flt(item.qty) / stock_and_asset_items_qty
)
elif total_rejected_qty:
item_proportion = flt(item.get("rejected_qty")) / flt(total_rejected_qty)
if i == (last_item_idx - 1):
item.item_tax_amount = flt(
@@ -351,7 +361,19 @@ class BuyingController(SubcontractingController):
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate
if (
not net_rate
and item.get("rejected_qty")
and frappe.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
)
):
net_rate = item.rejected_qty * item.net_rate
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
if not qty_in_stock_uom and item.get("rejected_qty"):
qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor)
if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = (

View File

@@ -96,7 +96,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[
"Completed",
"eval:(self.per_billed == 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
"eval:(self.per_billed >= 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],

View File

@@ -78,11 +78,11 @@ class EmailCampaign(Document):
end_date = getdate(self.end_date)
today_date = getdate(today())
if start_date > today_date:
self.status = "Scheduled"
self.db_set("status", "Scheduled", update_modified=False)
elif end_date >= today_date:
self.status = "In Progress"
self.db_set("status", "In Progress", update_modified=False)
elif end_date < today_date:
self.status = "Completed"
self.db_set("status", "Completed", update_modified=False)
# called through hooks to send campaign mails to leads

View File

@@ -51,6 +51,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"non_negative": 1,
"reqd": 1
},
{

View File

@@ -325,13 +325,18 @@ frappe.ui.form.on("Work Order", {
return operations_data;
},
},
function (data) {
function () {
const selected_rows = dialog.fields_dict["operations"].grid.get_selected_children();
if (selected_rows.length == 0) {
frappe.msgprint(__("Please select atleast one operation to create Job Card"));
return;
}
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
freeze: true,
args: {
work_order: frm.doc.name,
operations: data.operations,
operations: selected_rows,
},
callback: function () {
frm.reload_doc();
@@ -342,7 +347,7 @@ frappe.ui.form.on("Work Order", {
__("Create")
);
dialog.fields_dict["operations"].grid.wrapper.find(".grid-add-row").hide();
dialog.fields_dict["operations"].grid.grid_buttons.hide();
var pending_qty = 0;
frm.doc.operations.forEach((data) => {

View File

@@ -72,6 +72,7 @@
"fieldname": "hour_rate_electricity",
"fieldtype": "Currency",
"label": "Electricity Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency"
},
@@ -81,6 +82,7 @@
"fieldname": "hour_rate_consumable",
"fieldtype": "Currency",
"label": "Consumable Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency"
},
@@ -94,6 +96,7 @@
"fieldname": "hour_rate_rent",
"fieldtype": "Currency",
"label": "Rent Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency"
},
@@ -103,6 +106,7 @@
"fieldname": "hour_rate_labour",
"fieldtype": "Currency",
"label": "Wages",
"non_negative": 1,
"oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency"
},
@@ -138,6 +142,7 @@
"fieldname": "production_capacity",
"fieldtype": "Int",
"label": "Job Capacity",
"non_negative": 1,
"reqd": 1
},
{
@@ -240,7 +245,7 @@
"idx": 1,
"image_field": "on_status_image",
"links": [],
"modified": "2024-06-20 14:17:13.806609",
"modified": "2025-07-13 16:02:13.615001",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",
@@ -260,6 +265,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",

View File

@@ -320,7 +320,7 @@ erpnext.patches.v14_0.set_period_start_end_date_in_pcv
erpnext.patches.v14_0.update_closing_balances #20-12-2024
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
erpnext.patches.v14_0.update_subscription_details
erpnext.patches.v14_0.update_subscription_details # 23-07-2025
execute:frappe.delete_doc("Report", "Tax Detail", force=True)
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc
@@ -413,4 +413,7 @@ erpnext.patches.v15_0.update_pick_list_fields
erpnext.patches.v15_0.update_pegged_currencies
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
erpnext.patches.v15_0.rename_price_list_to_buying_price_list
erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request
erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice
erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting
execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1)

View File

@@ -12,6 +12,7 @@ def execute():
subscription_invoice.invoice,
"subscription",
subscription_invoice.parent,
update_modified=False,
)
frappe.delete_doc_if_exists("DocType", "Subscription Invoice")
frappe.delete_doc_if_exists("DocType", "Subscription Invoice", force=1)

View File

@@ -0,0 +1,13 @@
import frappe
import frappe.defaults
def execute():
if frappe.db.has_column("Material Request", "buying_price_list") and (
default_buying_price_list := frappe.defaults.get_defaults().buying_price_list
):
docs = frappe.get_all(
"Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name"
)
for doc in docs:
frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list)

View File

@@ -0,0 +1,25 @@
import frappe
def execute():
docs = frappe.get_all(
"GL Entry",
filters={"voucher_type": "Subcontracting Receipt", "account": ["is", "not set"], "is_cancelled": 0},
pluck="voucher_no",
)
for doc in docs:
doc = frappe.get_doc("Subcontracting Receipt", doc)
for item in doc.supplied_items:
account, cost_center = frappe.db.get_values(
"Subcontracting Receipt Item", item.reference_name, ["expense_account", "cost_center"]
)[0]
if not item.expense_account:
item.db_set("expense_account", account)
if not item.cost_center:
item.db_set("cost_center", cost_center)
doc.docstatus = 2
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
doc.make_gl_entries()

View File

@@ -88,9 +88,9 @@ frappe.ui.form.on("Project", {
);
frm.add_custom_button(
__("Update Total Purchase Cost"),
__("Update Costing and Billing"),
() => {
frm.events.update_total_purchase_cost(frm);
frm.events.update_costing_and_billing(frm);
},
__("Actions")
);
@@ -129,15 +129,15 @@ frappe.ui.form.on("Project", {
}
},
update_total_purchase_cost: function (frm) {
update_costing_and_billing: function (frm) {
frappe.call({
method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost",
method: "erpnext.projects.doctype.project.project.update_costing_and_billing",
args: { project: frm.doc.name },
freeze: true,
freeze_message: __("Recalculating Purchase Cost against this Project..."),
freeze_message: __("Updating Costing and Billing fields against this Project..."),
callback: function (r) {
if (r && !r.exc) {
frappe.msgprint(__("Total Purchase Cost has been updated"));
frappe.msgprint(__("Costing and Billing fields has been updated"));
frm.refresh();
}
},

View File

@@ -749,12 +749,7 @@ def calculate_total_purchase_cost(project: str | None = None):
@frappe.whitelist()
def recalculate_project_total_purchase_cost(project: str | None = None):
if project:
total_purchase_cost = calculate_total_purchase_cost(project)
frappe.db.set_value(
"Project",
project,
"total_purchase_cost",
(total_purchase_cost and total_purchase_cost[0][0] or 0),
)
def update_costing_and_billing(project: str | None = None):
project = frappe.get_doc("Project", project)
project.update_costing()
project.db_update()

View File

@@ -417,7 +417,7 @@ class BOMConfigurator {
doctype: doctype,
docname: docname,
qty: data.qty,
parent: node.data.parent_id,
parent: node.data.parent_id ? node.data.parent_id : this.frm.doc.name,
},
callback: (r) => {
node.data.qty = data.qty;

View File

@@ -176,6 +176,7 @@ erpnext.buying = {
this.frm.set_value("shipping_address", r.message.shipping_address || "");
},
});
erpnext.utils.set_letter_head(this.frm)
}
supplier_address() {

View File

@@ -652,9 +652,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
me.apply_product_discount(d);
}
},
() => {
async () => {
// for internal customer instead of pricing rule directly apply valuation rate on item
if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && me.frm.doc.represents_company === me.frm.doc.company) {
const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value(
"Accounts Settings", "fetch_valuation_rate_for_internal_transaction"
);
if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && fetch_valuation_rate_for_internal_transactions) {
me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time,
me.frm.doc.doctype, me.frm.doc.company);
} else {

View File

@@ -449,6 +449,16 @@ $.extend(erpnext.utils, {
});
return fiscal_year;
},
set_letter_head: function (frm) {
if (frm.fields_dict.letter_head) {
frappe.db.get_value("Company", frm.doc.company, "default_letter_head").then((res) => {
if (res.message?.default_letter_head) {
frm.set_value("letter_head", res.message.default_letter_head);
}
});
}
},
});
erpnext.utils.select_alternate_items = function (opts) {

View File

@@ -358,7 +358,6 @@
"options": "fa fa-bullhorn"
},
{
"allow_on_submit": 1,
"fieldname": "customer_address",
"fieldtype": "Link",
"hide_days": 1,
@@ -439,7 +438,6 @@
"width": "50%"
},
{
"allow_on_submit": 1,
"fieldname": "shipping_address_name",
"fieldtype": "Link",
"hide_days": 1,
@@ -1462,6 +1460,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Auto Repeat",
"no_copy": 1,
"options": "Auto Repeat"
},
{
@@ -1664,7 +1663,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-03-03 16:49:00.676927",
"modified": "2025-07-28 12:14:29.760988",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -3,6 +3,14 @@
frappe.query_reports["Sales Partner Commission Summary"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "sales_partner",
label: __("Sales Partner"),
@@ -28,13 +36,6 @@ frappe.query_reports["Sales Partner Commission Summary"] = {
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "customer",
label: __("Customer"),

View File

@@ -35,6 +35,12 @@ def get_columns(filters):
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Territory"),
"options": "Territory",
@@ -43,7 +49,13 @@ def get_columns(filters):
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
@@ -61,6 +73,7 @@ def get_columns(filters):
"label": _("Total Commission"),
"fieldname": "total_commission",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
]
@@ -70,19 +83,19 @@ def get_columns(filters):
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
SELECT
name, customer, territory, {} as posting_date, base_net_total as amount,
sales_partner, commission_rate, total_commission
sales_partner, commission_rate, total_commission, '{}' as currency
FROM
`tab{}`
WHERE
{} and docstatus = 1 and sales_partner is not null
and sales_partner != '' order by name desc, sales_partner
""".format(date_field, filters.get("doctype"), conditions),
""".format(date_field, company_currency, filters.get("doctype"), conditions),
filters,
as_dict=1,
)

View File

@@ -141,6 +141,7 @@ class DeprecatedBatchNoValuation:
& (sle.batch_no.isnotnull())
& (sle.is_cancelled == 0)
)
.for_update()
.groupby(sle.batch_no)
)
@@ -232,6 +233,7 @@ class DeprecatedBatchNoValuation:
& (sle.is_cancelled == 0)
& (sle.batch_no.isin(self.non_batchwise_valuation_batches))
)
.for_update()
.where(timestamp_condition)
.groupby(sle.batch_no)
)
@@ -278,6 +280,7 @@ class DeprecatedBatchNoValuation:
.where(timestamp_condition)
.orderby(sle.posting_datetime, order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.for_update()
.limit(1)
)
@@ -330,6 +333,7 @@ class DeprecatedBatchNoValuation:
& (bundle.type_of_transaction.isin(["Inward", "Outward"]))
& (bundle_child.batch_no.isin(self.non_batchwise_valuation_batches))
)
.for_update()
.where(timestamp_condition)
.groupby(bundle_child.batch_no)
)

View File

@@ -10,6 +10,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.utils import cint, flt
from erpnext.accounts.party import get_due_date
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
from erpnext.controllers.selling_controller import SellingController
@@ -917,8 +918,25 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
automatically_fetch_payment_terms = cint(
frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
if automatically_fetch_payment_terms and not doc.is_return:
doc.set_payment_schedule()
if not doc.is_return:
so, doctype, fieldname = doc.get_order_details()
if (
doc.linked_order_has_payment_terms(so, fieldname, doctype)
and not automatically_fetch_payment_terms
):
payment_terms_template = frappe.db.get_value(doctype, so, "payment_terms_template")
doc.payment_terms_template = payment_terms_template
doc.due_date = get_due_date(
doc.posting_date,
"Customer",
doc.customer,
doc.company,
template_name=doc.payment_terms_template,
)
elif automatically_fetch_payment_terms:
doc.set_payment_schedule()
return doc

View File

@@ -84,6 +84,7 @@ frappe.ui.form.on("Material Request", {
company: function (frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
onload_post_render: function (frm) {

View File

@@ -307,6 +307,7 @@
"fieldname": "transfer_status",
"fieldtype": "Select",
"label": "Transfer Status",
"no_copy": 1,
"options": "\nNot Started\nIn Transit\nCompleted",
"read_only": 1
},
@@ -364,7 +365,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2025-07-11 21:03:26.588307",
"modified": "2025-07-28 15:13:49.000037",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",

View File

@@ -35,6 +35,7 @@ class MaterialRequest(BuyingController):
from erpnext.stock.doctype.material_request_item.material_request_item import MaterialRequestItem
amended_from: DF.Link | None
buying_price_list: DF.Link | None
company: DF.Link
customer: DF.Link | None
items: DF.Table[MaterialRequestItem]
@@ -46,7 +47,6 @@ class MaterialRequest(BuyingController):
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
per_ordered: DF.Percent
per_received: DF.Percent
price_list: DF.Link | None
scan_barcode: DF.Data | None
schedule_date: DF.Date | None
select_print_heading: DF.Link | None

View File

@@ -10,7 +10,11 @@ frappe.listview_settings["Material Request"] = {
} else if (doc.transfer_status == "In Transit") {
return [__("In Transit"), "yellow", "transfer_status,=,In Transit"];
} else if (doc.transfer_status == "Completed") {
return [__("Completed"), "green", "transfer_status,=,Completed"];
if (doc.status == "Transferred") {
return [__("Completed"), "green", "transfer_status,=,Completed"];
} else {
return [__("Partially Received"), "yellow", "per_ordered,<,100"];
}
}
} else if (doc.docstatus == 1 && flt(doc.per_ordered, precision) == 0) {
return [__("Pending"), "orange", "per_ordered,=,0|docstatus,=,1"];

View File

@@ -21,6 +21,14 @@ frappe.ui.form.on("Pick List", {
"Stock Entry": "Stock Entry",
};
frm.set_query("warehouse", "locations", () => {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("parent_warehouse", () => {
return {
filters: {
@@ -91,6 +99,15 @@ frappe.ui.form.on("Pick List", {
});
}
},
pick_manually: function (frm) {
frm.fields_dict.locations.grid.update_docfield_property(
"warehouse",
"read_only",
!frm.doc.pick_manually
);
},
get_item_locations: (frm) => {
// Button on the form
frm.events.set_item_locations(frm, false);

View File

@@ -201,7 +201,7 @@
},
{
"default": "0",
"description": "If enabled then system won't override the picked qty / batches / serial numbers.",
"description": "If enabled then system won't override the picked qty / batches / serial numbers / warehouse.",
"fieldname": "pick_manually",
"fieldtype": "Check",
"label": "Pick Manually"
@@ -247,7 +247,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2025-05-31 19:18:30.860044",
"modified": "2025-07-23 08:34:32.099673",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@@ -668,6 +668,10 @@ class PurchaseReceipt(BuyingController):
warehouse_with_no_account = []
for d in self.get("items"):
remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
"Asset" if d.is_fixed_asset else "Stock"
)
if (
provisional_accounting_for_non_stock_items
and d.item_code not in stock_items
@@ -679,10 +683,6 @@ class PurchaseReceipt(BuyingController):
d, gl_entries, self.posting_date, d.get("provisional_expense_account")
)
elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return):
remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
"Asset" if d.is_fixed_asset else "Stock"
)
if not (
(erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
or (d.is_fixed_asset and not d.purchase_invoice)
@@ -737,7 +737,7 @@ class PurchaseReceipt(BuyingController):
make_amount_difference_entry(d)
make_sub_contracting_gl_entries(d)
make_divisional_loss_gl_entry(d, outgoing_amount)
elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or (
elif (d.warehouse and d.qty and d.warehouse not in warehouse_with_no_account) or (
not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
and d.rejected_warehouse
and d.rejected_warehouse not in warehouse_with_no_account
@@ -750,10 +750,18 @@ class PurchaseReceipt(BuyingController):
if d.rejected_qty and frappe.db.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
):
stock_asset_rbnb = (
self.get_company_default("asset_received_but_not_billed")
if d.is_fixed_asset
else self.get_company_default("stock_received_but_not_billed")
)
stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse)
stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"]
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
if not d.qty:
make_stock_received_but_not_billed_entry(d)
if warehouse_with_no_account:
frappe.msgprint(

View File

@@ -21,8 +21,8 @@ frappe.listview_settings["Purchase Receipt"] = {
return [__("To Bill"), "orange", "per_billed,<,100|docstatus,=,1"];
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
return [__("Partly Billed"), "yellow", "per_billed,<,100|docstatus,=,1"];
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
return [__("Completed"), "green", "per_billed,=,100|docstatus,=,1"];
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) >= 100) {
return [__("Completed"), "green", "per_billed,>=,100|docstatus,=,1"];
}
},

View File

@@ -3669,7 +3669,7 @@ class TestPurchaseReceipt(FrappeTestCase):
columns, data = execute(
filters=frappe._dict(
{"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company}
{"item_code": [item_code], "warehouse": [pr.items[0].warehouse], "company": pr.company}
)
)
@@ -4261,6 +4261,47 @@ class TestPurchaseReceipt(FrappeTestCase):
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0)
def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self):
item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
warehouse = create_warehouse(
"_Test In-ward Warehouse",
company="_Test Company with perpetual inventory",
)
rej_warehouse = create_warehouse(
"_Test Warehouse - Rejected Material",
company="_Test Company with perpetual inventory",
)
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1)
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1)
pr = make_purchase_receipt(
item_code=item.name,
qty=0,
rate=100,
company=company,
warehouse=warehouse,
rejected_qty=5,
rejected_warehouse=rej_warehouse,
)
gl_entry = frappe.get_all(
"GL Entry", filters={"debit": (">", 0), "voucher_no": pr.name}, pluck="name"
)
stock_value_diff = frappe.db.get_value(
"Stock Ledger Entry",
{"warehouse": rej_warehouse, "voucher_no": pr.name},
"stock_value_difference",
)
self.assertTrue(gl_entry)
self.assertEqual(stock_value_diff, 500.00)
def test_no_valuation_rate_for_rejected_materials(self):
item = make_item("Test Item with Rej Material No Valuation", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"

View File

@@ -387,7 +387,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
f"""
SELECT distinct item_code, item_name, item_group
SELECT distinct item_code, item_name
FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}

View File

@@ -1514,6 +1514,82 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertTrue(len(stock_ledgers) == 2)
def test_serial_no_backdated_stock_reco(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
serial_item = self.make_item(
"Test Serial Item Stock Reco Backdated",
{
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "TSISRB.####",
},
).name
warehouse = "_Test Warehouse - _TC"
se = make_stock_entry(
item_code=serial_item,
target=warehouse,
qty=1,
basic_rate=100,
use_serial_batch_fields=1,
)
serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0]
status = frappe.get_value(
"Serial No",
serial_no,
"status",
)
self.assertTrue(status == "Active")
sr = create_stock_reconciliation(
item_code=serial_item,
warehouse=warehouse,
qty=1,
rate=200,
use_serial_batch_fields=1,
serial_no=serial_no,
)
sr.reload()
status = frappe.get_value(
"Serial No",
serial_no,
"status",
)
self.assertTrue(status == "Active")
make_stock_entry(
item_code=serial_item,
source=warehouse,
qty=1,
basic_rate=100,
use_serial_batch_fields=1,
)
status = frappe.get_value(
"Serial No",
serial_no,
"status",
)
self.assertFalse(status == "Active")
sr.cancel()
status = frappe.get_value(
"Serial No",
serial_no,
"status",
)
self.assertFalse(status == "Active")
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@@ -242,19 +242,35 @@ def get_warehouses_based_on_account(account, company=None):
# Will be use for frappe.qb
def apply_warehouse_filter(query, sle, filters):
if warehouse := filters.get("warehouse"):
warehouse_table = frappe.qb.DocType("Warehouse")
if not (warehouses := filters.get("warehouse")):
return query
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
chilren_subquery = (
frappe.qb.from_(warehouse_table)
.select(warehouse_table.name)
.where(
(warehouse_table.lft >= lft)
& (warehouse_table.rgt <= rgt)
& (warehouse_table.name == sle.warehouse)
)
)
query = query.where(ExistsCriterion(chilren_subquery))
warehouse_table = frappe.qb.DocType("Warehouse")
if isinstance(warehouses, str):
warehouses = [warehouses]
warehouse_range = frappe.get_all(
"Warehouse",
filters={
"name": ("in", warehouses),
},
fields=["lft", "rgt"],
as_list=True,
)
child_query = frappe.qb.from_(warehouse_table).select(warehouse_table.name)
range_conditions = [
(warehouse_table.lft >= lft) & (warehouse_table.rgt <= rgt) for lft, rgt in warehouse_range
]
combined_condition = range_conditions[0]
for condition in range_conditions[1:]:
combined_condition = combined_condition | condition
child_query = child_query.where(combined_condition).where(warehouse_table.name == sle.warehouse)
query = query.where(ExistsCriterion(child_query))
return query

View File

@@ -36,38 +36,57 @@ frappe.query_reports["Stock Balance"] = {
},
{
fieldname: "item_code",
label: __("Item"),
fieldtype: "Link",
label: __("Items"),
fieldtype: "MultiSelectList",
width: "80",
options: "Item",
get_query: function () {
get_data: async function (txt) {
let item_group = frappe.query_report.get_filter_value("item_group");
return {
query: "erpnext.controllers.queries.item_query",
filters: {
...(item_group && { item_group }),
is_stock_item: 1,
},
let filters = {
...(item_group && { item_group }),
is_stock_item: 1,
};
let { message: data } = await frappe.call({
method: "erpnext.controllers.queries.item_query",
args: {
doctype: "Item",
txt: txt,
searchfield: "name",
start: 0,
page_len: 10,
filters: filters,
as_dict: 1,
},
});
data = data.map(({ name, description }) => {
return {
value: name,
description: description,
};
});
return data || [];
},
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
label: __("Warehouses"),
fieldtype: "MultiSelectList",
width: "80",
options: "Warehouse",
get_query: () => {
get_data: (txt) => {
let warehouse_type = frappe.query_report.get_filter_value("warehouse_type");
let company = frappe.query_report.get_filter_value("company");
return {
filters: {
...(warehouse_type && { warehouse_type }),
...(company && { company }),
},
let filters = {
...(warehouse_type && { warehouse_type }),
...(company && { company }),
};
return frappe.db.get_link_options("Warehouse", txt, filters);
},
},
{

View File

@@ -24,8 +24,8 @@ class StockBalanceFilter(TypedDict):
from_date: str
to_date: str
item_group: str | None
item: str | None
warehouse: str | None
item: list[str] | None
warehouse: list[str] | None
warehouse_type: str | None
include_uom: str | None # include extra info in converted UOM
show_stock_ageing_data: bool
@@ -283,8 +283,11 @@ class StockBalanceReport:
)
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.filters.get(fieldname):
query = query.where(table[fieldname] == self.filters.get(fieldname))
if value := self.filters.get(fieldname):
if isinstance(value, list | tuple):
query = query.where(table[fieldname].isin(value))
else:
query = query.where(table[fieldname] == value)
return query.run(as_dict=True)
@@ -347,6 +350,7 @@ class StockBalanceReport:
if self.filters.get("warehouse"):
query = apply_warehouse_filter(query, sle, self.filters)
elif warehouse_type := self.filters.get("warehouse_type"):
query = (
query.join(warehouse_table)
@@ -361,13 +365,11 @@ class StockBalanceReport:
children = get_descendants_of("Item Group", item_group, ignore_permissions=True)
query = query.where(item_table.item_group.isin([*children, item_group]))
for field in ["item_code", "brand"]:
if not self.filters.get(field):
continue
elif field == "item_code":
query = query.where(item_table.name == self.filters.get(field))
else:
query = query.where(item_table[field] == self.filters.get(field))
if item_codes := self.filters.get("item_code"):
query = query.where(item_table.name.isin(item_codes))
if brand := self.filters.get("brand"):
query = query.where(item_table.brand == brand)
return query

View File

@@ -23,7 +23,7 @@ class TestStockBalance(FrappeTestCase):
self.filters = _dict(
{
"company": "_Test Company",
"item_code": self.item.name,
"item_code": [self.item.name],
"from_date": "2020-01-01",
"to_date": str(today()),
}
@@ -165,6 +165,6 @@ class TestStockBalance(FrappeTestCase):
variant.save()
self.generate_stock_ledger(variant.name, [_dict(qty=5, rate=10)])
rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": variant.name}))
rows = stock_balance(self.filters.update({"show_variant_attributes": 1, "item_code": [variant.name]}))
self.assertPartialDictEq(attributes, rows[0])
self.assertInvariants(rows)

View File

@@ -27,25 +27,44 @@ frappe.query_reports["Stock Ledger"] = {
},
{
fieldname: "warehouse",
label: __("Warehouse"),
fieldtype: "Link",
label: __("Warehouses"),
fieldtype: "MultiSelectList",
options: "Warehouse",
get_query: function () {
get_data: function (txt) {
const company = frappe.query_report.get_filter_value("company");
return {
filters: { company: company },
};
return frappe.db.get_link_options("Warehouse", txt, {
company: company,
});
},
},
{
fieldname: "item_code",
label: __("Item"),
fieldtype: "Link",
label: __("Items"),
fieldtype: "MultiSelectList",
options: "Item",
get_query: function () {
return {
query: "erpnext.controllers.queries.item_query",
};
get_data: async function (txt) {
let { message: data } = await frappe.call({
method: "erpnext.controllers.queries.item_query",
args: {
doctype: "Item",
txt: txt,
searchfield: "name",
start: 0,
page_len: 10,
filters: {},
as_dict: 1,
},
});
data = data.map(({ name, description }) => {
return {
value: name,
description: description,
};
});
return data || [];
},
},
{

View File

@@ -456,19 +456,23 @@ def get_items(filters):
query = frappe.qb.from_(item).select(item.name)
conditions = []
if item_code := filters.get("item_code"):
conditions.append(item.name == item_code)
if item_codes := filters.get("item_code"):
conditions.append(item.name.isin(item_codes))
else:
if brand := filters.get("brand"):
conditions.append(item.brand == brand)
if item_group := filters.get("item_group"):
if condition := get_item_group_condition(item_group, item):
conditions.append(condition)
if filters.get("item_group") and (
condition := get_item_group_condition(filters.get("item_group"), item)
):
conditions.append(condition)
items = []
if conditions:
for condition in conditions:
query = query.where(condition)
items = [r[0] for r in query.run()]
return items
@@ -505,6 +509,7 @@ def get_item_details(items, sl_entries, include_uom):
return item_details
# TODO: THIS IS NOT USED
def get_sle_conditions(filters):
conditions = []
if filters.get("warehouse"):
@@ -535,8 +540,8 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
}
for fields in ["item_code", "warehouse"]:
if filters.get(fields):
query_filters[fields] = filters.get(fields)
if value := filters.get(fields):
query_filters[fields] = ("in", value)
opening_data = frappe.get_all(
"Stock Ledger Entry",
@@ -567,8 +572,16 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
)
for field in ["item_code", "warehouse", "company"]:
if filters.get(field):
query = query.where(table[field] == filters.get(field))
value = filters.get(field)
if not value:
continue
if isinstance(value, list | tuple):
query = query.where(table[field].isin(value))
else:
query = query.where(table[field] == value)
bundle_data = query.run(as_dict=True)
@@ -623,13 +636,34 @@ def get_opening_balance(filters, columns, sl_entries):
return row
def get_warehouse_condition(warehouse):
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
if warehouse_details:
return f" exists (select name from `tabWarehouse` wh \
where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
def get_warehouse_condition(warehouses):
if not warehouses:
return ""
return ""
if isinstance(warehouses, str):
warehouses = [warehouses]
warehouse_range = frappe.get_all(
"Warehouse",
filters={
"name": ("in", warehouses),
},
fields=["lft", "rgt"],
as_list=True,
)
if not warehouse_range:
return ""
alias = "wh"
conditions = []
for lft, rgt in warehouse_range:
conditions.append(f"({alias}.lft >= {lft} and {alias}.rgt <= {rgt})")
conditions = " or ".join(conditions)
return f" exists (select name from `tabWarehouse` {alias} \
where ({conditions}) and warehouse = {alias}.name)"
def get_item_group_condition(item_group, item_table=None):

View File

@@ -17,7 +17,7 @@ class TestStockLedgerReeport(FrappeTestCase):
company="_Test Company",
from_date=today(),
to_date=add_days(today(), 30),
item_code="_Test Stock Report Serial Item",
item_code=["_Test Stock Report Serial Item"],
)
def tearDown(self) -> None:

View File

@@ -17,8 +17,15 @@ batch = get_random("Batch")
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}),
("Stock Ledger", {"batch_no": batch}),
("Stock Ledger", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}),
("Stock Balance", {"_optional": True}),
("Stock Ledger", {"item_code": ["_Test Item"], "warehouse": ["_Test Warehouse - _TC"]}),
(
"Stock Balance",
{
"item_code": ["_Test Item"],
"warehouse": ["_Test Warehouse - _TC"],
"item_group": "_Test Item Group",
},
),
("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}),
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),

View File

@@ -359,7 +359,7 @@ class SerialBatchBundle:
self.update_serial_no_status_warehouse(self.sle, serial_nos)
def update_serial_no_status_warehouse(self, sle, serial_nos):
warehouse = self.warehouse if sle.actual_qty > 0 else None
warehouse = sle.warehouse if sle.actual_qty > 0 else None
if isinstance(serial_nos, str):
serial_nos = [serial_nos]
@@ -723,6 +723,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
& (parent.is_cancelled == 0)
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
.groupby(child.batch_no)
)

View File

@@ -1670,8 +1670,20 @@ def get_stock_ledger_entries(
):
"""get stock ledger entries filtered by specific posting datetime conditions"""
conditions = f" and posting_datetime {operator} %(posting_datetime)s"
if previous_sle.get("warehouse"):
conditions += " and warehouse = %(warehouse)s"
if item_code := previous_sle.get("item_code"):
if isinstance(item_code, list | tuple):
conditions += " and item_code in %(item_code)s"
else:
conditions += " and item_code = %(item_code)s"
if warehouse := previous_sle.get("warehouse"):
if isinstance(warehouse, list | tuple):
conditions += " and warehouse in %(warehouse)s"
else:
conditions += " and warehouse = %(warehouse)s"
elif previous_sle.get("warehouse_condition"):
conditions += " and " + previous_sle.get("warehouse_condition")
@@ -1714,8 +1726,7 @@ def get_stock_ledger_entries(
"""
select *, posting_datetime as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and is_cancelled = 0
where is_cancelled = 0
{conditions}
order by posting_datetime {order}, creation {order}
{limit} {for_update}""".format(

View File

@@ -192,6 +192,10 @@ frappe.ui.form.on("Subcontracting Order", {
});
},
company: function (frm) {
erpnext.utils.set_letter_head(frm);
},
get_materials_from_supplier: function (frm) {
let sco_rm_details = [];