mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 03:45:08 +00:00
Merge pull request #48832 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
23
.github/workflows/patch_faux.yml
vendored
Normal file
23
.github/workflows/patch_faux.yml
vendored
Normal 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"
|
||||
25
.github/workflows/server-tests-mariadb-faux.yml
vendored
Normal file
25
.github/workflows/server-tests-mariadb-faux.yml
vendored
Normal 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"
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", "");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}",
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 || [];
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}),
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user