mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-04 12:49:10 +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,
|
get_depr_schedule,
|
||||||
)
|
)
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
|
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||||
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def apply_tax_withholding(self):
|
def apply_tax_withholding(self):
|
||||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
JournalEntryTaxWithholding(self).apply()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def update_asset_value(self):
|
def update_asset_value(self):
|
||||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
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."))
|
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()
|
@frappe.whitelist()
|
||||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||||
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
|
|||||||
credit=None,
|
credit=None,
|
||||||
exchange_rate=None,
|
exchange_rate=None,
|
||||||
):
|
):
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
|
||||||
|
|
||||||
account_details = frappe.get_cached_value(
|
account_details = frappe.get_cached_value(
|
||||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
"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
|
# The date used to retreive the exchange rate here is the date passed
|
||||||
# in as an argument to this function.
|
# in as an argument to this function.
|
||||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and 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)
|
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||||
else:
|
else:
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"reference_detail_no",
|
"reference_detail_no",
|
||||||
"advance_voucher_type",
|
"advance_voucher_type",
|
||||||
"advance_voucher_no",
|
"advance_voucher_no",
|
||||||
|
"is_tax_withholding_account",
|
||||||
"col_break3",
|
"col_break3",
|
||||||
"is_advance",
|
"is_advance",
|
||||||
"user_remark",
|
"user_remark",
|
||||||
@@ -282,12 +283,19 @@
|
|||||||
"options": "advance_voucher_type",
|
"options": "advance_voucher_type",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_tax_withholding_account",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Tax Withholding Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-27 13:48:32.805100",
|
"modified": "2025-11-27 12:23:33.157655",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
|
|||||||
debit_in_account_currency: DF.Currency
|
debit_in_account_currency: DF.Currency
|
||||||
exchange_rate: DF.Float
|
exchange_rate: DF.Float
|
||||||
is_advance: DF.Literal["No", "Yes"]
|
is_advance: DF.Literal["No", "Yes"]
|
||||||
|
is_tax_withholding_account: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: 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}
|
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||||
|
doc_updates = {}
|
||||||
|
|
||||||
for ref in references:
|
for ref in references:
|
||||||
if not ref.payment_request:
|
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"),
|
title=_("Invalid Allocated Amount"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# update status
|
# determine status
|
||||||
if new_outstanding_amount == payment_request["grand_total"]:
|
if new_outstanding_amount == payment_request["grand_total"]:
|
||||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||||
elif new_outstanding_amount == 0:
|
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:
|
elif new_outstanding_amount > 0:
|
||||||
status = "Partially Paid"
|
status = "Partially Paid"
|
||||||
|
|
||||||
# update database
|
# prepare bulk update data
|
||||||
frappe.db.set_value(
|
doc_updates[ref.payment_request] = {
|
||||||
"Payment Request",
|
"outstanding_amount": new_outstanding_amount,
|
||||||
ref.payment_request,
|
"status": status,
|
||||||
{"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):
|
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) {
|
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||||
cur_frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Payment Request"),
|
__("Payment Request"),
|
||||||
function () {
|
function () {
|
||||||
me.make_payment_request();
|
me.make_payment_request();
|
||||||
|
|||||||
@@ -1349,7 +1349,11 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for item in self.get("items"):
|
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
|
# Do not book income for transfer within same company
|
||||||
if self.is_internal_transfer():
|
if self.is_internal_transfer():
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ def get_party_details(inv):
|
|||||||
if inv.doctype == "Sales Invoice":
|
if inv.doctype == "Sales Invoice":
|
||||||
party_type = "Customer"
|
party_type = "Customer"
|
||||||
party = inv.customer
|
party = inv.customer
|
||||||
|
elif inv.doctype == "Journal Entry":
|
||||||
|
party_type = inv.party_type
|
||||||
|
party = inv.party
|
||||||
else:
|
else:
|
||||||
party_type = "Supplier"
|
party_type = "Supplier"
|
||||||
party = inv.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
|
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)
|
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
|
||||||
else:
|
else:
|
||||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
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":
|
elif party_type == "Customer":
|
||||||
if tax_deducted:
|
if tax_deducted:
|
||||||
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
|
# 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:
|
else:
|
||||||
# if no TCS has been charged in FY,
|
# if no TCS has been charged in FY,
|
||||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
# 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):
|
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
|
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
|
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].tax_amount, 6000)
|
||||||
self.assertEqual(payment.taxes[0].allocated_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():
|
def cancel_invoices():
|
||||||
purchase_invoices = frappe.get_all(
|
purchase_invoices = frappe.get_all(
|
||||||
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
|
|||||||
return pe
|
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():
|
def create_records():
|
||||||
# create a new suppliers
|
# create a new suppliers
|
||||||
for name in [
|
for name in [
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
from pypika import Order
|
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
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_group_by_and_display_fields,
|
||||||
get_tax_accounts,
|
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):
|
def execute(filters=None):
|
||||||
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
tax_doctype="Purchase Taxes and Charges",
|
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)
|
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
|||||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||||
row.update(
|
row.update(
|
||||||
{
|
{
|
||||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
f"{tax}_rate": item_tax.get("tax_rate", 0),
|
||||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
f"{tax}_amount": item_tax.get("tax_amount", 0),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
total_tax += flt(item_tax.get("tax_amount"))
|
total_tax += flt(item_tax.get("tax_amount"))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder import functions as fn
|
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.nestedset import get_descendants_of
|
||||||
from frappe.utils.xlsxutils import handle_html
|
from frappe.utils.xlsxutils import handle_html
|
||||||
|
|
||||||
@@ -32,16 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
|||||||
if item_list:
|
if item_list:
|
||||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
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))
|
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)
|
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, {})
|
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||||
row.update(
|
row.update(
|
||||||
{
|
{
|
||||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
f"{tax}_rate": item_tax.get("tax_rate", 0),
|
||||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
f"{tax}_amount": item_tax.get("tax_amount", 0),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if item_tax.get("is_other_charges"):
|
if item_tax.get("is_other_charges"):
|
||||||
@@ -546,9 +536,10 @@ def get_tax_accounts(
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
item_row_map = {}
|
item_row_map = {}
|
||||||
tax_columns = []
|
tax_columns = {}
|
||||||
invoice_item_row = {}
|
invoice_item_row = {}
|
||||||
itemised_tax = {}
|
itemised_tax = {}
|
||||||
|
scrubbed_description_map = {}
|
||||||
add_deduct_tax = "charge_type"
|
add_deduct_tax = "charge_type"
|
||||||
|
|
||||||
tax_amount_precision = (
|
tax_amount_precision = (
|
||||||
@@ -605,9 +596,14 @@ def get_tax_accounts(
|
|||||||
tax_amount,
|
tax_amount,
|
||||||
) in tax_details:
|
) in tax_details:
|
||||||
description = handle_html(description)
|
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
|
# 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:
|
if item_wise_tax_detail:
|
||||||
try:
|
try:
|
||||||
@@ -641,7 +637,7 @@ def get_tax_accounts(
|
|||||||
else tax_value
|
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_rate": tax_rate,
|
||||||
"tax_amount": tax_value,
|
"tax_amount": tax_value,
|
||||||
@@ -653,7 +649,7 @@ def get_tax_accounts(
|
|||||||
continue
|
continue
|
||||||
elif charge_type == "Actual" and tax_amount:
|
elif charge_type == "Actual" and tax_amount:
|
||||||
for d in invoice_item_row.get(parent, []):
|
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_rate": "NA",
|
||||||
"tax_amount": flt(
|
"tax_amount": flt(
|
||||||
@@ -662,12 +658,14 @@ def get_tax_accounts(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
tax_columns.sort()
|
tax_columns_list = list(tax_columns.keys())
|
||||||
for desc in tax_columns:
|
tax_columns_list.sort()
|
||||||
|
for scrubbed_desc in tax_columns_list:
|
||||||
|
desc = tax_columns[scrubbed_desc]
|
||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(desc + " Rate"),
|
"label": _(desc + " Rate"),
|
||||||
"fieldname": frappe.scrub(desc + " Rate"),
|
"fieldname": f"{scrubbed_desc}_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
}
|
}
|
||||||
@@ -676,7 +674,7 @@ def get_tax_accounts(
|
|||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
"label": _(desc + " Amount"),
|
"label": _(desc + " Amount"),
|
||||||
"fieldname": frappe.scrub(desc + " Amount"),
|
"fieldname": f"{scrubbed_desc}_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"width": 100,
|
"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(
|
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"]
|
total_row["percent_gt"] += item["percent_gt"]
|
||||||
|
|
||||||
for tax in tax_columns:
|
for tax in tax_columns:
|
||||||
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
|
total_row.setdefault(f"{tax}_amount", 0.0)
|
||||||
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
|
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:
|
for repair in asset_repairs:
|
||||||
if repair.increase_in_asset_life:
|
if repair.increase_in_asset_life:
|
||||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||||
|
asset_repair.asset_doc = asset
|
||||||
asset_repair.modify_depreciation_schedule()
|
asset_repair.modify_depreciation_schedule()
|
||||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
|
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class AssetCapitalization(StockController):
|
|||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
self.restore_consumed_asset_items()
|
self.restore_consumed_asset_items()
|
||||||
|
self.update_target_asset()
|
||||||
|
|
||||||
def set_title(self):
|
def set_title(self):
|
||||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
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"))
|
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||||
|
|
||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
if self.docstatus == 2:
|
||||||
asset_doc.purchase_amount += total_target_asset_value
|
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.set_status("Work In Progress")
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.save()
|
asset_doc.save()
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ status_map = {
|
|||||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||||
["Completed", "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 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"],
|
["Cancelled", "eval:self.docstatus==2"],
|
||||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.model.document import Document
|
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 import cint, flt, round_based_on_smallest_currency_fraction
|
||||||
from frappe.utils.deprecations import deprecated
|
from frappe.utils.deprecations import deprecated
|
||||||
|
|
||||||
@@ -685,6 +686,22 @@ class calculate_taxes_and_totals:
|
|||||||
discount_amount = self.doc.discount_amount or 0
|
discount_amount = self.doc.discount_amount or 0
|
||||||
grand_total = self.doc.grand_total
|
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
|
# validate that discount amount cannot exceed the total before discount
|
||||||
if (
|
if (
|
||||||
(grand_total >= 0 and discount_amount > grand_total)
|
(grand_total >= 0 and discount_amount > grand_total)
|
||||||
|
|||||||
@@ -389,10 +389,12 @@ frappe.ui.form.on("BOM", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
has_template_rm.forEach((d) => {
|
has_template_rm.forEach((d) => {
|
||||||
|
let bom_qty = dialog.fields_dict.qty?.value || 1;
|
||||||
|
|
||||||
dialog.fields_dict.items.df.data.push({
|
dialog.fields_dict.items.df.data.push({
|
||||||
item_code: d.item_code,
|
item_code: d.item_code,
|
||||||
variant_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,
|
source_warehouse: d.source_warehouse,
|
||||||
operation: d.operation,
|
operation: d.operation,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -178,17 +178,12 @@ class JobCard(Document):
|
|||||||
|
|
||||||
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
|
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
|
||||||
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
|
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
|
||||||
|
frappe.throw(
|
||||||
msg = f"""
|
_(
|
||||||
Qty To Manufacture in the job card
|
"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}."
|
||||||
cannot be greater than Qty To Manufacture in the
|
).format(bold(self.operation), form_link),
|
||||||
work order for the operation {bold(self.operation)}.
|
title=_("Extra Job Card Quantity"),
|
||||||
<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"))
|
|
||||||
|
|
||||||
def set_sub_operations(self):
|
def set_sub_operations(self):
|
||||||
if not self.sub_operations and self.operation:
|
if not self.sub_operations and self.operation:
|
||||||
@@ -1064,14 +1059,16 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if row.completed_qty < current_operation_qty:
|
if row.completed_qty < current_operation_qty:
|
||||||
msg = f"""The completed quantity {bold(current_operation_qty)}
|
frappe.throw(
|
||||||
of an operation {bold(self.operation)} cannot be greater
|
_(
|
||||||
than the completed quantity {bold(row.completed_qty)}
|
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
|
||||||
of a previous operation
|
).format(
|
||||||
{bold(row.operation)}.
|
bold(current_operation_qty),
|
||||||
"""
|
bold(self.operation),
|
||||||
|
bold(row.completed_qty),
|
||||||
frappe.throw(_(msg))
|
bold(row.operation),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_work_order(self):
|
def validate_work_order(self):
|
||||||
if self.is_work_order_closed():
|
if self.is_work_order_closed():
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
|
|||||||
if (
|
if (
|
||||||
frm.doc.docstatus === 1 &&
|
frm.doc.docstatus === 1 &&
|
||||||
["Closed", "Completed"].includes(frm.doc.status) &&
|
["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(
|
frm.add_custom_button(
|
||||||
__("Disassemble Order"),
|
__("Disassemble Order"),
|
||||||
@@ -406,7 +407,6 @@ frappe.ui.form.on("Work Order", {
|
|||||||
work_order_id: frm.doc.name,
|
work_order_id: frm.doc.name,
|
||||||
purpose: "Disassemble",
|
purpose: "Disassemble",
|
||||||
qty: data.qty,
|
qty: data.qty,
|
||||||
target_warehouse: data.target_warehouse,
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((stock_entry) => {
|
.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) => {
|
return new Promise((resolve, reject) => {
|
||||||
frm.qty_prompt = frappe.prompt(
|
frm.qty_prompt = frappe.prompt(
|
||||||
fields,
|
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)
|
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 = frappe.new_doc("Work Order")
|
||||||
wo_doc.production_item = item
|
wo_doc.production_item = item
|
||||||
wo_doc.update(item_details)
|
wo_doc.update(item_details)
|
||||||
|
|||||||
@@ -1093,6 +1093,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
this.frm.refresh_field("payment_schedule");
|
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(doc, cdt, cdn) {
|
||||||
// due_date is to be changed, payment terms template and/or payment schedule must
|
// 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
|
// be removed as due_date is automatically changed based on payment terms
|
||||||
|
|||||||
@@ -1091,7 +1091,7 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "status",
|
"oldfieldname": "status",
|
||||||
"oldfieldtype": "Select",
|
"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_hide": 1,
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
@@ -1404,7 +1404,7 @@
|
|||||||
"idx": 146,
|
"idx": 146,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-04 19:20:47.724218",
|
"modified": "2025-12-02 23:55:25.415443",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Delivery Note",
|
"name": "Delivery Note",
|
||||||
|
|||||||
@@ -126,7 +126,9 @@ class DeliveryNote(SellingController):
|
|||||||
shipping_address_name: DF.Link | None
|
shipping_address_name: DF.Link | None
|
||||||
shipping_rule: DF.Link | None
|
shipping_rule: DF.Link | None
|
||||||
source: 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_category: DF.Link | None
|
||||||
tax_id: DF.Data | None
|
tax_id: DF.Data | None
|
||||||
taxes: DF.Table[SalesTaxesandCharges]
|
taxes: DF.Table[SalesTaxesandCharges]
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ frappe.listview_settings["Delivery Note"] = {
|
|||||||
"currency",
|
"currency",
|
||||||
],
|
],
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (cint(doc.is_return) == 1) {
|
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") {
|
} else if (doc.status === "Closed") {
|
||||||
return [__("Closed"), "green", "status,=,Closed"];
|
return [__("Closed"), "green", "status,=,Closed"];
|
||||||
} else if (doc.status === "Return Issued") {
|
} else if (doc.status === "Return Issued") {
|
||||||
|
|||||||
@@ -2581,6 +2581,7 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
dn = make_delivery_note(so.name)
|
dn = make_delivery_note(so.name)
|
||||||
dn.submit()
|
dn.submit()
|
||||||
self.assertEqual(dn.per_billed, 0)
|
self.assertEqual(dn.per_billed, 0)
|
||||||
|
self.assertEqual(dn.status, "To Bill")
|
||||||
|
|
||||||
si = make_sales_invoice(dn.name)
|
si = make_sales_invoice(dn.name)
|
||||||
si.location = "Test Location"
|
si.location = "Test Location"
|
||||||
@@ -2595,6 +2596,7 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
dn.load_from_db()
|
dn.load_from_db()
|
||||||
self.assertEqual(dn.per_billed, 100)
|
self.assertEqual(dn.per_billed, 100)
|
||||||
self.assertEqual(dn.per_returned, 100)
|
self.assertEqual(dn.per_returned, 100)
|
||||||
|
self.assertEqual(returned.status, "Return")
|
||||||
|
|
||||||
def test_sales_return_for_product_bundle(self):
|
def test_sales_return_for_product_bundle(self):
|
||||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
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) => {
|
["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(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.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
|
||||||
frm.set_query("item_group", () => {
|
frm.set_query("item_group", () => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -243,8 +243,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "is_fixed_asset",
|
"fieldname": "is_fixed_asset",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Fixed Asset",
|
"label": "Is Fixed Asset"
|
||||||
"set_only_once": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_in_quick_entry": 1,
|
"allow_in_quick_entry": 1,
|
||||||
@@ -895,7 +894,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2025-08-08 14:58:48.674193",
|
"modified": "2025-12-04 09:11:56.029567",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ class Item(Document):
|
|||||||
self.set_onload("stock_exists", self.stock_ledger_created())
|
self.set_onload("stock_exists", self.stock_ledger_created())
|
||||||
self.set_onload("asset_naming_series", get_asset_naming_series())
|
self.set_onload("asset_naming_series", get_asset_naming_series())
|
||||||
self.set_onload("current_valuation_method", get_valuation_method(self.name))
|
self.set_onload("current_valuation_method", get_valuation_method(self.name))
|
||||||
|
self.set_onload("asset_exists", self.has_submitted_assets())
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
if frappe.db.get_default("item_naming_by") == "Naming Series":
|
if frappe.db.get_default("item_naming_by") == "Naming Series":
|
||||||
@@ -306,9 +307,8 @@ class Item(Document):
|
|||||||
if self.stock_ledger_created():
|
if self.stock_ledger_created():
|
||||||
frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created."))
|
frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created."))
|
||||||
|
|
||||||
if not self.is_fixed_asset:
|
if not self.is_fixed_asset and not self.is_new():
|
||||||
asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
|
if self.has_submitted_assets():
|
||||||
if asset:
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')
|
_('"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
|
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):
|
def update_item_price(self):
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -743,7 +743,10 @@ class PickList(TransactionBase):
|
|||||||
pi_item.serial_no,
|
pi_item.serial_no,
|
||||||
(
|
(
|
||||||
Case()
|
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)
|
.else_(pi_item.stock_qty)
|
||||||
).as_("picked_qty"),
|
).as_("picked_qty"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -645,6 +645,46 @@ class TestPickList(FrappeTestCase):
|
|||||||
if dn_item.item_code == "_Test Item 2":
|
if dn_item.item_code == "_Test Item 2":
|
||||||
self.assertEqual(dn_item.qty, 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):
|
def test_picklist_with_multi_uom(self):
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name
|
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) {
|
get_indicator: function (doc) {
|
||||||
if (cint(doc.is_return) == 1 && doc.status == "Return") {
|
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") {
|
} else if (doc.status === "Closed") {
|
||||||
return [__("Closed"), "green", "status,=,Closed"];
|
return [__("Closed"), "green", "status,=,Closed"];
|
||||||
} else if (flt(doc.per_returned, 2) === 100) {
|
} else if (flt(doc.per_returned, 2) === 100) {
|
||||||
|
|||||||
@@ -4448,6 +4448,87 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(srbnb_cost, 1000)
|
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():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
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) {
|
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"));
|
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]
|
child_row = group_by_voucher[key]
|
||||||
if row.serial_no:
|
if row.serial_no:
|
||||||
child_row["serial_nos"].append(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:
|
if row.batch_no:
|
||||||
child_row["batch_nos"][row.batch_no] += row.qty
|
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) {
|
get_warehouse_details: function (frm, cdt, cdn) {
|
||||||
var child = locals[cdt][cdn];
|
var child = locals[cdt][cdn];
|
||||||
if (!child.bom_no) {
|
if (!child.bom_no) {
|
||||||
@@ -833,7 +841,7 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
|
|
||||||
frm.doc.items.forEach((item) => {
|
frm.doc.items.forEach((item) => {
|
||||||
if (item.is_finished_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) {
|
qty(frm, cdt, cdn) {
|
||||||
frm.events.set_basic_rate(frm, cdt, cdn);
|
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
|
||||||
let item = frappe.get_doc(cdt, cdn);
|
|
||||||
if (item.is_finished_item) {
|
|
||||||
frm.events.set_fg_completed_qty(frm);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
conversion_factor(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) {
|
s_warehouse(frm, cdt, cdn) {
|
||||||
|
|||||||
@@ -243,7 +243,66 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
self.validate_same_source_target_warehouse_during_material_transfer()
|
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):
|
def on_submit(self):
|
||||||
|
self.set_serial_batch_for_disassembly()
|
||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
self.make_bundle_using_old_serial_batch_fields()
|
self.make_bundle_using_old_serial_batch_fields()
|
||||||
self.update_disassembled_order()
|
self.update_disassembled_order()
|
||||||
@@ -1856,7 +1915,13 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
|
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:
|
for row in items:
|
||||||
child_row = self.append("items", {})
|
child_row = self.append("items", {})
|
||||||
@@ -1874,7 +1939,7 @@ class StockEntry(StockController):
|
|||||||
child_row.qty = disassemble_qty
|
child_row.qty = disassemble_qty
|
||||||
|
|
||||||
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
|
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
|
child_row.is_finished_item = 0 if row.is_finished_item else 1
|
||||||
|
|
||||||
def get_items_from_manufacture_entry(self):
|
def get_items_from_manufacture_entry(self):
|
||||||
@@ -1893,6 +1958,8 @@ class StockEntry(StockController):
|
|||||||
"`tabStock Entry Detail`.`is_finished_item`",
|
"`tabStock Entry Detail`.`is_finished_item`",
|
||||||
"`tabStock Entry Detail`.`batch_no`",
|
"`tabStock Entry Detail`.`batch_no`",
|
||||||
"`tabStock Entry Detail`.`serial_no`",
|
"`tabStock Entry Detail`.`serial_no`",
|
||||||
|
"`tabStock Entry Detail`.`s_warehouse`",
|
||||||
|
"`tabStock Entry Detail`.`t_warehouse`",
|
||||||
"`tabStock Entry Detail`.`use_serial_batch_fields`",
|
"`tabStock Entry Detail`.`use_serial_batch_fields`",
|
||||||
],
|
],
|
||||||
filters=[
|
filters=[
|
||||||
@@ -3259,8 +3326,8 @@ def get_items_from_subcontract_order(source_name, target_doc=None):
|
|||||||
return target_doc
|
return target_doc
|
||||||
|
|
||||||
|
|
||||||
def get_available_materials(work_order) -> dict:
|
def get_available_materials(work_order, stock_entry_doc=None) -> dict:
|
||||||
data = get_stock_entry_data(work_order)
|
data = get_stock_entry_data(work_order, stock_entry_doc=stock_entry_doc)
|
||||||
|
|
||||||
available_materials = {}
|
available_materials = {}
|
||||||
for row in data:
|
for row in data:
|
||||||
@@ -3268,6 +3335,9 @@ def get_available_materials(work_order) -> dict:
|
|||||||
if row.purpose != "Material Transfer for Manufacture":
|
if row.purpose != "Material Transfer for Manufacture":
|
||||||
key = (row.item_code, row.s_warehouse)
|
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:
|
if key not in available_materials:
|
||||||
available_materials.setdefault(
|
available_materials.setdefault(
|
||||||
key,
|
key,
|
||||||
@@ -3278,7 +3348,9 @@ def get_available_materials(work_order) -> dict:
|
|||||||
|
|
||||||
item_data = available_materials[key]
|
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
|
item_data.qty += row.qty
|
||||||
if row.batch_no:
|
if row.batch_no:
|
||||||
item_data.batch_details[row.batch_no] += row.qty
|
item_data.batch_details[row.batch_no] += row.qty
|
||||||
@@ -3318,7 +3390,7 @@ def get_available_materials(work_order) -> dict:
|
|||||||
return available_materials
|
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 (
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
get_voucher_wise_serial_batch_from_bundle,
|
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.name == stock_entry_detail.parent)
|
||||||
& (stock_entry.work_order == work_order)
|
& (stock_entry.work_order == work_order)
|
||||||
& (stock_entry.docstatus == 1)
|
& (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)
|
.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:
|
if not data:
|
||||||
return []
|
return []
|
||||||
@@ -3375,6 +3463,9 @@ def get_stock_entry_data(work_order):
|
|||||||
if row.purpose != "Material Transfer for Manufacture":
|
if row.purpose != "Material Transfer for Manufacture":
|
||||||
key = (row.item_code, row.s_warehouse, row.name)
|
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):
|
if bundle_data.get(key):
|
||||||
row.update(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:
|
if sle.dependant_sle_voucher_detail_no:
|
||||||
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
|
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:
|
if self.exceptions:
|
||||||
self.raise_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):
|
def has_stock_reco_with_serial_batch(self, sle):
|
||||||
if (
|
if (
|
||||||
sle.voucher_type == "Stock Reconciliation"
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
@@ -696,6 +718,13 @@ class update_entries_after:
|
|||||||
{"item_code": self.item_code, "warehouse": self.args.warehouse}
|
{"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))
|
return list(self.get_sle_after_datetime(args))
|
||||||
|
|
||||||
def get_dependent_entries_to_fix(self, entries_to_fix, sle):
|
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):
|
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
|
||||||
self.distinct_item_warehouses[key] = val
|
self.distinct_item_warehouses[key] = val
|
||||||
self.new_items_found = True
|
self.new_items_found = True
|
||||||
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
|
elif (
|
||||||
dependant_sle.voucher_no
|
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"):
|
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
|
||||||
return
|
return
|
||||||
@@ -1156,7 +1187,11 @@ class update_entries_after:
|
|||||||
|
|
||||||
def get_dynamic_incoming_outgoing_rate(self, sle):
|
def get_dynamic_incoming_outgoing_rate(self, sle):
|
||||||
# Get updated incoming/outgoing rate from transaction
|
# 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)
|
rate = self.get_incoming_outgoing_rate_from_transaction(sle)
|
||||||
|
|
||||||
if flt(sle.actual_qty) >= 0:
|
if flt(sle.actual_qty) >= 0:
|
||||||
@@ -1813,6 +1848,9 @@ def get_stock_ledger_entries(
|
|||||||
if operator in (">", "<=") and previous_sle.get("name"):
|
if operator in (">", "<=") and previous_sle.get("name"):
|
||||||
conditions += " and name!=%(name)s"
|
conditions += " and name!=%(name)s"
|
||||||
|
|
||||||
|
if previous_sle.get("filter_voucher_type"):
|
||||||
|
conditions += " and voucher_type = %(filter_voucher_type)s"
|
||||||
|
|
||||||
if extra_cond:
|
if extra_cond:
|
||||||
conditions += f"{extra_cond}"
|
conditions += f"{extra_cond}"
|
||||||
|
|
||||||
@@ -1908,8 +1946,7 @@ def get_valuation_rate(
|
|||||||
& (table.warehouse == warehouse)
|
& (table.warehouse == warehouse)
|
||||||
& (table.batch_no == batch_no)
|
& (table.batch_no == batch_no)
|
||||||
& (table.is_cancelled == 0)
|
& (table.is_cancelled == 0)
|
||||||
& (table.voucher_no != voucher_no)
|
& ((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
|
||||||
& (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)))
|
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
|
||||||
|
|
||||||
return incoming_rate
|
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:
|
else:
|
||||||
deserialized_data = data
|
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)
|
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(
|
frappe.enqueue(
|
||||||
job,
|
job,
|
||||||
deserialized_data=deserialized_data,
|
deserialized_data=deserialized_data,
|
||||||
|
|||||||
Reference in New Issue
Block a user