mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-07 23:31:20 +00:00
Merge pull request #50981 from frappe/version-15-hotfix
This commit is contained in:
@@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
JournalEntryTaxWithholding(self).apply()
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
@@ -1281,6 +1196,230 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Accounts table cannot be blank."))
|
||||
|
||||
|
||||
class JournalEntryTaxWithholding:
|
||||
def __init__(self, journal_entry):
|
||||
self.doc: JournalEntry = journal_entry
|
||||
self.party = None
|
||||
self.party_type = None
|
||||
self.party_account = None
|
||||
self.party_row = None
|
||||
self.existing_tds_rows = []
|
||||
self.precision = None
|
||||
self.has_multiple_parties = False
|
||||
|
||||
# Direction fields based on party type
|
||||
self.party_field = None # "credit" for Supplier, "debit" for Customer
|
||||
self.reverse_field = None # opposite of party_field
|
||||
|
||||
def apply(self):
|
||||
if not self._set_party_info():
|
||||
return
|
||||
|
||||
self._setup_direction_fields()
|
||||
self._reset_existing_tds()
|
||||
|
||||
if not self._should_apply_tds():
|
||||
self._cleanup_duplicate_tds_rows(None)
|
||||
return
|
||||
|
||||
if self.has_multiple_parties:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
net_total = self._calculate_net_total()
|
||||
if net_total <= 0:
|
||||
return
|
||||
|
||||
tds_details = self._get_tds_details(net_total)
|
||||
if not tds_details or not tds_details.get("tax_amount"):
|
||||
return
|
||||
|
||||
self._create_or_update_tds_row(tds_details)
|
||||
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
|
||||
|
||||
self._recalculate_totals()
|
||||
|
||||
def _should_apply_tds(self):
|
||||
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
|
||||
|
||||
def _set_party_info(self):
|
||||
for row in self.doc.get("accounts"):
|
||||
if row.party_type in ("Customer", "Supplier") and row.party:
|
||||
if self.party and row.party != self.party:
|
||||
self.has_multiple_parties = True
|
||||
|
||||
if not self.party:
|
||||
self.party = row.party
|
||||
self.party_type = row.party_type
|
||||
self.party_account = row.account
|
||||
self.party_row = row
|
||||
|
||||
if row.get("is_tax_withholding_account"):
|
||||
self.existing_tds_rows.append(row)
|
||||
|
||||
return bool(self.party)
|
||||
|
||||
def _setup_direction_fields(self):
|
||||
"""
|
||||
For Supplier (TDS): party has credit, TDS reduces credit
|
||||
For Customer (TCS): party has debit, TCS increases debit
|
||||
"""
|
||||
if self.party_type == "Supplier":
|
||||
self.party_field = "credit"
|
||||
self.reverse_field = "debit"
|
||||
else: # Customer
|
||||
self.party_field = "debit"
|
||||
self.reverse_field = "credit"
|
||||
|
||||
self.precision = self.doc.precision(self.party_field, self.party_row)
|
||||
|
||||
def _reset_existing_tds(self):
|
||||
for row in self.existing_tds_rows:
|
||||
# TDS amount is always in credit (liability to government)
|
||||
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
|
||||
if not tds_amount:
|
||||
continue
|
||||
|
||||
self._update_party_amount(tds_amount, is_reversal=True)
|
||||
|
||||
# zero_out_tds_row
|
||||
row.update(
|
||||
{
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
def _update_party_amount(self, amount, is_reversal=False):
|
||||
amount = flt(amount, self.precision)
|
||||
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
|
||||
|
||||
# Determine which field the party amount is in
|
||||
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
|
||||
|
||||
# If amount is in reverse field, flip the signs
|
||||
if active_field == self.reverse_field:
|
||||
amount = -amount
|
||||
amount_in_party_currency = -amount_in_party_currency
|
||||
|
||||
# Direction multiplier based on party type:
|
||||
# Customer (TCS): +1 (add to debit)
|
||||
# Supplier (TDS): -1 (subtract from credit)
|
||||
direction = 1 if self.party_type == "Customer" else -1
|
||||
|
||||
# Reversal inverts the direction
|
||||
if is_reversal:
|
||||
direction = -direction
|
||||
|
||||
adjustment = amount * direction
|
||||
adjustment_in_party_currency = amount_in_party_currency * direction
|
||||
|
||||
active_field_account_currency = f"{active_field}_in_account_currency"
|
||||
|
||||
self.party_row.update(
|
||||
{
|
||||
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
|
||||
active_field_account_currency: flt(
|
||||
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
|
||||
self.precision,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def _calculate_net_total(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
account_type_map = get_account_type_map(self.doc.company)
|
||||
|
||||
return flt(
|
||||
sum(
|
||||
d.get(self.reverse_field) - d.get(self.party_field)
|
||||
for d in self.doc.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
and d.account != self.party_account
|
||||
and not d.get("is_tax_withholding_account")
|
||||
),
|
||||
self.precision,
|
||||
)
|
||||
|
||||
def _get_tds_details(self, net_total):
|
||||
return get_party_tax_withholding_details(
|
||||
frappe._dict(
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"doctype": self.doc.doctype,
|
||||
"company": self.doc.company,
|
||||
"posting_date": self.doc.posting_date,
|
||||
"tax_withholding_net_total": net_total,
|
||||
"base_tax_withholding_net_total": net_total,
|
||||
"grand_total": net_total,
|
||||
}
|
||||
),
|
||||
self.doc.tax_withholding_category,
|
||||
)
|
||||
|
||||
def _create_or_update_tds_row(self, tds_details):
|
||||
tax_account = tds_details.get("account_head")
|
||||
account_currency = get_account_currency(tax_account)
|
||||
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
|
||||
|
||||
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
|
||||
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
|
||||
|
||||
# Find existing TDS row for this account
|
||||
tax_row = None
|
||||
for row in self.doc.get("accounts"):
|
||||
if row.account == tax_account and row.get("is_tax_withholding_account"):
|
||||
tax_row = row
|
||||
break
|
||||
|
||||
if not tax_row:
|
||||
tax_row = self.doc.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_account,
|
||||
"account_currency": account_currency,
|
||||
"exchange_rate": exchange_rate,
|
||||
"cost_center": tds_details.get("cost_center"),
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"is_tax_withholding_account": 1,
|
||||
},
|
||||
)
|
||||
|
||||
# TDS/TCS is always credited (liability to government)
|
||||
tax_row.update(
|
||||
{
|
||||
"credit": tax_amount,
|
||||
"credit_in_account_currency": tax_amount_in_account_currency,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
self._cleanup_duplicate_tds_rows(tax_row)
|
||||
|
||||
def _cleanup_duplicate_tds_rows(self, current_tax_row):
|
||||
rows_to_remove = [
|
||||
row
|
||||
for row in self.doc.get("accounts")
|
||||
if row.get("is_tax_withholding_account") and row != current_tax_row
|
||||
]
|
||||
|
||||
for row in rows_to_remove:
|
||||
self.doc.remove(row)
|
||||
|
||||
def _recalculate_totals(self):
|
||||
self.doc.set_amounts_in_company_currency()
|
||||
self.doc.set_total_debit_credit()
|
||||
self.doc.set_against_account()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1672,8 +1809,8 @@ def get_exchange_rate(
|
||||
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -282,12 +283,19 @@
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -850,6 +850,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
)
|
||||
|
||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||
doc_updates = {}
|
||||
|
||||
for ref in references:
|
||||
if not ref.payment_request:
|
||||
@@ -875,7 +876,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
|
||||
# update status
|
||||
# determine status
|
||||
if new_outstanding_amount == payment_request["grand_total"]:
|
||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||
elif new_outstanding_amount == 0:
|
||||
@@ -883,12 +884,15 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
elif new_outstanding_amount > 0:
|
||||
status = "Partially Paid"
|
||||
|
||||
# update database
|
||||
frappe.db.set_value(
|
||||
"Payment Request",
|
||||
ref.payment_request,
|
||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||
)
|
||||
# prepare bulk update data
|
||||
doc_updates[ref.payment_request] = {
|
||||
"outstanding_amount": new_outstanding_amount,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
# bulk update all payment requests
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update("Payment Request", doc_updates)
|
||||
|
||||
|
||||
def get_dummy_message(doc):
|
||||
|
||||
@@ -126,8 +126,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
cur_frm.add_custom_button(
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
|
||||
@@ -1349,7 +1349,11 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
|
||||
if (
|
||||
flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
or item.is_fixed_asset
|
||||
or enable_discount_accounting
|
||||
):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
@@ -85,6 +85,9 @@ def get_party_details(inv):
|
||||
if inv.doctype == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party = inv.customer
|
||||
elif inv.doctype == "Journal Entry":
|
||||
party_type = inv.party_type
|
||||
party = inv.party
|
||||
else:
|
||||
party_type = "Supplier"
|
||||
party = inv.supplier
|
||||
@@ -155,7 +158,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
party_type, parties, inv, tax_details, posting_date, pan_no
|
||||
)
|
||||
|
||||
if party_type == "Supplier":
|
||||
if party_type == "Supplier" or inv.doctype == "Journal Entry":
|
||||
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
|
||||
else:
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
@@ -346,7 +349,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
|
||||
tax_amount = 0
|
||||
if inv.doctype == "Sales Invoice":
|
||||
tax_amount = 0
|
||||
else:
|
||||
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
|
||||
else:
|
||||
# if no TCS has been charged in FY,
|
||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||
@@ -718,7 +724,7 @@ def get_advance_adjusted_in_invoice(inv):
|
||||
|
||||
|
||||
def get_invoice_total_without_tcs(inv, tax_details):
|
||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||
|
||||
return inv.grand_total - tcs_tax_row_amount
|
||||
|
||||
@@ -848,6 +848,90 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||
|
||||
def test_tds_on_journal_entry_for_supplier(self):
|
||||
"""Test TDS deduction for Supplier in Debit Note"""
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
|
||||
jv = make_journal_entry_with_tax_withholding(
|
||||
party_type="Supplier",
|
||||
party="Test TDS Supplier",
|
||||
voucher_type="Debit Note",
|
||||
amount=50000,
|
||||
save=False,
|
||||
)
|
||||
jv.apply_tds = 1
|
||||
jv.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
jv.save()
|
||||
|
||||
# Again saving should not change tds amount
|
||||
jv.user_remark = "Test TDS on Journal Entry for Supplier"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
# TDS = 50000 * 10% = 5000
|
||||
self.assertEqual(len(jv.accounts), 3)
|
||||
|
||||
# Find TDS account row
|
||||
tds_row = None
|
||||
supplier_row = None
|
||||
for row in jv.accounts:
|
||||
if row.account == "TDS - _TC":
|
||||
tds_row = row
|
||||
elif row.party == "Test TDS Supplier":
|
||||
supplier_row = row
|
||||
|
||||
self.assertEqual(tds_row.credit, 5000)
|
||||
self.assertEqual(tds_row.debit, 0)
|
||||
|
||||
# Supplier amount should be reduced by TDS
|
||||
self.assertEqual(supplier_row.credit, 45000)
|
||||
jv.cancel()
|
||||
|
||||
def test_tcs_on_journal_entry_for_customer(self):
|
||||
"""Test TCS collection for Customer in Credit Note"""
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
|
||||
# Create Credit Note with amount exceeding threshold
|
||||
jv = make_journal_entry_with_tax_withholding(
|
||||
party_type="Customer",
|
||||
party="Test TCS Customer",
|
||||
voucher_type="Credit Note",
|
||||
amount=50000,
|
||||
save=False,
|
||||
)
|
||||
jv.apply_tds = 1
|
||||
jv.tax_withholding_category = "Cumulative Threshold TCS"
|
||||
jv.save()
|
||||
|
||||
# Again saving should not change tds amount
|
||||
jv.user_remark = "Test TCS on Journal Entry for Customer"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
# Assert TCS calculation (10% on amount above threshold of 30000)
|
||||
self.assertEqual(len(jv.accounts), 3)
|
||||
|
||||
# Find TCS account row
|
||||
tcs_row = None
|
||||
customer_row = None
|
||||
for row in jv.accounts:
|
||||
if row.account == "TCS - _TC":
|
||||
tcs_row = row
|
||||
elif row.party == "Test TCS Customer":
|
||||
customer_row = row
|
||||
|
||||
# TCS should be credited (liability to government)
|
||||
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
|
||||
self.assertEqual(tcs_row.debit, 0)
|
||||
|
||||
# Customer amount should be increased by TCS
|
||||
self.assertEqual(customer_row.debit, 52000)
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
|
||||
return pe
|
||||
|
||||
|
||||
def make_journal_entry_with_tax_withholding(
|
||||
party_type,
|
||||
party,
|
||||
voucher_type,
|
||||
amount,
|
||||
cost_center=None,
|
||||
posting_date=None,
|
||||
save=True,
|
||||
submit=False,
|
||||
):
|
||||
"""Helper function to create Journal Entry for tax withholding"""
|
||||
if not cost_center:
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = posting_date or today()
|
||||
jv.company = "_Test Company"
|
||||
jv.voucher_type = voucher_type
|
||||
jv.multi_currency = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
# Debit Note: Expense Dr, Supplier Cr
|
||||
expense_account = "Stock Received But Not Billed - _TC"
|
||||
party_account = "Creditors - _TC"
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": expense_account,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
else: # Customer
|
||||
# Credit Note: Customer Dr, Income Cr
|
||||
party_account = "Debtors - _TC"
|
||||
income_account = "Sales - _TC"
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": income_account,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if save or submit:
|
||||
jv.insert()
|
||||
|
||||
if submit:
|
||||
jv.submit()
|
||||
|
||||
return jv
|
||||
|
||||
|
||||
def create_records():
|
||||
# create a new suppliers
|
||||
for name in [
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
@@ -16,7 +15,7 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i
|
||||
get_group_by_and_display_fields,
|
||||
get_tax_accounts,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
tax_doctype="Purchase Taxes and Charges",
|
||||
)
|
||||
|
||||
scrubbed_tax_fields = {}
|
||||
|
||||
for tax in tax_columns:
|
||||
scrubbed_tax_fields.update(
|
||||
{
|
||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
||||
}
|
||||
)
|
||||
|
||||
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||
|
||||
data = []
|
||||
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
{
|
||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
||||
f"{tax}_rate": item_tax.get("tax_rate", 0),
|
||||
f"{tax}_amount": item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
|
||||
@@ -32,16 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
scrubbed_tax_fields = {}
|
||||
|
||||
for tax in tax_columns:
|
||||
scrubbed_tax_fields.update(
|
||||
{
|
||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
||||
}
|
||||
)
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
|
||||
@@ -102,8 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
{
|
||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
||||
f"{tax}_rate": item_tax.get("tax_rate", 0),
|
||||
f"{tax}_amount": item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
if item_tax.get("is_other_charges"):
|
||||
@@ -546,9 +536,10 @@ def get_tax_accounts(
|
||||
import json
|
||||
|
||||
item_row_map = {}
|
||||
tax_columns = []
|
||||
tax_columns = {}
|
||||
invoice_item_row = {}
|
||||
itemised_tax = {}
|
||||
scrubbed_description_map = {}
|
||||
add_deduct_tax = "charge_type"
|
||||
|
||||
tax_amount_precision = (
|
||||
@@ -605,9 +596,14 @@ def get_tax_accounts(
|
||||
tax_amount,
|
||||
) in tax_details:
|
||||
description = handle_html(description)
|
||||
if description not in tax_columns and tax_amount:
|
||||
scrubbed_description = scrubbed_description_map.get(description)
|
||||
if not scrubbed_description:
|
||||
scrubbed_description = frappe.scrub(description)
|
||||
scrubbed_description_map[description] = scrubbed_description
|
||||
|
||||
if scrubbed_description not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
tax_columns.append(description)
|
||||
tax_columns[scrubbed_description] = description
|
||||
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
@@ -641,7 +637,7 @@ def get_tax_accounts(
|
||||
else tax_value
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"tax_amount": tax_value,
|
||||
@@ -653,7 +649,7 @@ def get_tax_accounts(
|
||||
continue
|
||||
elif charge_type == "Actual" and tax_amount:
|
||||
for d in invoice_item_row.get(parent, []):
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
|
||||
{
|
||||
"tax_rate": "NA",
|
||||
"tax_amount": flt(
|
||||
@@ -662,12 +658,14 @@ def get_tax_accounts(
|
||||
}
|
||||
)
|
||||
|
||||
tax_columns.sort()
|
||||
for desc in tax_columns:
|
||||
tax_columns_list = list(tax_columns.keys())
|
||||
tax_columns_list.sort()
|
||||
for scrubbed_desc in tax_columns_list:
|
||||
desc = tax_columns[scrubbed_desc]
|
||||
columns.append(
|
||||
{
|
||||
"label": _(desc + " Rate"),
|
||||
"fieldname": frappe.scrub(desc + " Rate"),
|
||||
"fieldname": f"{scrubbed_desc}_rate",
|
||||
"fieldtype": "Float",
|
||||
"width": 100,
|
||||
}
|
||||
@@ -676,7 +674,7 @@ def get_tax_accounts(
|
||||
columns.append(
|
||||
{
|
||||
"label": _(desc + " Amount"),
|
||||
"fieldname": frappe.scrub(desc + " Amount"),
|
||||
"fieldname": f"{scrubbed_desc}_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
@@ -714,7 +712,7 @@ def get_tax_accounts(
|
||||
},
|
||||
]
|
||||
|
||||
return itemised_tax, tax_columns
|
||||
return itemised_tax, tax_columns_list
|
||||
|
||||
|
||||
def add_total_row(
|
||||
@@ -807,5 +805,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
|
||||
total_row["percent_gt"] += item["percent_gt"]
|
||||
|
||||
for tax in tax_columns:
|
||||
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
|
||||
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
|
||||
total_row.setdefault(f"{tax}_amount", 0.0)
|
||||
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])
|
||||
|
||||
@@ -537,6 +537,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.asset_doc = asset
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ class AssetCapitalization(StockController):
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.restore_consumed_asset_items()
|
||||
self.update_target_asset()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
@@ -607,8 +608,12 @@ class AssetCapitalization(StockController):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
||||
asset_doc.purchase_amount += total_target_asset_value
|
||||
if self.docstatus == 2:
|
||||
asset_doc.gross_purchase_amount -= total_target_asset_value
|
||||
asset_doc.purchase_amount -= total_target_asset_value
|
||||
else:
|
||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
||||
asset_doc.purchase_amount += total_target_asset_value
|
||||
asset_doc.set_status("Work In Progress")
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
@@ -86,6 +86,7 @@ status_map = {
|
||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import functions
|
||||
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
@@ -685,6 +686,22 @@ class calculate_taxes_and_totals:
|
||||
discount_amount = self.doc.discount_amount or 0
|
||||
grand_total = self.doc.grand_total
|
||||
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
doctype = frappe.qb.DocType(self.doc.doctype)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(functions.Sum(doctype.discount_amount).as_("total_return_discount"))
|
||||
.where(
|
||||
(doctype.return_against == self.doc.return_against)
|
||||
& (doctype.is_return == 1)
|
||||
& (doctype.docstatus == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
total_return_discount = abs(result[0].get("total_return_discount") or 0)
|
||||
discount_amount += total_return_discount
|
||||
|
||||
# validate that discount amount cannot exceed the total before discount
|
||||
if (
|
||||
(grand_total >= 0 and discount_amount > grand_total)
|
||||
|
||||
@@ -389,10 +389,12 @@ frappe.ui.form.on("BOM", {
|
||||
);
|
||||
|
||||
has_template_rm.forEach((d) => {
|
||||
let bom_qty = dialog.fields_dict.qty?.value || 1;
|
||||
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
item_code: d.item_code,
|
||||
variant_item_code: "",
|
||||
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
|
||||
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
|
||||
source_warehouse: d.source_warehouse,
|
||||
operation: d.operation,
|
||||
});
|
||||
|
||||
@@ -178,17 +178,12 @@ class JobCard(Document):
|
||||
|
||||
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
|
||||
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
|
||||
|
||||
msg = f"""
|
||||
Qty To Manufacture in the job card
|
||||
cannot be greater than Qty To Manufacture in the
|
||||
work order for the operation {bold(self.operation)}.
|
||||
<br><br><b>Solution: </b> Either you can reduce the
|
||||
Qty To Manufacture in the job card or set the
|
||||
'Overproduction Percentage For Work Order'
|
||||
in the {form_link}."""
|
||||
|
||||
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <br><br><b>Solution: </b> Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}."
|
||||
).format(bold(self.operation), form_link),
|
||||
title=_("Extra Job Card Quantity"),
|
||||
)
|
||||
|
||||
def set_sub_operations(self):
|
||||
if not self.sub_operations and self.operation:
|
||||
@@ -1064,14 +1059,16 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
if row.completed_qty < current_operation_qty:
|
||||
msg = f"""The completed quantity {bold(current_operation_qty)}
|
||||
of an operation {bold(self.operation)} cannot be greater
|
||||
than the completed quantity {bold(row.completed_qty)}
|
||||
of a previous operation
|
||||
{bold(row.operation)}.
|
||||
"""
|
||||
|
||||
frappe.throw(_(msg))
|
||||
frappe.throw(
|
||||
_(
|
||||
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
|
||||
).format(
|
||||
bold(current_operation_qty),
|
||||
bold(self.operation),
|
||||
bold(row.completed_qty),
|
||||
bold(row.operation),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.is_work_order_closed():
|
||||
|
||||
@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
|
||||
if (
|
||||
frm.doc.docstatus === 1 &&
|
||||
["Closed", "Completed"].includes(frm.doc.status) &&
|
||||
frm.doc.produced_qty > 0
|
||||
frm.doc.produced_qty > 0 &&
|
||||
frm.doc.produced_qty > frm.doc.disassembled_qty
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Disassemble Order"),
|
||||
@@ -406,7 +407,6 @@ frappe.ui.form.on("Work Order", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: "Disassemble",
|
||||
qty: data.qty,
|
||||
target_warehouse: data.target_warehouse,
|
||||
});
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
@@ -863,24 +863,6 @@ erpnext.work_order = {
|
||||
},
|
||||
];
|
||||
|
||||
if (purpose === "Disassemble") {
|
||||
fields.push({
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
fieldname: "target_warehouse",
|
||||
label: __("Target Warehouse"),
|
||||
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
frm.qty_prompt = frappe.prompt(
|
||||
fields,
|
||||
|
||||
@@ -1373,6 +1373,13 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
|
||||
|
||||
item_details = get_item_details(item, project)
|
||||
|
||||
if frappe.db.get_value("Item", item, "variant_of"):
|
||||
if variant_bom := frappe.db.get_value(
|
||||
"BOM",
|
||||
{"item": item, "is_default": 1, "docstatus": 1},
|
||||
):
|
||||
bom_no = variant_bom
|
||||
|
||||
wo_doc = frappe.new_doc("Work Order")
|
||||
wo_doc.production_item = item
|
||||
wo_doc.update(item_details)
|
||||
|
||||
@@ -1093,6 +1093,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
|
||||
cost_center(doc) {
|
||||
this.frm.doc.items.forEach((item) => {
|
||||
item.cost_center = doc.cost_center;
|
||||
});
|
||||
|
||||
this.frm.refresh_field("items");
|
||||
}
|
||||
|
||||
due_date(doc, cdt, cdn) {
|
||||
// due_date is to be changed, payment terms template and/or payment schedule must
|
||||
// be removed as due_date is automatically changed based on payment terms
|
||||
|
||||
@@ -1091,7 +1091,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
|
||||
"options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
|
||||
"print_hide": 1,
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
@@ -1404,7 +1404,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:20:47.724218",
|
||||
"modified": "2025-12-02 23:55:25.415443",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -126,7 +126,9 @@ class DeliveryNote(SellingController):
|
||||
shipping_address_name: DF.Link | None
|
||||
shipping_rule: DF.Link | None
|
||||
source: DF.Link | None
|
||||
status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"]
|
||||
status: DF.Literal[
|
||||
"", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
|
||||
]
|
||||
tax_category: DF.Link | None
|
||||
tax_id: DF.Data | None
|
||||
taxes: DF.Table[SalesTaxesandCharges]
|
||||
|
||||
@@ -12,8 +12,8 @@ frappe.listview_settings["Delivery Note"] = {
|
||||
"currency",
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
if (cint(doc.is_return) == 1) {
|
||||
return [__("Return"), "gray", "is_return,=,Yes"];
|
||||
if (cint(doc.is_return) == 1 && doc.status == "Return") {
|
||||
return [__("Return"), "gray", "is_return,=,1"];
|
||||
} else if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (doc.status === "Return Issued") {
|
||||
|
||||
@@ -2581,6 +2581,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.submit()
|
||||
self.assertEqual(dn.per_billed, 0)
|
||||
self.assertEqual(dn.status, "To Bill")
|
||||
|
||||
si = make_sales_invoice(dn.name)
|
||||
si.location = "Test Location"
|
||||
@@ -2595,6 +2596,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.load_from_db()
|
||||
self.assertEqual(dn.per_billed, 100)
|
||||
self.assertEqual(dn.per_returned, 100)
|
||||
self.assertEqual(returned.status, "Return")
|
||||
|
||||
def test_sales_return_for_product_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
@@ -224,7 +224,7 @@ frappe.ui.form.on("Item", {
|
||||
["is_stock_item", "has_serial_no", "has_batch_no", "has_variants"].forEach((fieldname) => {
|
||||
frm.set_df_property(fieldname, "read_only", stock_exists);
|
||||
});
|
||||
|
||||
frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0);
|
||||
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
|
||||
frm.set_query("item_group", () => {
|
||||
return {
|
||||
|
||||
@@ -243,8 +243,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "is_fixed_asset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Fixed Asset",
|
||||
"set_only_once": 1
|
||||
"label": "Is Fixed Asset"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
@@ -895,7 +894,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-08-08 14:58:48.674193",
|
||||
"modified": "2025-12-04 09:11:56.029567",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -155,6 +155,7 @@ class Item(Document):
|
||||
self.set_onload("stock_exists", self.stock_ledger_created())
|
||||
self.set_onload("asset_naming_series", get_asset_naming_series())
|
||||
self.set_onload("current_valuation_method", get_valuation_method(self.name))
|
||||
self.set_onload("asset_exists", self.has_submitted_assets())
|
||||
|
||||
def autoname(self):
|
||||
if frappe.db.get_default("item_naming_by") == "Naming Series":
|
||||
@@ -306,9 +307,8 @@ class Item(Document):
|
||||
if self.stock_ledger_created():
|
||||
frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created."))
|
||||
|
||||
if not self.is_fixed_asset:
|
||||
asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
|
||||
if asset:
|
||||
if not self.is_fixed_asset and not self.is_new():
|
||||
if self.has_submitted_assets():
|
||||
frappe.throw(
|
||||
_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')
|
||||
)
|
||||
@@ -525,6 +525,9 @@ class Item(Document):
|
||||
)
|
||||
return self._stock_ledger_created
|
||||
|
||||
def has_submitted_assets(self):
|
||||
return bool(frappe.db.exists("Asset", {"item_code": self.name, "docstatus": 1}))
|
||||
|
||||
def update_item_price(self):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
|
||||
@@ -743,7 +743,10 @@ class PickList(TransactionBase):
|
||||
pi_item.serial_no,
|
||||
(
|
||||
Case()
|
||||
.when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
|
||||
.when(
|
||||
(pi_item.picked_qty > 0) & (pi_item.docstatus == 1),
|
||||
pi_item.picked_qty - pi_item.delivered_qty,
|
||||
)
|
||||
.else_(pi_item.stock_qty)
|
||||
).as_("picked_qty"),
|
||||
)
|
||||
|
||||
@@ -645,6 +645,46 @@ class TestPickList(FrappeTestCase):
|
||||
if dn_item.item_code == "_Test Item 2":
|
||||
self.assertEqual(dn_item.qty, 2)
|
||||
|
||||
def test_picklist_reserved_qty_validation(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
test_stock_item = "_Test Stock Item"
|
||||
|
||||
# Ensure stock item exists
|
||||
if not frappe.db.exists("Item", test_stock_item):
|
||||
create_item(
|
||||
item_code=test_stock_item,
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
# Add initial stock qty
|
||||
make_stock_entry(item_code=test_stock_item, to_warehouse=warehouse, qty=15)
|
||||
|
||||
# Create SO for 10 qty
|
||||
sales_order_1 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)
|
||||
|
||||
# Create and Submit picklist for SO
|
||||
picklist_1 = create_pick_list(sales_order_1.name)
|
||||
picklist_1.submit()
|
||||
|
||||
# Create DN for 5 qty
|
||||
dn = create_delivery_note(picklist_1.name)
|
||||
dn.items[0].qty = 5
|
||||
dn.save()
|
||||
dn.submit()
|
||||
|
||||
# Verify partly delivered state
|
||||
picklist_1.reload()
|
||||
self.assertEqual(picklist_1.status, "Partly Delivered")
|
||||
|
||||
# Create another SO (10 qty)
|
||||
sales_order_2 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)
|
||||
|
||||
# Expected pick qty = 5
|
||||
picklist_2 = create_pick_list(sales_order_2.name)
|
||||
self.assertEqual(picklist_2.locations[0].qty, 5)
|
||||
|
||||
def test_picklist_with_multi_uom(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
|
||||
|
||||
@@ -12,7 +12,7 @@ frappe.listview_settings["Purchase Receipt"] = {
|
||||
],
|
||||
get_indicator: function (doc) {
|
||||
if (cint(doc.is_return) == 1 && doc.status == "Return") {
|
||||
return [__("Return"), "gray", "is_return,=,Yes"];
|
||||
return [__("Return"), "gray", "is_return,=,1"];
|
||||
} else if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (flt(doc.per_returned, 2) === 100) {
|
||||
|
||||
@@ -4448,6 +4448,87 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(srbnb_cost, 1000)
|
||||
|
||||
def test_lcv_for_repack_entry(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
create_landed_cost_voucher,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
for item in [
|
||||
"Potatoes Raw Material Item",
|
||||
"Fries Finished Goods Item",
|
||||
]:
|
||||
create_item(item)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Potatoes Raw Material Item",
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
qty=100,
|
||||
rate=50,
|
||||
)
|
||||
|
||||
wh1 = create_warehouse("WH A1", company=pr.company)
|
||||
wh2 = create_warehouse("WH A2", company=pr.company)
|
||||
|
||||
ste = make_stock_entry(
|
||||
purpose="Repack",
|
||||
source="_Test Warehouse - _TC",
|
||||
item_code="Potatoes Raw Material Item",
|
||||
qty=100,
|
||||
company=pr.company,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
ste.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "Fries Finished Goods Item",
|
||||
"qty": 50,
|
||||
"t_warehouse": wh1,
|
||||
},
|
||||
)
|
||||
|
||||
ste.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "Fries Finished Goods Item",
|
||||
"qty": 50,
|
||||
"t_warehouse": wh2,
|
||||
},
|
||||
)
|
||||
|
||||
ste.insert()
|
||||
ste.submit()
|
||||
ste.reload()
|
||||
|
||||
for row in ste.items:
|
||||
if row.t_warehouse:
|
||||
self.assertEqual(row.valuation_rate, 50)
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": ste.doctype, "voucher_no": ste.name, "actual_qty": (">", 0)},
|
||||
pluck="stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(sles, [2500.0, 2500.0])
|
||||
|
||||
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2000 * -1)
|
||||
|
||||
ste.reload()
|
||||
|
||||
for row in ste.items:
|
||||
if row.t_warehouse:
|
||||
self.assertEqual(row.valuation_rate, 30)
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": ste.doctype, "voucher_no": ste.name, "actual_qty": (">", 0)},
|
||||
pluck="stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(sles, [1500.0, 1500.0])
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -9,7 +9,7 @@ frappe.ui.form.on("Quality Inspection", {
|
||||
},
|
||||
|
||||
set_default_company(frm) {
|
||||
if (!frm.doc.company) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.company) {
|
||||
frm.set_value("company", frappe.defaults.get_default("company"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2532,6 +2532,9 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> dict[str, dict]:
|
||||
child_row = group_by_voucher[key]
|
||||
if row.serial_no:
|
||||
child_row["serial_nos"].append(row.serial_no)
|
||||
child_row["item_row"].qty = len(child_row["serial_nos"]) * (
|
||||
-1 if row.type_of_transaction == "Outward" else 1
|
||||
)
|
||||
|
||||
if row.batch_no:
|
||||
child_row["batch_nos"][row.batch_no] += row.qty
|
||||
|
||||
@@ -571,6 +571,14 @@ frappe.ui.form.on("Stock Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
set_rate_and_fg_qty: function (frm, cdt, cdn) {
|
||||
frm.events.set_basic_rate(frm, cdt, cdn);
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (item.is_finished_item) {
|
||||
frm.events.set_fg_completed_qty(frm);
|
||||
}
|
||||
},
|
||||
|
||||
get_warehouse_details: function (frm, cdt, cdn) {
|
||||
var child = locals[cdt][cdn];
|
||||
if (!child.bom_no) {
|
||||
@@ -833,7 +841,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
|
||||
frm.doc.items.forEach((item) => {
|
||||
if (item.is_finished_item) {
|
||||
fg_completed_qty += flt(item.qty);
|
||||
fg_completed_qty += flt(item.transfer_qty);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -859,15 +867,11 @@ frappe.ui.form.on("Stock Entry Detail", {
|
||||
},
|
||||
|
||||
qty(frm, cdt, cdn) {
|
||||
frm.events.set_basic_rate(frm, cdt, cdn);
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (item.is_finished_item) {
|
||||
frm.events.set_fg_completed_qty(frm);
|
||||
}
|
||||
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
conversion_factor(frm, cdt, cdn) {
|
||||
frm.events.set_basic_rate(frm, cdt, cdn);
|
||||
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
|
||||
},
|
||||
|
||||
s_warehouse(frm, cdt, cdn) {
|
||||
|
||||
@@ -243,7 +243,66 @@ class StockEntry(StockController):
|
||||
|
||||
self.validate_same_source_target_warehouse_during_material_transfer()
|
||||
|
||||
def set_serial_batch_for_disassembly(self):
|
||||
if self.purpose != "Disassemble":
|
||||
return
|
||||
|
||||
available_materials = get_available_materials(self.work_order, self)
|
||||
for row in self.items:
|
||||
warehouse = row.s_warehouse or row.t_warehouse
|
||||
materials = available_materials.get((row.item_code, warehouse))
|
||||
if not materials:
|
||||
continue
|
||||
|
||||
batches = defaultdict(float)
|
||||
serial_nos = []
|
||||
qty = row.transfer_qty
|
||||
for batch_no, batch_qty in materials.batch_details.items():
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
batch_qty = abs(batch_qty)
|
||||
if batch_qty <= qty:
|
||||
batches[batch_no] = batch_qty
|
||||
qty -= batch_qty
|
||||
else:
|
||||
batches[batch_no] = qty
|
||||
qty = 0
|
||||
|
||||
if materials.serial_nos:
|
||||
serial_nos = materials.serial_nos[: int(row.transfer_qty)]
|
||||
|
||||
if not serial_nos and not batches:
|
||||
continue
|
||||
|
||||
bundle_doc = SerialBatchCreation(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
"qty": row.transfer_qty,
|
||||
"type_of_transaction": "Inward" if row.t_warehouse else "Outward",
|
||||
"company": self.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches)
|
||||
|
||||
row.serial_and_batch_bundle = bundle_doc.name
|
||||
row.use_serial_batch_fields = 0
|
||||
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle_doc.name,
|
||||
"use_serial_batch_fields": 0,
|
||||
}
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.set_serial_batch_for_disassembly()
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_disassembled_order()
|
||||
@@ -1856,7 +1915,13 @@ class StockEntry(StockController):
|
||||
|
||||
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
|
||||
|
||||
items_dict = get_bom_items_as_dict(self.bom_no, self.company, disassemble_qty)
|
||||
items_dict = get_bom_items_as_dict(
|
||||
self.bom_no,
|
||||
self.company,
|
||||
disassemble_qty,
|
||||
fetch_exploded=self.use_multi_level_bom,
|
||||
fetch_qty_in_stock_uom=False,
|
||||
)
|
||||
|
||||
for row in items:
|
||||
child_row = self.append("items", {})
|
||||
@@ -1874,7 +1939,7 @@ class StockEntry(StockController):
|
||||
child_row.qty = disassemble_qty
|
||||
|
||||
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
|
||||
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
|
||||
child_row.t_warehouse = row.s_warehouse
|
||||
child_row.is_finished_item = 0 if row.is_finished_item else 1
|
||||
|
||||
def get_items_from_manufacture_entry(self):
|
||||
@@ -1893,6 +1958,8 @@ class StockEntry(StockController):
|
||||
"`tabStock Entry Detail`.`is_finished_item`",
|
||||
"`tabStock Entry Detail`.`batch_no`",
|
||||
"`tabStock Entry Detail`.`serial_no`",
|
||||
"`tabStock Entry Detail`.`s_warehouse`",
|
||||
"`tabStock Entry Detail`.`t_warehouse`",
|
||||
"`tabStock Entry Detail`.`use_serial_batch_fields`",
|
||||
],
|
||||
filters=[
|
||||
@@ -3259,8 +3326,8 @@ def get_items_from_subcontract_order(source_name, target_doc=None):
|
||||
return target_doc
|
||||
|
||||
|
||||
def get_available_materials(work_order) -> dict:
|
||||
data = get_stock_entry_data(work_order)
|
||||
def get_available_materials(work_order, stock_entry_doc=None) -> dict:
|
||||
data = get_stock_entry_data(work_order, stock_entry_doc=stock_entry_doc)
|
||||
|
||||
available_materials = {}
|
||||
for row in data:
|
||||
@@ -3268,6 +3335,9 @@ def get_available_materials(work_order) -> dict:
|
||||
if row.purpose != "Material Transfer for Manufacture":
|
||||
key = (row.item_code, row.s_warehouse)
|
||||
|
||||
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
|
||||
key = (row.item_code, row.s_warehouse or row.warehouse)
|
||||
|
||||
if key not in available_materials:
|
||||
available_materials.setdefault(
|
||||
key,
|
||||
@@ -3278,7 +3348,9 @@ def get_available_materials(work_order) -> dict:
|
||||
|
||||
item_data = available_materials[key]
|
||||
|
||||
if row.purpose == "Material Transfer for Manufacture":
|
||||
if row.purpose == "Material Transfer for Manufacture" or (
|
||||
stock_entry_doc and stock_entry_doc.purpose == "Disassemble" and row.purpose == "Manufacture"
|
||||
):
|
||||
item_data.qty += row.qty
|
||||
if row.batch_no:
|
||||
item_data.batch_details[row.batch_no] += row.qty
|
||||
@@ -3318,7 +3390,7 @@ def get_available_materials(work_order) -> dict:
|
||||
return available_materials
|
||||
|
||||
|
||||
def get_stock_entry_data(work_order):
|
||||
def get_stock_entry_data(work_order, stock_entry_doc=None):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_voucher_wise_serial_batch_from_bundle,
|
||||
)
|
||||
@@ -3350,19 +3422,35 @@ def get_stock_entry_data(work_order):
|
||||
(stock_entry.name == stock_entry_detail.parent)
|
||||
& (stock_entry.work_order == work_order)
|
||||
& (stock_entry.docstatus == 1)
|
||||
& (stock_entry_detail.s_warehouse.isnotnull())
|
||||
& (
|
||||
stock_entry.purpose.isin(
|
||||
[
|
||||
"Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Material Transfer for Manufacture",
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
|
||||
).run(as_dict=1)
|
||||
)
|
||||
|
||||
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
|
||||
data = data.where(
|
||||
stock_entry.purpose.isin(
|
||||
[
|
||||
"Disassemble",
|
||||
"Manufacture",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
data = data.where(stock_entry.name != stock_entry_doc.name)
|
||||
else:
|
||||
data = data.where(
|
||||
stock_entry.purpose.isin(
|
||||
[
|
||||
"Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Material Transfer for Manufacture",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
data = data.where(stock_entry_detail.s_warehouse.isnotnull())
|
||||
|
||||
data = data.run(as_dict=1)
|
||||
|
||||
if not data:
|
||||
return []
|
||||
@@ -3375,6 +3463,9 @@ def get_stock_entry_data(work_order):
|
||||
if row.purpose != "Material Transfer for Manufacture":
|
||||
key = (row.item_code, row.s_warehouse, row.name)
|
||||
|
||||
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
|
||||
key = (row.item_code, row.s_warehouse or row.warehouse, row.name)
|
||||
|
||||
if bundle_data.get(key):
|
||||
row.update(bundle_data.get(key))
|
||||
|
||||
|
||||
@@ -647,10 +647,32 @@ class update_entries_after:
|
||||
|
||||
if sle.dependant_sle_voucher_detail_no:
|
||||
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
|
||||
if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no):
|
||||
# for repack entries, we need to repost both source and target warehouses
|
||||
self.update_distinct_item_warehouses_for_repack(sle)
|
||||
|
||||
if self.exceptions:
|
||||
self.raise_exceptions()
|
||||
|
||||
def update_distinct_item_warehouses_for_repack(self, sle):
|
||||
sles = (
|
||||
frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": sle.voucher_no,
|
||||
"actual_qty": (">", 0),
|
||||
"is_cancelled": 0,
|
||||
"voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no),
|
||||
},
|
||||
fields=["*"],
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
for dependant_sle in sles:
|
||||
self.update_distinct_item_warehouses(dependant_sle)
|
||||
|
||||
def has_stock_reco_with_serial_batch(self, sle):
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
@@ -696,6 +718,13 @@ class update_entries_after:
|
||||
{"item_code": self.item_code, "warehouse": self.args.warehouse}
|
||||
)
|
||||
|
||||
key = (self.item_code, self.args.warehouse)
|
||||
if key in self.distinct_item_warehouses and self.distinct_item_warehouses[key].get(
|
||||
"transfer_entry_to_repost"
|
||||
):
|
||||
# only repost stock entries
|
||||
args["filter_voucher_type"] = "Stock Entry"
|
||||
|
||||
return list(self.get_sle_after_datetime(args))
|
||||
|
||||
def get_dependent_entries_to_fix(self, entries_to_fix, sle):
|
||||
@@ -729,8 +758,10 @@ class update_entries_after:
|
||||
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
|
||||
dependant_sle.voucher_no
|
||||
elif (
|
||||
dependant_sle.actual_qty > 0
|
||||
and dependant_sle.voucher_type == "Stock Entry"
|
||||
and is_transfer_stock_entry(dependant_sle.voucher_no)
|
||||
):
|
||||
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
|
||||
return
|
||||
@@ -1156,7 +1187,11 @@ class update_entries_after:
|
||||
|
||||
def get_dynamic_incoming_outgoing_rate(self, sle):
|
||||
# Get updated incoming/outgoing rate from transaction
|
||||
if sle.recalculate_rate or self.has_landed_cost_based_on_pi(sle):
|
||||
if (
|
||||
sle.recalculate_rate
|
||||
or self.has_landed_cost_based_on_pi(sle)
|
||||
or (sle.voucher_type == "Stock Entry" and sle.actual_qty > 0 and is_repack_entry(sle.voucher_no))
|
||||
):
|
||||
rate = self.get_incoming_outgoing_rate_from_transaction(sle)
|
||||
|
||||
if flt(sle.actual_qty) >= 0:
|
||||
@@ -1813,6 +1848,9 @@ def get_stock_ledger_entries(
|
||||
if operator in (">", "<=") and previous_sle.get("name"):
|
||||
conditions += " and name!=%(name)s"
|
||||
|
||||
if previous_sle.get("filter_voucher_type"):
|
||||
conditions += " and voucher_type = %(filter_voucher_type)s"
|
||||
|
||||
if extra_cond:
|
||||
conditions += f"{extra_cond}"
|
||||
|
||||
@@ -1908,8 +1946,7 @@ def get_valuation_rate(
|
||||
& (table.warehouse == warehouse)
|
||||
& (table.batch_no == batch_no)
|
||||
& (table.is_cancelled == 0)
|
||||
& (table.voucher_no != voucher_no)
|
||||
& (table.voucher_type != voucher_type)
|
||||
& ((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2454,3 +2491,8 @@ def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
|
||||
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
|
||||
|
||||
return incoming_rate
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def is_repack_entry(stock_entry_id):
|
||||
return frappe.get_cached_value("Stock Entry", stock_entry_id, "purpose") == "Repack"
|
||||
|
||||
@@ -16,9 +16,30 @@ def transaction_processing(data, from_doctype, to_doctype):
|
||||
else:
|
||||
deserialized_data = data
|
||||
|
||||
skipped_records = [d for d in deserialized_data if d.get("status") in ("On Hold", "Closed")]
|
||||
|
||||
deserialized_data = [d for d in deserialized_data if d.get("status") not in ("On Hold", "Closed")]
|
||||
|
||||
length_of_data = len(deserialized_data)
|
||||
|
||||
frappe.msgprint(_("Started a background job to create {1} {0}").format(to_doctype, length_of_data))
|
||||
skipped_msg = ""
|
||||
|
||||
if skipped_records:
|
||||
skipped_msg = _("{0} creation for the following records will be skipped.").format(to_doctype)
|
||||
|
||||
skipped_msg += (
|
||||
"<br><br><ul>"
|
||||
+ "".join(_("<li>{}</li>").format(frappe.bold(row.get("name"))) for row in skipped_records)
|
||||
+ "</ul>"
|
||||
)
|
||||
|
||||
if not length_of_data:
|
||||
frappe.msgprint(skipped_msg)
|
||||
return
|
||||
|
||||
frappe.msgprint(
|
||||
_("Started a background job to create {1} {0}. {2}").format(to_doctype, length_of_data, skipped_msg)
|
||||
)
|
||||
frappe.enqueue(
|
||||
job,
|
||||
deserialized_data=deserialized_data,
|
||||
|
||||
Reference in New Issue
Block a user