mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 07:32:50 +00:00
Merge branch 'version-16-hotfix' into pr-52968
This commit is contained in:
@@ -6,7 +6,7 @@ import frappe
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils.user import is_website_user
|
from frappe.utils.user import is_website_user
|
||||||
|
|
||||||
__version__ = "16.3.0"
|
__version__ = "16.7.3"
|
||||||
|
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
|
|||||||
@@ -205,7 +205,7 @@
|
|||||||
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
||||||
"fieldname": "automatically_fetch_payment_terms",
|
"fieldname": "automatically_fetch_payment_terms",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Automatically Fetch Payment Terms from Order"
|
"label": "Automatically Fetch Payment Terms from Order/Quotation"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
||||||
@@ -697,7 +697,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-04 17:15:38.609327",
|
"modified": "2026-02-27 01:04:09.415288",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -1065,8 +1065,12 @@ class PaymentEntry(AccountsController):
|
|||||||
total_allocated_amount += flt(d.allocated_amount)
|
total_allocated_amount += flt(d.allocated_amount)
|
||||||
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
|
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
|
||||||
|
|
||||||
self.total_allocated_amount = abs(total_allocated_amount)
|
self.total_allocated_amount = flt(
|
||||||
self.base_total_allocated_amount = abs(base_total_allocated_amount)
|
abs(total_allocated_amount), self.precision("total_allocated_amount")
|
||||||
|
)
|
||||||
|
self.base_total_allocated_amount = flt(
|
||||||
|
abs(base_total_allocated_amount), self.precision("base_total_allocated_amount")
|
||||||
|
)
|
||||||
|
|
||||||
def set_unallocated_amount(self):
|
def set_unallocated_amount(self):
|
||||||
self.unallocated_amount = 0
|
self.unallocated_amount = 0
|
||||||
|
|||||||
@@ -4,19 +4,6 @@
|
|||||||
frappe.ui.form.on("POS Closing Entry", {
|
frappe.ui.form.on("POS Closing Entry", {
|
||||||
onload: async function (frm) {
|
onload: async function (frm) {
|
||||||
frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log", "Sales Invoice"];
|
frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log", "Sales Invoice"];
|
||||||
frm.set_query("pos_profile", function (doc) {
|
|
||||||
return {
|
|
||||||
filters: { user: doc.user },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.set_query("user", function (doc) {
|
|
||||||
return {
|
|
||||||
query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers",
|
|
||||||
filters: { parent: doc.pos_profile },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.set_query("pos_opening_entry", function (doc) {
|
frm.set_query("pos_opening_entry", function (doc) {
|
||||||
return { filters: { status: "Open", docstatus: 1 } };
|
return { filters: { status: "Open", docstatus: 1 } };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -346,7 +346,6 @@ def apply_pricing_rule(args, doc=None):
|
|||||||
|
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
if not args.transaction_type:
|
|
||||||
set_transaction_type(args)
|
set_transaction_type(args)
|
||||||
|
|
||||||
# list of dictionaries
|
# list of dictionaries
|
||||||
@@ -683,23 +682,23 @@ def remove_pricing_rules(item_list):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def set_transaction_type(args):
|
def set_transaction_type(pricing_ctx: frappe._dict) -> None:
|
||||||
if args.transaction_type:
|
if pricing_ctx.transaction_type in ["buying", "selling"]:
|
||||||
return
|
return
|
||||||
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
if pricing_ctx.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||||
args.transaction_type = "selling"
|
pricing_ctx.transaction_type = "selling"
|
||||||
elif args.doctype in (
|
elif pricing_ctx.doctype in (
|
||||||
"Material Request",
|
"Material Request",
|
||||||
"Supplier Quotation",
|
"Supplier Quotation",
|
||||||
"Purchase Order",
|
"Purchase Order",
|
||||||
"Purchase Receipt",
|
"Purchase Receipt",
|
||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
):
|
):
|
||||||
args.transaction_type = "buying"
|
pricing_ctx.transaction_type = "buying"
|
||||||
elif args.customer:
|
elif pricing_ctx.customer:
|
||||||
args.transaction_type = "selling"
|
pricing_ctx.transaction_type = "selling"
|
||||||
else:
|
else:
|
||||||
args.transaction_type = "buying"
|
pricing_ctx.transaction_type = "buying"
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -800,8 +800,7 @@
|
|||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Time Sheets",
|
"label": "Time Sheets",
|
||||||
"options": "Sales Invoice Timesheet",
|
"options": "Sales Invoice Timesheet",
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -2331,7 +2330,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2026-02-25 12:41:57.043459",
|
"modified": "2026-02-28 17:58:56.453076",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -1451,6 +1451,9 @@ class SalesInvoice(SellingController):
|
|||||||
return asset_qty_map
|
return asset_qty_map
|
||||||
|
|
||||||
def process_asset_depreciation(self):
|
def process_asset_depreciation(self):
|
||||||
|
if self.is_internal_transfer():
|
||||||
|
return
|
||||||
|
|
||||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||||
self.depreciate_asset_on_sale()
|
self.depreciate_asset_on_sale()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.contacts.doctype.address.address import get_default_address
|
from frappe.contacts.doctype.address.address import get_default_address
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
from frappe.query_builder.functions import IfNull
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr
|
||||||
from frappe.utils.nestedset import get_root_of
|
from frappe.utils.nestedset import get_root_of
|
||||||
|
|
||||||
@@ -83,6 +85,8 @@ class TaxRule(Document):
|
|||||||
frappe.throw(_("Tax Template is mandatory."))
|
frappe.throw(_("Tax Template is mandatory."))
|
||||||
|
|
||||||
def validate_filters(self):
|
def validate_filters(self):
|
||||||
|
TaxRule = DocType("Tax Rule")
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"tax_type": self.tax_type,
|
"tax_type": self.tax_type,
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
@@ -105,33 +109,34 @@ class TaxRule(Document):
|
|||||||
"company": self.company,
|
"company": self.company,
|
||||||
}
|
}
|
||||||
|
|
||||||
conds = ""
|
query = (
|
||||||
for d in filters:
|
frappe.qb.from_(TaxRule).select(TaxRule.name, TaxRule.priority).where(TaxRule.name != self.name)
|
||||||
if conds:
|
|
||||||
conds += " and "
|
|
||||||
conds += f"""ifnull({d}, '') = {frappe.db.escape(cstr(filters[d]))}"""
|
|
||||||
|
|
||||||
if self.from_date and self.to_date:
|
|
||||||
conds += f""" and ((from_date > '{self.from_date}' and from_date < '{self.to_date}') or
|
|
||||||
(to_date > '{self.from_date}' and to_date < '{self.to_date}') or
|
|
||||||
('{self.from_date}' > from_date and '{self.from_date}' < to_date) or
|
|
||||||
('{self.from_date}' = from_date and '{self.to_date}' = to_date))"""
|
|
||||||
|
|
||||||
elif self.from_date and not self.to_date:
|
|
||||||
conds += f""" and to_date > '{self.from_date}'"""
|
|
||||||
|
|
||||||
elif self.to_date and not self.from_date:
|
|
||||||
conds += f""" and from_date < '{self.to_date}'"""
|
|
||||||
|
|
||||||
tax_rule = frappe.db.sql(
|
|
||||||
f"select name, priority \
|
|
||||||
from `tabTax Rule` where {conds} and name != '{self.name}'",
|
|
||||||
as_dict=1,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if tax_rule:
|
for field, value in filters.items():
|
||||||
if tax_rule[0].priority == self.priority:
|
query = query.where(IfNull(TaxRule[field], "") == cstr(value))
|
||||||
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
|
|
||||||
|
if self.from_date and self.to_date:
|
||||||
|
query = query.where(
|
||||||
|
((TaxRule.from_date > self.from_date) & (TaxRule.from_date < self.to_date))
|
||||||
|
| ((TaxRule.to_date > self.from_date) & (TaxRule.to_date < self.to_date))
|
||||||
|
| ((self.from_date > TaxRule.from_date) & (self.from_date < TaxRule.to_date))
|
||||||
|
| ((TaxRule.from_date == self.from_date) & (TaxRule.to_date == self.to_date))
|
||||||
|
)
|
||||||
|
|
||||||
|
elif self.from_date:
|
||||||
|
query = query.where(TaxRule.to_date > self.from_date)
|
||||||
|
|
||||||
|
elif self.to_date:
|
||||||
|
query = query.where(TaxRule.from_date < self.to_date)
|
||||||
|
|
||||||
|
tax_rule = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if tax_rule and tax_rule[0].priority == self.priority:
|
||||||
|
frappe.throw(
|
||||||
|
_("Tax Rule Conflicts with {0}").format(tax_rule[0].name),
|
||||||
|
ConflictingTaxRule,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.tests import IntegrationTestCase
|
from frappe.tests import IntegrationTestCase
|
||||||
from frappe.utils import today
|
from frappe.utils import add_days, today
|
||||||
|
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||||
@@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, IntegrationTestCase):
|
|||||||
if not do_not_submit:
|
if not do_not_submit:
|
||||||
pi = pi.submit()
|
pi = pi.submit()
|
||||||
return pi
|
return pi
|
||||||
|
|
||||||
|
def test_payment_terms_template_filters(self):
|
||||||
|
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||||
|
|
||||||
|
payment_term1 = frappe.get_doc(
|
||||||
|
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
|
||||||
|
).insert()
|
||||||
|
payment_term2 = frappe.get_doc(
|
||||||
|
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
template = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template",
|
||||||
|
"template_name": "_Test 50-50",
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template Detail",
|
||||||
|
"due_date_based_on": "Day(s) after invoice date",
|
||||||
|
"payment_term": payment_term1.name,
|
||||||
|
"description": "_Test 50-50",
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"credit_days": 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template Detail",
|
||||||
|
"due_date_based_on": "Day(s) after invoice date",
|
||||||
|
"payment_term": payment_term2.name,
|
||||||
|
"description": "_Test 50-50",
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"credit_days": 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
template.insert()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range": "30, 60, 90, 120",
|
||||||
|
"based_on_payment_terms": 1,
|
||||||
|
"payment_terms_template": template.name,
|
||||||
|
"ageing_based_on": "Posting Date",
|
||||||
|
}
|
||||||
|
|
||||||
|
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||||
|
pi.payment_terms_template = template.name
|
||||||
|
schedule = get_payment_terms(template.name)
|
||||||
|
pi.set("payment_schedule", [])
|
||||||
|
|
||||||
|
for row in schedule:
|
||||||
|
row["due_date"] = add_days(pi.posting_date, row.get("credit_days", 0))
|
||||||
|
pi.append("payment_schedule", row)
|
||||||
|
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
row = report[1][0]
|
||||||
|
|
||||||
|
self.assertEqual(len(report[1]), 2)
|
||||||
|
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||||
|
|||||||
@@ -1035,9 +1035,8 @@ class ReceivablePayableReport:
|
|||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
self.customer = qb.DocType("Customer")
|
self.customer = qb.DocType("Customer")
|
||||||
|
|
||||||
if self.filters.get("customer_group"):
|
if self.filters.get("customer_group"):
|
||||||
groups = get_customer_group_with_children(self.filters.customer_group)
|
groups = get_party_group_with_children("Customer", self.filters.customer_group)
|
||||||
customers = (
|
customers = (
|
||||||
qb.from_(self.customer)
|
qb.from_(self.customer)
|
||||||
.select(self.customer.name)
|
.select(self.customer.name)
|
||||||
@@ -1049,13 +1048,17 @@ class ReceivablePayableReport:
|
|||||||
self.get_hierarchical_filters("Territory", "territory")
|
self.get_hierarchical_filters("Territory", "territory")
|
||||||
|
|
||||||
if self.filters.get("payment_terms_template"):
|
if self.filters.get("payment_terms_template"):
|
||||||
self.qb_selection_filter.append(
|
customer_ptt = self.ple.party.isin(
|
||||||
self.ple.party.isin(
|
|
||||||
qb.from_(self.customer)
|
qb.from_(self.customer)
|
||||||
.select(self.customer.name)
|
.select(self.customer.name)
|
||||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
si_ptt = self.add_payment_term_template_filters("Sales Invoice")
|
||||||
|
|
||||||
|
sales_ptt = self.ple.against_voucher_no.isin(si_ptt)
|
||||||
|
|
||||||
|
self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt]))
|
||||||
|
|
||||||
if self.filters.get("sales_partner"):
|
if self.filters.get("sales_partner"):
|
||||||
self.qb_selection_filter.append(
|
self.qb_selection_filter.append(
|
||||||
@@ -1081,14 +1084,53 @@ class ReceivablePayableReport:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.filters.get("payment_terms_template"):
|
if self.filters.get("payment_terms_template"):
|
||||||
self.qb_selection_filter.append(
|
supplier_ptt = self.ple.party.isin(
|
||||||
self.ple.party.isin(
|
|
||||||
qb.from_(supplier)
|
qb.from_(supplier)
|
||||||
.select(supplier.name)
|
.select(supplier.name)
|
||||||
.where(supplier.payment_terms == self.filters.get("supplier_group"))
|
.where(supplier.payment_terms == self.filters.get("payment_terms_template"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pi_ptt = self.add_payment_term_template_filters("Purchase Invoice")
|
||||||
|
|
||||||
|
purchase_ptt = self.ple.against_voucher_no.isin(pi_ptt)
|
||||||
|
|
||||||
|
self.qb_selection_filter.append(Criterion.any([supplier_ptt, purchase_ptt]))
|
||||||
|
|
||||||
|
def add_payment_term_template_filters(self, dtype):
|
||||||
|
voucher_type = qb.DocType(dtype)
|
||||||
|
|
||||||
|
ptt = (
|
||||||
|
qb.from_(voucher_type)
|
||||||
|
.select(voucher_type.name)
|
||||||
|
.where(voucher_type.payment_terms_template == self.filters.get("payment_terms_template"))
|
||||||
|
.where(voucher_type.company == self.filters.company)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if dtype == "Purchase Invoice":
|
||||||
|
party = "Supplier"
|
||||||
|
party_group_type = "supplier_group"
|
||||||
|
acc_type = "credit_to"
|
||||||
|
else:
|
||||||
|
party = "Customer"
|
||||||
|
party_group_type = "customer_group"
|
||||||
|
acc_type = "debit_to"
|
||||||
|
|
||||||
|
if self.filters.get(party_group_type):
|
||||||
|
party_groups = get_party_group_with_children(party, self.filters.get(party_group_type))
|
||||||
|
ptt = ptt.where((voucher_type[party_group_type]).isin(party_groups))
|
||||||
|
|
||||||
|
if self.filters.party:
|
||||||
|
ptt = ptt.where((voucher_type[party.lower()]).isin(self.filters.party))
|
||||||
|
|
||||||
|
if self.filters.cost_center:
|
||||||
|
cost_centers = get_cost_centers_with_children(self.filters.cost_center)
|
||||||
|
ptt = ptt.where(voucher_type.cost_center.isin(cost_centers))
|
||||||
|
|
||||||
|
if self.filters.party_account:
|
||||||
|
ptt = ptt.where(voucher_type[acc_type] == self.filters.party_account)
|
||||||
|
|
||||||
|
return ptt
|
||||||
|
|
||||||
def get_hierarchical_filters(self, doctype, key):
|
def get_hierarchical_filters(self, doctype, key):
|
||||||
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
||||||
|
|
||||||
@@ -1330,20 +1372,26 @@ class ReceivablePayableReport:
|
|||||||
self.err_journals = [x[0] for x in results] if results else []
|
self.err_journals = [x[0] for x in results] if results else []
|
||||||
|
|
||||||
|
|
||||||
def get_customer_group_with_children(customer_groups):
|
def get_party_group_with_children(party, party_groups):
|
||||||
if not isinstance(customer_groups, list):
|
if party not in ("Customer", "Supplier"):
|
||||||
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
|
return []
|
||||||
|
|
||||||
all_customer_groups = []
|
group_dtype = f"{party} Group"
|
||||||
for d in customer_groups:
|
if not isinstance(party_groups, list):
|
||||||
if frappe.db.exists("Customer Group", d):
|
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
|
||||||
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
|
|
||||||
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
all_party_groups = []
|
||||||
all_customer_groups += [c.name for c in children]
|
for d in party_groups:
|
||||||
|
if frappe.db.exists(group_dtype, d):
|
||||||
|
lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"])
|
||||||
|
children = frappe.get_all(
|
||||||
|
group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name"
|
||||||
|
)
|
||||||
|
all_party_groups += children
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
|
||||||
|
|
||||||
return list(set(all_customer_groups))
|
return list(set(all_party_groups))
|
||||||
|
|
||||||
|
|
||||||
class InitSQLProceduresForAR:
|
class InitSQLProceduresForAR:
|
||||||
|
|||||||
@@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
|||||||
self.assertEqual(len(report[1]), 1)
|
self.assertEqual(len(report[1]), 1)
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||||
|
|
||||||
|
def test_payment_terms_template_filters(self):
|
||||||
|
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||||
|
|
||||||
|
payment_term1 = frappe.get_doc(
|
||||||
|
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
|
||||||
|
).insert()
|
||||||
|
payment_term2 = frappe.get_doc(
|
||||||
|
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
template = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template",
|
||||||
|
"template_name": "_Test 50-50",
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template Detail",
|
||||||
|
"due_date_based_on": "Day(s) after invoice date",
|
||||||
|
"payment_term": payment_term1.name,
|
||||||
|
"description": "_Test 50-50",
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"credit_days": 15,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template Detail",
|
||||||
|
"due_date_based_on": "Day(s) after invoice date",
|
||||||
|
"payment_term": payment_term2.name,
|
||||||
|
"description": "_Test 50-50",
|
||||||
|
"invoice_portion": 50,
|
||||||
|
"credit_days": 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
template.insert()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range": "30, 60, 90, 120",
|
||||||
|
"based_on_payment_terms": 1,
|
||||||
|
"payment_terms_template": template.name,
|
||||||
|
"ageing_based_on": "Posting Date",
|
||||||
|
}
|
||||||
|
|
||||||
|
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
si.payment_terms_template = template.name
|
||||||
|
schedule = get_payment_terms(template.name)
|
||||||
|
si.set("payment_schedule", [])
|
||||||
|
|
||||||
|
for row in schedule:
|
||||||
|
row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0))
|
||||||
|
si.append("payment_schedule", row)
|
||||||
|
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
report = execute(filters)
|
||||||
|
row = report[1][0]
|
||||||
|
|
||||||
|
self.assertEqual(len(report[1]), 2)
|
||||||
|
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import add_months, flt, formatdate
|
from frappe.utils import add_months, flt, formatdate
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.controllers.trends import get_period_date_ranges
|
from erpnext.controllers.trends import get_period_date_ranges
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ def execute(filters=None):
|
|||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
|
validate_filters(filters)
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
if filters.get("budget_against_filter"):
|
if filters.get("budget_against_filter"):
|
||||||
dimensions = filters.get("budget_against_filter")
|
dimensions = filters.get("budget_against_filter")
|
||||||
@@ -31,6 +34,10 @@ def execute(filters=None):
|
|||||||
return columns, data, None, chart_data
|
return columns, data, None, chart_data
|
||||||
|
|
||||||
|
|
||||||
|
def validate_filters(filters):
|
||||||
|
validate_budget_dimensions(filters)
|
||||||
|
|
||||||
|
|
||||||
def get_budget_records(filters, dimensions):
|
def get_budget_records(filters, dimensions):
|
||||||
budget_against_field = frappe.scrub(filters["budget_against"])
|
budget_against_field = frappe.scrub(filters["budget_against"])
|
||||||
|
|
||||||
@@ -51,7 +58,7 @@ def get_budget_records(filters, dimensions):
|
|||||||
b.company = %s
|
b.company = %s
|
||||||
AND b.docstatus = 1
|
AND b.docstatus = 1
|
||||||
AND b.budget_against = %s
|
AND b.budget_against = %s
|
||||||
AND b.{budget_against_field} IN ({', '.join(['%s'] * len(dimensions))})
|
AND b.{budget_against_field} IN ({", ".join(["%s"] * len(dimensions))})
|
||||||
AND (
|
AND (
|
||||||
b.from_fiscal_year <= %s
|
b.from_fiscal_year <= %s
|
||||||
AND b.to_fiscal_year >= %s
|
AND b.to_fiscal_year >= %s
|
||||||
@@ -404,6 +411,17 @@ def get_budget_dimensions(filters):
|
|||||||
) # nosec
|
) # nosec
|
||||||
|
|
||||||
|
|
||||||
|
def validate_budget_dimensions(filters):
|
||||||
|
dimensions = [d.get("document_type") for d in get_dimensions(with_cost_center_and_project=True)[0]]
|
||||||
|
if filters.get("budget_against") and filters.get("budget_against") not in dimensions:
|
||||||
|
frappe.throw(
|
||||||
|
title=_("Invalid Accounting Dimension"),
|
||||||
|
msg=_("{0} is not a valid Accounting Dimension.").format(
|
||||||
|
frappe.bold(filters.get("budget_against"))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_comparison_chart_data(filters, columns, data):
|
def build_comparison_chart_data(filters, columns, data):
|
||||||
if not data:
|
if not data:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ frappe.query_reports["Consolidated Trial Balance"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "show_net_values",
|
||||||
|
label: __("Show net values in opening and closing columns"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldname: "show_group_accounts",
|
fieldname: "show_group_accounts",
|
||||||
label: __("Show Group Accounts"),
|
label: __("Show Group Accounts"),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from erpnext.accounts.report.financial_statements import (
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.report.trial_balance.trial_balance import (
|
from erpnext.accounts.report.trial_balance.trial_balance import (
|
||||||
accumulate_values_into_parents,
|
accumulate_values_into_parents,
|
||||||
|
calculate_total_row,
|
||||||
calculate_values,
|
calculate_values,
|
||||||
get_opening_balances,
|
get_opening_balances,
|
||||||
hide_group_accounts,
|
hide_group_accounts,
|
||||||
@@ -44,7 +45,6 @@ def execute(filters: dict | None = None):
|
|||||||
|
|
||||||
def validate_filters(filters):
|
def validate_filters(filters):
|
||||||
validate_companies(filters)
|
validate_companies(filters)
|
||||||
filters.show_net_values = True
|
|
||||||
tb_validate_filters(filters)
|
tb_validate_filters(filters)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,16 +99,20 @@ def get_data(filters) -> list[list]:
|
|||||||
tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
|
tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency)
|
||||||
consolidate_trial_balance_data(data, tb_data)
|
consolidate_trial_balance_data(data, tb_data)
|
||||||
|
|
||||||
for d in data:
|
if filters.get("show_net_values"):
|
||||||
prepare_opening_closing(d)
|
prepare_opening_closing_for_ctb(data)
|
||||||
|
|
||||||
total_row = calculate_total_row(data, reporting_currency)
|
|
||||||
|
|
||||||
data.extend([{}, total_row])
|
|
||||||
|
|
||||||
if not filters.get("show_group_accounts"):
|
if not filters.get("show_group_accounts"):
|
||||||
data = hide_group_accounts(data)
|
data = hide_group_accounts(data)
|
||||||
|
|
||||||
|
total_row = calculate_total_row(
|
||||||
|
data, reporting_currency, show_group_accounts=filters.get("show_group_accounts")
|
||||||
|
)
|
||||||
|
|
||||||
|
calculate_foreign_currency_translation_reserve(total_row, data, filters=filters)
|
||||||
|
|
||||||
|
data.extend([total_row])
|
||||||
|
|
||||||
if filters.get("presentation_currency"):
|
if filters.get("presentation_currency"):
|
||||||
update_to_presentation_currency(
|
update_to_presentation_currency(
|
||||||
data,
|
data,
|
||||||
@@ -207,10 +211,6 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
|
|||||||
data = []
|
data = []
|
||||||
|
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
# Prepare opening closing for group account
|
|
||||||
if parent_children_map.get(d.account) and filters.get("show_net_values"):
|
|
||||||
prepare_opening_closing(d)
|
|
||||||
|
|
||||||
has_value = False
|
has_value = False
|
||||||
row = {
|
row = {
|
||||||
"account": d.name,
|
"account": d.name,
|
||||||
@@ -242,35 +242,9 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def calculate_total_row(data, reporting_currency):
|
def calculate_foreign_currency_translation_reserve(total_row, data, filters):
|
||||||
total_row = {
|
if not data or not total_row:
|
||||||
"account": "'" + _("Total") + "'",
|
return
|
||||||
"account_name": "'" + _("Total") + "'",
|
|
||||||
"warn_if_negative": True,
|
|
||||||
"opening_debit": 0.0,
|
|
||||||
"opening_credit": 0.0,
|
|
||||||
"debit": 0.0,
|
|
||||||
"credit": 0.0,
|
|
||||||
"closing_debit": 0.0,
|
|
||||||
"closing_credit": 0.0,
|
|
||||||
"parent_account": None,
|
|
||||||
"indent": 0,
|
|
||||||
"has_value": True,
|
|
||||||
"currency": reporting_currency,
|
|
||||||
}
|
|
||||||
|
|
||||||
for d in data:
|
|
||||||
if not d.get("parent_account"):
|
|
||||||
for field in value_fields:
|
|
||||||
total_row[field] += d[field]
|
|
||||||
|
|
||||||
if data:
|
|
||||||
calculate_foreign_currency_translation_reserve(total_row, data)
|
|
||||||
|
|
||||||
return total_row
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_foreign_currency_translation_reserve(total_row, data):
|
|
||||||
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
|
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"]
|
||||||
dr_cr_diff = total_row["debit"] - total_row["credit"]
|
dr_cr_diff = total_row["debit"] - total_row["credit"]
|
||||||
|
|
||||||
@@ -289,7 +263,7 @@ def calculate_foreign_currency_translation_reserve(total_row, data):
|
|||||||
"root_type": data[idx].get("root_type"),
|
"root_type": data[idx].get("root_type"),
|
||||||
"account_type": "Equity",
|
"account_type": "Equity",
|
||||||
"parent_account": data[idx].get("account"),
|
"parent_account": data[idx].get("account"),
|
||||||
"indent": data[idx].get("indent") + 1,
|
"indent": data[idx].get("indent") + 1 if filters.get("show_group_accounts") else 0,
|
||||||
"has_value": True,
|
"has_value": True,
|
||||||
"currency": total_row.get("currency"),
|
"currency": total_row.get("currency"),
|
||||||
}
|
}
|
||||||
@@ -297,6 +271,7 @@ def calculate_foreign_currency_translation_reserve(total_row, data):
|
|||||||
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
|
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
|
||||||
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
|
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
|
||||||
|
|
||||||
|
if filters.get("show_net_values"):
|
||||||
prepare_opening_closing(fctr_row)
|
prepare_opening_closing(fctr_row)
|
||||||
|
|
||||||
data.insert(idx + 1, fctr_row)
|
data.insert(idx + 1, fctr_row)
|
||||||
@@ -396,6 +371,11 @@ def update_to_presentation_currency(data, from_currency, to_currency, date, igno
|
|||||||
d.update(currency=to_currency)
|
d.update(currency=to_currency)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_opening_closing_for_ctb(data):
|
||||||
|
for d in data:
|
||||||
|
prepare_opening_closing(d)
|
||||||
|
|
||||||
|
|
||||||
def get_columns():
|
def get_columns():
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ def calculate_values(
|
|||||||
prepare_opening_closing(d)
|
prepare_opening_closing(d)
|
||||||
|
|
||||||
|
|
||||||
def calculate_total_row(accounts, company_currency):
|
def calculate_total_row(data, company_currency, show_group_accounts=True):
|
||||||
total_row = {
|
total_row = {
|
||||||
"account": "'" + _("Total") + "'",
|
"account": "'" + _("Total") + "'",
|
||||||
"account_name": "'" + _("Total") + "'",
|
"account_name": "'" + _("Total") + "'",
|
||||||
@@ -407,10 +407,16 @@ def calculate_total_row(accounts, company_currency):
|
|||||||
"currency": company_currency,
|
"currency": company_currency,
|
||||||
}
|
}
|
||||||
|
|
||||||
for d in accounts:
|
def sum_value_fields(row):
|
||||||
if not d.parent_account:
|
|
||||||
for field in value_fields:
|
for field in value_fields:
|
||||||
total_row[field] += d[field]
|
total_row[field] += row[field]
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
if not show_group_accounts:
|
||||||
|
sum_value_fields(d)
|
||||||
|
|
||||||
|
elif show_group_accounts and not d.get("parent_account"):
|
||||||
|
sum_value_fields(d)
|
||||||
|
|
||||||
return total_row
|
return total_row
|
||||||
|
|
||||||
@@ -456,11 +462,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
|||||||
row["has_value"] = has_value
|
row["has_value"] = has_value
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
total_row = calculate_total_row(accounts, company_currency)
|
|
||||||
|
|
||||||
if not filters.get("show_group_accounts"):
|
if not filters.get("show_group_accounts"):
|
||||||
data = hide_group_accounts(data)
|
data = hide_group_accounts(data)
|
||||||
|
|
||||||
|
total_row = calculate_total_row(
|
||||||
|
data, company_currency, show_group_accounts=filters.get("show_group_accounts")
|
||||||
|
)
|
||||||
|
|
||||||
data.extend([{}, total_row])
|
data.extend([{}, total_row])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
this.show_general_ledger();
|
this.show_general_ledger();
|
||||||
|
erpnext.toggle_serial_batch_fields(this.frm);
|
||||||
|
|
||||||
if (this.frm.doc.stock_items && this.frm.doc.stock_items.length) {
|
if (this.frm.doc.stock_items && this.frm.doc.stock_items.length) {
|
||||||
this.show_stock_ledger();
|
this.show_stock_ledger();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"doctype": "Module Onboarding",
|
"doctype": "Module Onboarding",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"modified": "2026-02-26 10:45:47.970714",
|
"modified": "2026-02-26 10:50:47.970714",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Onboarding",
|
"name": "Asset Onboarding",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2026-02-26 10:44:59.557156",
|
"modified": "2026-02-26 10:50:59.557156",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Learn Asset",
|
"name": "Learn Asset",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"order_confirmation_date",
|
"order_confirmation_date",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"transaction_time",
|
||||||
"schedule_date",
|
"schedule_date",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"company",
|
"company",
|
||||||
@@ -1311,6 +1312,14 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_tnkm",
|
"fieldname": "section_break_tnkm",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Now",
|
||||||
|
"depends_on": "is_internal_supplier",
|
||||||
|
"fieldname": "transaction_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"label": "Time",
|
||||||
|
"mandatory_depends_on": "is_internal_supplier"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -1318,7 +1327,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-23 14:22:33.323946",
|
"modified": "2026-03-02 00:40:47.119584",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order",
|
"name": "Purchase Order",
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
total_qty: DF.Float
|
total_qty: DF.Float
|
||||||
total_taxes_and_charges: DF.Currency
|
total_taxes_and_charges: DF.Currency
|
||||||
transaction_date: DF.Date
|
transaction_date: DF.Date
|
||||||
|
transaction_time: DF.Time | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -250,10 +250,17 @@ frappe.ui.form.on("Request for Quotation", {
|
|||||||
"subject",
|
"subject",
|
||||||
])
|
])
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
frm.set_value(
|
if (r.message.use_html) {
|
||||||
"message_for_supplier",
|
frm.set_value({
|
||||||
r.message.use_html ? r.message.response_html : r.message.response
|
mfs_html: r.message.response_html,
|
||||||
);
|
use_html: 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frm.set_value({
|
||||||
|
message_for_supplier: r.message.response,
|
||||||
|
use_html: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
frm.set_value("subject", r.message.subject);
|
frm.set_value("subject", r.message.subject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
"send_document_print",
|
"send_document_print",
|
||||||
"sec_break_email_2",
|
"sec_break_email_2",
|
||||||
"subject",
|
"subject",
|
||||||
|
"use_html",
|
||||||
"message_for_supplier",
|
"message_for_supplier",
|
||||||
|
"mfs_html",
|
||||||
"terms_section_break",
|
"terms_section_break",
|
||||||
"incoterm",
|
"incoterm",
|
||||||
"named_place",
|
"named_place",
|
||||||
@@ -142,12 +144,13 @@
|
|||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"default": "Please supply the specified items at the best possible rates",
|
"default": "Please supply the specified items at the best possible rates",
|
||||||
|
"depends_on": "eval:doc.use_html == 0",
|
||||||
"fieldname": "message_for_supplier",
|
"fieldname": "message_for_supplier",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Message for Supplier",
|
"label": "Message for Supplier",
|
||||||
"print_hide": 1,
|
"mandatory_depends_on": "eval:doc.use_html == 0",
|
||||||
"reqd": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@@ -324,6 +327,22 @@
|
|||||||
"label": "Subject",
|
"label": "Subject",
|
||||||
"not_nullable": 1,
|
"not_nullable": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"depends_on": "eval:doc.use_html == 1",
|
||||||
|
"fieldname": "mfs_html",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Message for Supplier",
|
||||||
|
"mandatory_depends_on": "eval:doc.use_html == 1",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "use_html",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Use HTML"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -331,7 +350,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-06 10:31:08.747043",
|
"modified": "2026-03-01 23:38:48.079274",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class RequestforQuotation(BuyingController):
|
|||||||
incoterm: DF.Link | None
|
incoterm: DF.Link | None
|
||||||
items: DF.Table[RequestforQuotationItem]
|
items: DF.Table[RequestforQuotationItem]
|
||||||
letter_head: DF.Link | None
|
letter_head: DF.Link | None
|
||||||
message_for_supplier: DF.TextEditor
|
message_for_supplier: DF.TextEditor | None
|
||||||
|
mfs_html: DF.Code | None
|
||||||
named_place: DF.Data | None
|
named_place: DF.Data | None
|
||||||
naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
|
naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
|
||||||
opportunity: DF.Link | None
|
opportunity: DF.Link | None
|
||||||
@@ -61,6 +62,7 @@ class RequestforQuotation(BuyingController):
|
|||||||
tc_name: DF.Link | None
|
tc_name: DF.Link | None
|
||||||
terms: DF.TextEditor | None
|
terms: DF.TextEditor | None
|
||||||
transaction_date: DF.Date
|
transaction_date: DF.Date
|
||||||
|
use_html: DF.Check
|
||||||
vendor: DF.Link | None
|
vendor: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@@ -100,8 +102,16 @@ class RequestforQuotation(BuyingController):
|
|||||||
["use_html", "response", "response_html", "subject"],
|
["use_html", "response", "response_html", "subject"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.use_html = data.use_html
|
||||||
|
|
||||||
|
if data.use_html:
|
||||||
|
if not self.mfs_html:
|
||||||
|
self.mfs_html = data.response_html
|
||||||
|
else:
|
||||||
if not self.message_for_supplier:
|
if not self.message_for_supplier:
|
||||||
self.message_for_supplier = data.response_html if data.use_html else data.response
|
self.message_for_supplier = data.response
|
||||||
|
|
||||||
if not self.subject:
|
if not self.subject:
|
||||||
self.subject = data.subject
|
self.subject = data.subject
|
||||||
|
|
||||||
@@ -304,7 +314,10 @@ class RequestforQuotation(BuyingController):
|
|||||||
else:
|
else:
|
||||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||||
|
|
||||||
rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
|
message_template = self.mfs_html if self.use_html else self.message_for_supplier
|
||||||
|
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
|
||||||
|
rendered_message = frappe.render_template(message_template, doc_args)
|
||||||
|
|
||||||
subject_source = (
|
subject_source = (
|
||||||
self.subject
|
self.subject
|
||||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TestPurchaseOrder(IntegrationTestCase):
|
|||||||
self.assertEqual(sq.get("items")[1].rate, 300)
|
self.assertEqual(sq.get("items")[1].rate, 300)
|
||||||
self.assertEqual(sq.get("items")[1].description, "test")
|
self.assertEqual(sq.get("items")[1].description, "test")
|
||||||
|
|
||||||
def test_update_supplier_quotation_child_rate_disallow(self):
|
def test_update_supplier_quotation_child_rate(self):
|
||||||
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
|
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
|
||||||
sq.submit()
|
sq.submit()
|
||||||
trans_item = json.dumps(
|
trans_item = json.dumps(
|
||||||
@@ -47,6 +47,22 @@ class TestPurchaseOrder(IntegrationTestCase):
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||||
|
sq.reload()
|
||||||
|
self.assertEqual(sq.get("items")[0].rate, 300)
|
||||||
|
po = make_purchase_order(sq.name)
|
||||||
|
po.schedule_date = add_days(today(), 1)
|
||||||
|
po.submit()
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": sq.items[0].item_code,
|
||||||
|
"rate": 20,
|
||||||
|
"qty": sq.items[0].qty,
|
||||||
|
"docname": sq.items[0].name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ def get_data(filters):
|
|||||||
"cost_center": po.cost_center,
|
"cost_center": po.cost_center,
|
||||||
"project": po.project,
|
"project": po.project,
|
||||||
"requesting_site": po.warehouse,
|
"requesting_site": po.warehouse,
|
||||||
"requestor": po.owner,
|
"requestor": mr_record.get("owner", po.owner),
|
||||||
"material_request_no": po.material_request,
|
"material_request_no": po.material_request,
|
||||||
"item_code": po.item_code,
|
"item_code": po.item_code,
|
||||||
"quantity": flt(po.qty),
|
"quantity": flt(po.qty),
|
||||||
|
|||||||
@@ -2525,13 +2525,14 @@ class AccountsController(TransactionBase):
|
|||||||
grand_total = flt(self.get("rounded_total") or self.grand_total)
|
grand_total = flt(self.get("rounded_total") or self.grand_total)
|
||||||
automatically_fetch_payment_terms = 0
|
automatically_fetch_payment_terms = 0
|
||||||
|
|
||||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
|
||||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
|
||||||
grand_total = grand_total - flt(self.write_off_amount)
|
|
||||||
po_or_so, doctype, fieldname = self.get_order_details()
|
po_or_so, doctype, fieldname = self.get_order_details()
|
||||||
automatically_fetch_payment_terms = cint(
|
automatically_fetch_payment_terms = cint(
|
||||||
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
||||||
)
|
)
|
||||||
|
if self.doctype != "Sales Order":
|
||||||
|
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
||||||
|
grand_total = grand_total - flt(self.write_off_amount)
|
||||||
|
|
||||||
if self.get("total_advance"):
|
if self.get("total_advance"):
|
||||||
if party_account_currency == self.company_currency:
|
if party_account_currency == self.company_currency:
|
||||||
@@ -2547,7 +2548,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
if not self.get("payment_schedule"):
|
if not self.get("payment_schedule"):
|
||||||
if (
|
if (
|
||||||
self.doctype in ["Sales Invoice", "Purchase Invoice"]
|
self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
|
||||||
and automatically_fetch_payment_terms
|
and automatically_fetch_payment_terms
|
||||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||||
):
|
):
|
||||||
@@ -2605,16 +2606,18 @@ class AccountsController(TransactionBase):
|
|||||||
if not self.get("items"):
|
if not self.get("items"):
|
||||||
return None, None, None
|
return None, None, None
|
||||||
if self.doctype == "Sales Invoice":
|
if self.doctype == "Sales Invoice":
|
||||||
po_or_so = self.get("items")[0].get("sales_order")
|
prev_doc = self.get("items")[0].get("sales_order")
|
||||||
po_or_so_doctype = "Sales Order"
|
prev_doctype = "Sales Order"
|
||||||
po_or_so_doctype_name = "sales_order"
|
prev_doctype_name = "sales_order"
|
||||||
|
elif self.doctype == "Purchase Invoice":
|
||||||
|
prev_doc = self.get("items")[0].get("purchase_order")
|
||||||
|
prev_doctype = "Purchase Order"
|
||||||
|
prev_doctype_name = "purchase_order"
|
||||||
else:
|
else:
|
||||||
po_or_so = self.get("items")[0].get("purchase_order")
|
prev_doc = self.get("items")[0].get("prevdoc_docname")
|
||||||
po_or_so_doctype = "Purchase Order"
|
prev_doctype = "Quotation"
|
||||||
po_or_so_doctype_name = "purchase_order"
|
prev_doctype_name = "prevdoc_docname"
|
||||||
|
return prev_doc, prev_doctype, prev_doctype_name
|
||||||
return po_or_so, po_or_so_doctype, po_or_so_doctype_name
|
|
||||||
|
|
||||||
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
|
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
|
||||||
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
|
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
|
||||||
@@ -3872,20 +3875,28 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def validate_quantity(child_item, new_data):
|
def validate_quantity_and_rate(child_item, new_data):
|
||||||
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
|
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
|
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
|
||||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||||
),
|
),
|
||||||
title=_("Invalid Qty"),
|
title=_("Invalid Qty"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
|
qty_limits = {
|
||||||
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
|
||||||
|
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
|
||||||
|
}
|
||||||
|
|
||||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
if parent_doctype in qty_limits:
|
||||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
qty_field, error_message = qty_limits[parent_doctype]
|
||||||
|
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row #{0}:").format(new_data.get("idx"))
|
||||||
|
+ error_message.format(frappe.bold(new_data.get("item_code"))),
|
||||||
|
title=_("Invalid Qty"),
|
||||||
|
)
|
||||||
|
|
||||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||||
if (parent_doctype == "Quotation" and not ordered_items) or (
|
if (parent_doctype == "Quotation" and not ordered_items) or (
|
||||||
@@ -3898,7 +3909,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
if parent_doctype == "Quotation"
|
if parent_doctype == "Quotation"
|
||||||
else purchased_items.get(child_item.name)
|
else purchased_items.get(child_item.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
if qty_to_check:
|
if qty_to_check:
|
||||||
|
if not rate_unchanged:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||||
|
).format(frappe.bold(new_data.get("item_code")))
|
||||||
|
)
|
||||||
|
|
||||||
if flt(new_data.get("qty")) < qty_to_check:
|
if flt(new_data.get("qty")) < qty_to_check:
|
||||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||||
|
|
||||||
@@ -4017,10 +4036,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
validate_quantity(child_item, d)
|
validate_quantity_and_rate(child_item, d)
|
||||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
|
||||||
if not rate_unchanged:
|
|
||||||
frappe.throw(_("Rates cannot be modified for quoted items"))
|
|
||||||
|
|
||||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||||
any_qty_changed = True
|
any_qty_changed = True
|
||||||
|
|||||||
@@ -1011,8 +1011,15 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
|
|||||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||||
|
|
||||||
if doctype == "Packed Item":
|
if doctype == "Packed Item":
|
||||||
|
if key is None:
|
||||||
|
key = frappe.get_cached_value(
|
||||||
|
"Packed Item",
|
||||||
|
{"parent_detail_docname": row.voucher_detail_no, "item_code": row.item_code},
|
||||||
|
field,
|
||||||
|
)
|
||||||
if key is None:
|
if key is None:
|
||||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||||
|
|
||||||
if row.voucher_type == "Delivery Note":
|
if row.voucher_type == "Delivery Note":
|
||||||
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
||||||
elif row.voucher_type == "Sales Invoice":
|
elif row.voucher_type == "Sales Invoice":
|
||||||
|
|||||||
@@ -333,9 +333,10 @@ class SellingController(StockController):
|
|||||||
if is_internal_customer or not is_stock_item:
|
if is_internal_customer or not is_stock_item:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if item.get("incoming_rate") and item.base_net_rate < (
|
rate_field = "valuation_rate" if self.doctype in ["Sales Order", "Quotation"] else "incoming_rate"
|
||||||
|
if item.get(rate_field) and item.base_net_rate < (
|
||||||
valuation_rate := flt(
|
valuation_rate := flt(
|
||||||
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
item.get(rate_field) * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
throw_message(
|
throw_message(
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
if not self.get("is_return"):
|
if not self.get("is_return"):
|
||||||
self.validate_inspection()
|
self.validate_inspection()
|
||||||
|
|
||||||
|
self.validate_warehouse_of_sabb()
|
||||||
self.validate_serialized_batch()
|
self.validate_serialized_batch()
|
||||||
self.clean_serial_nos()
|
self.clean_serial_nos()
|
||||||
self.validate_customer_provided_item()
|
self.validate_customer_provided_item()
|
||||||
@@ -75,6 +77,45 @@ class StockController(AccountsController):
|
|||||||
super().on_update()
|
super().on_update()
|
||||||
self.check_zero_rate()
|
self.check_zero_rate()
|
||||||
|
|
||||||
|
def validate_warehouse_of_sabb(self):
|
||||||
|
if self.is_internal_transfer():
|
||||||
|
return
|
||||||
|
|
||||||
|
doc_before_save = self.get_doc_before_save()
|
||||||
|
|
||||||
|
for row in self.items:
|
||||||
|
if not row.get("serial_and_batch_bundle"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
sabb_details = frappe.db.get_value(
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
row.serial_and_batch_bundle,
|
||||||
|
["type_of_transaction", "warehouse", "has_serial_no"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
if not sabb_details:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if sabb_details.type_of_transaction != "Outward":
|
||||||
|
continue
|
||||||
|
|
||||||
|
warehouse = row.get("warehouse") or row.get("s_warehouse")
|
||||||
|
if sabb_details.warehouse != warehouse:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}."
|
||||||
|
).format(row.idx, warehouse, sabb_details.warehouse, row.serial_and_batch_bundle)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.doctype == "Stock Reconciliation":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if sabb_details.has_serial_no and doc_before_save and doc_before_save.get("items"):
|
||||||
|
prev_row = doc_before_save.get("items", {"idx": row.idx})
|
||||||
|
if prev_row and prev_row[0].serial_and_batch_bundle != row.serial_and_batch_bundle:
|
||||||
|
sabb_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||||
|
sabb_doc.validate_serial_no_status()
|
||||||
|
|
||||||
def reset_conversion_factor(self):
|
def reset_conversion_factor(self):
|
||||||
for row in self.get("items"):
|
for row in self.get("items"):
|
||||||
if row.uom != row.stock_uom:
|
if row.uom != row.stock_uom:
|
||||||
@@ -2087,7 +2128,9 @@ def check_item_quality_inspection(doctype, items):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_quality_inspections(doctype, docname, items, inspection_type):
|
def make_quality_inspections(
|
||||||
|
company: str, doctype: str, docname: str, items: str | list, inspection_type: str
|
||||||
|
):
|
||||||
if isinstance(items, str):
|
if isinstance(items, str):
|
||||||
items = json.loads(items)
|
items = json.loads(items)
|
||||||
|
|
||||||
@@ -2106,6 +2149,7 @@ def make_quality_inspections(doctype, docname, items, inspection_type):
|
|||||||
|
|
||||||
quality_inspection = frappe.get_doc(
|
quality_inspection = frappe.get_doc(
|
||||||
{
|
{
|
||||||
|
"company": company,
|
||||||
"doctype": "Quality Inspection",
|
"doctype": "Quality Inspection",
|
||||||
"inspection_type": inspection_type,
|
"inspection_type": inspection_type,
|
||||||
"inspected_by": frappe.session.user,
|
"inspected_by": frappe.session.user,
|
||||||
|
|||||||
@@ -307,6 +307,21 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.frm.set_query("uom", "items", function (doc, cdt, cdn) {
|
||||||
|
let row = locals[cdt][cdn];
|
||||||
|
|
||||||
|
if (!row.item_code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.get_item_uom_query",
|
||||||
|
filters: {
|
||||||
|
item_code: row.item_code,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
me.frm.set_query("contact_person", erpnext.queries["contact_query"]);
|
me.frm.set_query("contact_person", erpnext.queries["contact_query"]);
|
||||||
|
|
||||||
if (me.frm.doc.opportunity_from == "Lead") {
|
if (me.frm.doc.opportunity_from == "Lead") {
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ def create_prospect_against_crm_deal():
|
|||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if doc.contacts and len(doc.contacts):
|
||||||
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
||||||
|
|
||||||
create_address("Prospect", prospect_name, doc.address)
|
create_address("Prospect", prospect_name, doc.address)
|
||||||
frappe.response["message"] = prospect_name
|
frappe.response["message"] = prospect_name
|
||||||
|
|
||||||
|
|||||||
21
erpnext/desktop_icon/organization.json
Normal file
21
erpnext/desktop_icon/organization.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"app": "erpnext",
|
||||||
|
"bg_color": "blue",
|
||||||
|
"creation": "2026-02-24 17:43:08.379896",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Desktop Icon",
|
||||||
|
"hidden": 0,
|
||||||
|
"icon_type": "Link",
|
||||||
|
"idx": 0,
|
||||||
|
"label": "Organization",
|
||||||
|
"link_to": "Organization",
|
||||||
|
"link_type": "Workspace Sidebar",
|
||||||
|
"modified": "2026-02-24 17:59:39.885360",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Organization",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"parent_icon": "",
|
||||||
|
"restrict_removal": 0,
|
||||||
|
"roles": [],
|
||||||
|
"standard": 1
|
||||||
|
}
|
||||||
@@ -219,6 +219,16 @@ website_route_rules = [
|
|||||||
{"from_route": "/tasks", "to_route": "Task"},
|
{"from_route": "/tasks", "to_route": "Task"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
standard_navbar_items = [
|
||||||
|
{
|
||||||
|
"item_label": "Clear Demo Data",
|
||||||
|
"item_type": "Action",
|
||||||
|
"action": "erpnext.demo.clear_demo();",
|
||||||
|
"is_standard": 1,
|
||||||
|
"condition": "eval: frappe.boot.sysdefaults.demo_company",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
standard_portal_menu_items = [
|
standard_portal_menu_items = [
|
||||||
{"title": "Projects", "route": "/project", "reference_doctype": "Project", "role": "Customer"},
|
{"title": "Projects", "route": "/project", "reference_doctype": "Project", "role": "Customer"},
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1321,9 +1321,9 @@ class JobCard(Document):
|
|||||||
|
|
||||||
def is_work_order_closed(self):
|
def is_work_order_closed(self):
|
||||||
if self.work_order:
|
if self.work_order:
|
||||||
status = frappe.get_value("Work Order", self.work_order)
|
status = frappe.get_value("Work Order", self.work_order, "status")
|
||||||
|
|
||||||
if status == "Closed":
|
if status in ["Closed", "Stopped"]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -225,7 +225,12 @@ class WorkOrder(Document):
|
|||||||
frappe.throw(_("Actual End Date cannot be before Actual Start Date"))
|
frappe.throw(_("Actual End Date cannot be before Actual Start Date"))
|
||||||
|
|
||||||
def validate_fg_warehouse_for_reservation(self):
|
def validate_fg_warehouse_for_reservation(self):
|
||||||
if self.reserve_stock and self.sales_order and not self.subcontracting_inward_order:
|
if (
|
||||||
|
self.reserve_stock
|
||||||
|
and self.sales_order
|
||||||
|
and not self.subcontracting_inward_order
|
||||||
|
and not self.production_plan_sub_assembly_item
|
||||||
|
):
|
||||||
warehouses = frappe.get_all(
|
warehouses = frappe.get_all(
|
||||||
"Sales Order Item",
|
"Sales Order Item",
|
||||||
filters={"parent": self.sales_order, "item_code": self.production_item},
|
filters={"parent": self.sales_order, "item_code": self.production_item},
|
||||||
@@ -413,39 +418,52 @@ class WorkOrder(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_sales_order(self):
|
def validate_sales_order(self):
|
||||||
|
if self.production_plan_sub_assembly_item:
|
||||||
|
return
|
||||||
|
|
||||||
if self.sales_order:
|
if self.sales_order:
|
||||||
self.check_sales_order_on_hold_or_close()
|
self.check_sales_order_on_hold_or_close()
|
||||||
so = frappe.db.sql(
|
|
||||||
"""
|
SalesOrder = frappe.qb.DocType("Sales Order")
|
||||||
select so.name, so_item.delivery_date, so.project
|
SalesOrderItem = frappe.qb.DocType("Sales Order Item")
|
||||||
from `tabSales Order` so
|
PackedItem = frappe.qb.DocType("Packed Item")
|
||||||
inner join `tabSales Order Item` so_item on so_item.parent = so.name
|
ProductBundleItem = frappe.qb.DocType("Product Bundle Item")
|
||||||
left join `tabProduct Bundle Item` pk_item on so_item.item_code = pk_item.parent
|
|
||||||
where so.name=%s and so.docstatus = 1
|
so = (
|
||||||
and so.skip_delivery_note = 0 and (
|
frappe.qb.from_(SalesOrder)
|
||||||
so_item.item_code=%s or
|
.inner_join(SalesOrderItem)
|
||||||
pk_item.item_code=%s )
|
.on(SalesOrderItem.parent == SalesOrder.name)
|
||||||
""",
|
.left_join(ProductBundleItem)
|
||||||
(self.sales_order, self.production_item, self.production_item),
|
.on(ProductBundleItem.parent == SalesOrderItem.item_code)
|
||||||
as_dict=1,
|
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
|
||||||
|
.where(
|
||||||
|
(SalesOrder.skip_delivery_note == 0)
|
||||||
|
& (SalesOrder.docstatus == 1)
|
||||||
|
& (SalesOrder.name == self.sales_order)
|
||||||
|
& (
|
||||||
|
(SalesOrderItem.item_code == self.production_item)
|
||||||
|
| (ProductBundleItem.item_code == self.production_item)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run(as_dict=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not so:
|
if not so:
|
||||||
so = frappe.db.sql(
|
so = (
|
||||||
"""
|
frappe.qb.from_(SalesOrder)
|
||||||
select
|
.inner_join(SalesOrderItem)
|
||||||
so.name, so_item.delivery_date, so.project
|
.on(SalesOrderItem.parent == SalesOrder.name)
|
||||||
from
|
.inner_join(PackedItem)
|
||||||
`tabSales Order` so, `tabSales Order Item` so_item, `tabPacked Item` packed_item
|
.on(PackedItem.parent == SalesOrder.name)
|
||||||
where so.name=%s
|
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
|
||||||
and so.name=so_item.parent
|
.where(
|
||||||
and so.name=packed_item.parent
|
(SalesOrder.name == self.sales_order)
|
||||||
and so.skip_delivery_note = 0
|
& (SalesOrder.skip_delivery_note == 0)
|
||||||
and so_item.item_code = packed_item.parent_item
|
& (SalesOrderItem.item_code == PackedItem.parent_item)
|
||||||
and so.docstatus = 1 and packed_item.item_code=%s
|
& (SalesOrder.docstatus == 1)
|
||||||
""",
|
& (PackedItem.item_code == self.production_item)
|
||||||
(self.sales_order, self.production_item),
|
)
|
||||||
as_dict=1,
|
.run(as_dict=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(so):
|
if len(so):
|
||||||
@@ -651,7 +669,7 @@ class WorkOrder(Document):
|
|||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
|
from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item
|
||||||
|
|
||||||
if self.sales_order and self.sales_order_item:
|
if self.sales_order and self.sales_order_item and not self.production_plan_sub_assembly_item:
|
||||||
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
|
update_produced_qty_in_so_item(self.sales_order, self.sales_order_item)
|
||||||
|
|
||||||
if self.production_plan:
|
if self.production_plan:
|
||||||
@@ -695,19 +713,25 @@ class WorkOrder(Document):
|
|||||||
self.db_set("disassembled_qty", self.disassembled_qty)
|
self.db_set("disassembled_qty", self.disassembled_qty)
|
||||||
|
|
||||||
def get_transferred_or_manufactured_qty(self, purpose, fieldname):
|
def get_transferred_or_manufactured_qty(self, purpose, fieldname):
|
||||||
table = frappe.qb.DocType("Stock Entry")
|
parent = frappe.qb.DocType("Stock Entry")
|
||||||
query = frappe.qb.from_(table).where(
|
|
||||||
(table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose)
|
query = frappe.qb.from_(parent).where(
|
||||||
|
(parent.work_order == self.name)
|
||||||
|
& (parent.docstatus == 1)
|
||||||
|
& (parent.purpose == purpose)
|
||||||
|
& (parent.is_additional_transfer_entry == cint(fieldname == "additional_transferred_qty"))
|
||||||
)
|
)
|
||||||
|
|
||||||
if purpose == "Manufacture":
|
if purpose == "Manufacture":
|
||||||
query = query.select(Sum(table.fg_completed_qty) - Sum(table.process_loss_qty))
|
child = frappe.qb.DocType("Stock Entry Detail")
|
||||||
else:
|
query = (
|
||||||
query = query.select(Sum(table.fg_completed_qty))
|
query.join(child)
|
||||||
|
.on(parent.name == child.parent)
|
||||||
query = query.where(
|
.select(Sum(child.transfer_qty))
|
||||||
table.is_additional_transfer_entry == cint(fieldname == "additional_transferred_qty")
|
.where(child.is_finished_item == 1)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
query = query.select(Sum(parent.fg_completed_qty))
|
||||||
|
|
||||||
return flt(query.run()[0][0])
|
return flt(query.run()[0][0])
|
||||||
|
|
||||||
@@ -1159,7 +1183,7 @@ class WorkOrder(Document):
|
|||||||
doc.db_set("status", doc.status)
|
doc.db_set("status", doc.status)
|
||||||
|
|
||||||
def update_work_order_qty_in_so(self):
|
def update_work_order_qty_in_so(self):
|
||||||
if not self.sales_order and not self.sales_order_item:
|
if (not self.sales_order and not self.sales_order_item) or self.production_plan_sub_assembly_item:
|
||||||
return
|
return
|
||||||
|
|
||||||
total_bundle_qty = 1
|
total_bundle_qty = 1
|
||||||
|
|||||||
@@ -469,3 +469,5 @@ erpnext.patches.v15_0.delete_quotation_lost_record_detail
|
|||||||
erpnext.patches.v16_0.add_portal_redirects
|
erpnext.patches.v16_0.add_portal_redirects
|
||||||
erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2
|
erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2
|
||||||
erpnext.patches.v16_0.migrate_asset_type_checkboxes_to_select
|
erpnext.patches.v16_0.migrate_asset_type_checkboxes_to_select
|
||||||
|
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
|
||||||
|
erpnext.patches.v16_0.enable_serial_batch_setting
|
||||||
|
|||||||
9
erpnext/patches/v16_0/enable_serial_batch_setting.py
Normal file
9
erpnext/patches/v16_0/enable_serial_batch_setting.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if not frappe.get_all("Serial No", limit=1) and not frappe.get_all("Batch", limit=1):
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "enable_serial_and_batch_no_for_item", 1)
|
||||||
|
frappe.db.set_default("enable_serial_and_batch_no_for_item", 1)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
PurchaseOrderItem = DocType("Purchase Order Item")
|
||||||
|
MaterialRequestItem = DocType("Material Request Item")
|
||||||
|
|
||||||
|
poi_query = (
|
||||||
|
frappe.qb.from_(PurchaseOrderItem)
|
||||||
|
.select(PurchaseOrderItem.sales_order_item, Sum(PurchaseOrderItem.stock_qty))
|
||||||
|
.where(PurchaseOrderItem.sales_order_item.isnotnull() & PurchaseOrderItem.docstatus == 1)
|
||||||
|
.groupby(PurchaseOrderItem.sales_order_item)
|
||||||
|
)
|
||||||
|
|
||||||
|
mri_query = (
|
||||||
|
frappe.qb.from_(MaterialRequestItem)
|
||||||
|
.select(MaterialRequestItem.sales_order_item, Sum(MaterialRequestItem.stock_qty))
|
||||||
|
.where(MaterialRequestItem.sales_order_item.isnotnull() & MaterialRequestItem.docstatus == 1)
|
||||||
|
.groupby(MaterialRequestItem.sales_order_item)
|
||||||
|
)
|
||||||
|
|
||||||
|
poi_data = poi_query.run()
|
||||||
|
mri_data = mri_query.run()
|
||||||
|
|
||||||
|
updates_against_poi = {data[0]: {"ordered_qty": data[1]} for data in poi_data}
|
||||||
|
updates_against_mri = {data[0]: {"requested_qty": data[1], "ordered_qty": 0} for data in mri_data}
|
||||||
|
|
||||||
|
frappe.db.auto_commit_on_many_writes = 1
|
||||||
|
frappe.db.bulk_update("Sales Order Item", updates_against_mri)
|
||||||
|
frappe.db.bulk_update("Sales Order Item", updates_against_poi)
|
||||||
|
frappe.db.auto_commit_on_many_writes = 0
|
||||||
@@ -205,7 +205,7 @@ frappe.ui.form.on("Project", {
|
|||||||
|
|
||||||
collect_progress: function (frm) {
|
collect_progress: function (frm) {
|
||||||
if (frm.doc.collect_progress && !frm.doc.subject) {
|
if (frm.doc.collect_progress && !frm.doc.subject) {
|
||||||
frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name]));
|
frm.set_value("subject", __("For project - {0}, update your status", [frm.doc.project_name]));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,29 +12,21 @@
|
|||||||
"project_name",
|
"project_name",
|
||||||
"status",
|
"status",
|
||||||
"project_type",
|
"project_type",
|
||||||
"is_active",
|
|
||||||
"percent_complete_method",
|
"percent_complete_method",
|
||||||
"percent_complete",
|
|
||||||
"column_break_5",
|
"column_break_5",
|
||||||
"project_template",
|
"project_template",
|
||||||
"expected_start_date",
|
|
||||||
"expected_end_date",
|
|
||||||
"priority",
|
"priority",
|
||||||
"department",
|
"department",
|
||||||
"customer_details",
|
"is_active",
|
||||||
"customer",
|
"percent_complete",
|
||||||
"column_break_14",
|
|
||||||
"sales_order",
|
|
||||||
"users_section",
|
|
||||||
"users",
|
|
||||||
"copied_from",
|
|
||||||
"section_break0",
|
|
||||||
"notes",
|
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
|
"expected_start_date",
|
||||||
"actual_start_date",
|
"actual_start_date",
|
||||||
"actual_time",
|
"actual_time",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
|
"expected_end_date",
|
||||||
"actual_end_date",
|
"actual_end_date",
|
||||||
|
"costing_tab",
|
||||||
"project_details",
|
"project_details",
|
||||||
"estimated_costing",
|
"estimated_costing",
|
||||||
"total_costing_amount",
|
"total_costing_amount",
|
||||||
@@ -50,7 +42,7 @@
|
|||||||
"gross_margin",
|
"gross_margin",
|
||||||
"column_break_37",
|
"column_break_37",
|
||||||
"per_gross_margin",
|
"per_gross_margin",
|
||||||
"monitor_progress",
|
"monitor_progress_tab",
|
||||||
"collect_progress",
|
"collect_progress",
|
||||||
"holiday_list",
|
"holiday_list",
|
||||||
"frequency",
|
"frequency",
|
||||||
@@ -63,7 +55,18 @@
|
|||||||
"weekly_time_to_send",
|
"weekly_time_to_send",
|
||||||
"column_break_45",
|
"column_break_45",
|
||||||
"subject",
|
"subject",
|
||||||
"message"
|
"message",
|
||||||
|
"more_info_tab",
|
||||||
|
"customer_details",
|
||||||
|
"customer",
|
||||||
|
"column_break_14",
|
||||||
|
"sales_order",
|
||||||
|
"users_section",
|
||||||
|
"users",
|
||||||
|
"copied_from",
|
||||||
|
"section_break0",
|
||||||
|
"notes",
|
||||||
|
"connections_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -231,7 +234,7 @@
|
|||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "section_break_18",
|
"fieldname": "section_break_18",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Start and End Dates"
|
"label": "Timeline"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "actual_start_date",
|
"fieldname": "actual_start_date",
|
||||||
@@ -258,7 +261,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "project_details",
|
"fieldname": "project_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Costing and Billing",
|
"label": "Costing and Billing",
|
||||||
@@ -329,7 +331,6 @@
|
|||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "margin",
|
"fieldname": "margin",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Margin",
|
"label": "Margin",
|
||||||
@@ -357,12 +358,6 @@
|
|||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "monitor_progress",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Monitor Progress"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "collect_progress",
|
"fieldname": "collect_progress",
|
||||||
@@ -455,6 +450,27 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Subject",
|
"label": "Subject",
|
||||||
"mandatory_depends_on": "collect_progress"
|
"mandatory_depends_on": "collect_progress"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "costing_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Costing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "monitor_progress_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Progress"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "more_info_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "More Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "connections_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Connections",
|
||||||
|
"show_dashboard": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-puzzle-piece",
|
"icon": "fa fa-puzzle-piece",
|
||||||
@@ -462,7 +478,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 4,
|
"max_attachments": 4,
|
||||||
"modified": "2025-08-21 17:57:58.314809",
|
"modified": "2026-03-04 11:09:55.253367",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Project",
|
"name": "Project",
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ frappe.ui.form.on("Project Template", {
|
|||||||
frappe.ui.form.on("Project Template Task", {
|
frappe.ui.form.on("Project Template Task", {
|
||||||
task: function (frm, cdt, cdn) {
|
task: function (frm, cdt, cdn) {
|
||||||
var row = locals[cdt][cdn];
|
var row = locals[cdt][cdn];
|
||||||
|
|
||||||
|
if (!row.task) {
|
||||||
|
row.subject = null;
|
||||||
|
refresh_field("tasks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
frappe.db.get_value("Task", row.task, "subject", (value) => {
|
frappe.db.get_value("Task", row.task, "subject", (value) => {
|
||||||
row.subject = value.subject;
|
row.subject = value.subject;
|
||||||
refresh_field("tasks");
|
refresh_field("tasks");
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"type",
|
"type",
|
||||||
"color",
|
"color",
|
||||||
"is_group",
|
"is_group",
|
||||||
"is_template",
|
|
||||||
"column_break0",
|
"column_break0",
|
||||||
"status",
|
"status",
|
||||||
"priority",
|
"priority",
|
||||||
@@ -21,17 +20,21 @@
|
|||||||
"parent_task",
|
"parent_task",
|
||||||
"completed_by",
|
"completed_by",
|
||||||
"completed_on",
|
"completed_on",
|
||||||
|
"section_break_dafi",
|
||||||
|
"is_template",
|
||||||
|
"column_break_vvfp",
|
||||||
|
"start",
|
||||||
|
"duration",
|
||||||
"sb_timeline",
|
"sb_timeline",
|
||||||
"exp_start_date",
|
"exp_start_date",
|
||||||
"expected_time",
|
"expected_time",
|
||||||
"start",
|
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"exp_end_date",
|
"exp_end_date",
|
||||||
"progress",
|
"progress",
|
||||||
"duration",
|
|
||||||
"is_milestone",
|
"is_milestone",
|
||||||
"sb_details",
|
"sb_details",
|
||||||
"description",
|
"description",
|
||||||
|
"dependencies_tab",
|
||||||
"sb_depends_on",
|
"sb_depends_on",
|
||||||
"depends_on",
|
"depends_on",
|
||||||
"depends_on_tasks",
|
"depends_on_tasks",
|
||||||
@@ -44,12 +47,13 @@
|
|||||||
"total_costing_amount",
|
"total_costing_amount",
|
||||||
"column_break_20",
|
"column_break_20",
|
||||||
"total_billing_amount",
|
"total_billing_amount",
|
||||||
|
"more_info_tab",
|
||||||
"sb_more_info",
|
"sb_more_info",
|
||||||
|
"company",
|
||||||
"review_date",
|
"review_date",
|
||||||
"closing_date",
|
"closing_date",
|
||||||
"column_break_22",
|
"column_break_22",
|
||||||
"department",
|
"department",
|
||||||
"company",
|
|
||||||
"lft",
|
"lft",
|
||||||
"rgt",
|
"rgt",
|
||||||
"old_parent",
|
"old_parent",
|
||||||
@@ -78,7 +82,6 @@
|
|||||||
"oldfieldname": "project",
|
"oldfieldname": "project",
|
||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Project",
|
"options": "Project",
|
||||||
"remember_last_selected_value": 1,
|
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -218,7 +221,6 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "sb_depends_on",
|
"fieldname": "sb_depends_on",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Dependencies",
|
|
||||||
"oldfieldtype": "Section Break"
|
"oldfieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -298,10 +300,9 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "sb_more_info",
|
"fieldname": "sb_more_info",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "More Info"
|
"label": "Additional Info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.status == \"Closed\" || doc.status == \"Pending Review\"",
|
"depends_on": "eval:doc.status == \"Closed\" || doc.status == \"Pending Review\"",
|
||||||
@@ -334,8 +335,7 @@
|
|||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company"
|
||||||
"remember_last_selected_value": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "lft",
|
"fieldname": "lft",
|
||||||
@@ -368,6 +368,7 @@
|
|||||||
"options": "User"
|
"options": "User"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_in_quick_entry": 1,
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "is_template",
|
"fieldname": "is_template",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
@@ -397,6 +398,24 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Template Task"
|
"label": "Template Task"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dependencies_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Dependencies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "more_info_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "More Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_dafi",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vvfp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-check",
|
"icon": "fa fa-check",
|
||||||
@@ -404,11 +423,11 @@
|
|||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"max_attachments": 5,
|
"max_attachments": 5,
|
||||||
"modified": "2025-10-16 08:39:12.214577",
|
"modified": "2026-03-04 11:47:10.454548",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Task",
|
"name": "Task",
|
||||||
"naming_rule": "Expression (old style)",
|
"naming_rule": "Expression",
|
||||||
"nsm_parent_field": "parent_task",
|
"nsm_parent_field": "parent_task",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -425,6 +444,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"search_fields": "subject",
|
"search_fields": "subject",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"show_preview_popup": 1,
|
"show_preview_popup": 1,
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ class Task(NestedSet):
|
|||||||
def validate_status(self):
|
def validate_status(self):
|
||||||
if self.is_template and self.status != "Template":
|
if self.is_template and self.status != "Template":
|
||||||
self.status = "Template"
|
self.status = "Template"
|
||||||
|
if self.status == "Template" and not self.is_template:
|
||||||
|
self.status = "Open"
|
||||||
if self.status != self.get_db_value("status") and self.status == "Completed":
|
if self.status != self.get_db_value("status") and self.status == "Completed":
|
||||||
for d in self.depends_on:
|
for d in self.depends_on:
|
||||||
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
|
if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
|
||||||
|
|||||||
@@ -260,6 +260,33 @@ frappe.ui.form.on("Timesheet", {
|
|||||||
parent_project: function (frm) {
|
parent_project: function (frm) {
|
||||||
set_project_in_timelog(frm);
|
set_project_in_timelog(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
employee: function (frm) {
|
||||||
|
if (frm.doc.employee && frm.doc.time_logs) {
|
||||||
|
const selected_employee = frm.doc.employee;
|
||||||
|
frm.doc.time_logs.forEach((row) => {
|
||||||
|
if (row.activity_type) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
|
||||||
|
args: {
|
||||||
|
employee: frm.doc.employee,
|
||||||
|
activity_type: row.activity_type,
|
||||||
|
currency: frm.doc.currency,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
if (selected_employee !== frm.doc.employee) return;
|
||||||
|
row.billing_rate = r.message["billing_rate"];
|
||||||
|
row.costing_rate = r.message["costing_rate"];
|
||||||
|
frm.refresh_fields("time_logs");
|
||||||
|
calculate_billing_costing_amount(frm, row.doctype, row.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Timesheet Detail", {
|
frappe.ui.form.on("Timesheet Detail", {
|
||||||
|
|||||||
@@ -18,28 +18,29 @@
|
|||||||
"column_break_3",
|
"column_break_3",
|
||||||
"status",
|
"status",
|
||||||
"parent_project",
|
"parent_project",
|
||||||
"employee_detail",
|
|
||||||
"employee",
|
|
||||||
"employee_name",
|
|
||||||
"department",
|
|
||||||
"column_break_9",
|
|
||||||
"user",
|
"user",
|
||||||
"start_date",
|
"start_date",
|
||||||
"end_date",
|
"end_date",
|
||||||
|
"employee_detail",
|
||||||
|
"employee",
|
||||||
|
"department",
|
||||||
|
"column_break_9",
|
||||||
|
"employee_name",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"time_logs",
|
"time_logs",
|
||||||
"working_hours",
|
"working_hours",
|
||||||
"total_hours",
|
"total_hours",
|
||||||
|
"billing_tab",
|
||||||
"billing_details",
|
"billing_details",
|
||||||
"total_billable_hours",
|
"total_billable_hours",
|
||||||
|
"total_billable_amount",
|
||||||
|
"total_costing_amount",
|
||||||
"base_total_billable_amount",
|
"base_total_billable_amount",
|
||||||
"base_total_billed_amount",
|
|
||||||
"base_total_costing_amount",
|
"base_total_costing_amount",
|
||||||
"column_break_10",
|
"column_break_10",
|
||||||
"total_billed_hours",
|
"total_billed_hours",
|
||||||
"total_billable_amount",
|
|
||||||
"total_billed_amount",
|
"total_billed_amount",
|
||||||
"total_costing_amount",
|
"base_total_billed_amount",
|
||||||
"per_billed",
|
"per_billed",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
"note",
|
"note",
|
||||||
@@ -176,7 +177,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "billing_details",
|
"fieldname": "billing_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Billing Details",
|
"label": "Billing Details",
|
||||||
@@ -304,13 +304,18 @@
|
|||||||
"fieldname": "exchange_rate",
|
"fieldname": "exchange_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Exchange Rate"
|
"label": "Exchange Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "billing_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Billing"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-clock-o",
|
"icon": "fa fa-clock-o",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-19 13:48:23.453636",
|
"modified": "2026-03-04 11:56:51.438298",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Timesheet",
|
"name": "Timesheet",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M38.5714 0H15.4286C6.90761 0 0 6.90761 0 15.4286V38.5714C0 47.0924 6.90761 54 15.4286 54H38.5714C47.0924 54 54 47.0924 54 38.5714V15.4286C54 6.90761 47.0924 0 38.5714 0Z" fill="#0289F7"/>
|
||||||
|
<path d="M19.2857 15.4286H22.1786C23.7763 15.4286 25.0714 16.7237 25.0714 18.3214V24.1071C25.0714 25.7048 23.7763 27 22.1786 27H19.2857V38.5714H15.4286V27H11.5714V23.1428H21.2143V19.2857H11.5714V15.4286H15.4286V11.5714H19.2857V15.4286ZM38.5714 38.5714H34.7143V34.7143H31.8214C30.2238 34.7143 28.9286 33.4191 28.9286 31.8214V26.0357C28.9286 24.438 30.2238 23.1428 31.8214 23.1428H34.7143V11.5714H38.5714V23.1428H42.4286V27H32.7857V30.8571H42.4286V34.7143H38.5714V38.5714Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 787 B |
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 0H8C3.58172 0 0 3.58172 0 8V20C0 24.4183 3.58172 28 8 28H20C24.4183 28 28 24.4183 28 20V8C28 3.58172 24.4183 0 20 0Z" fill="#0289F7"/>
|
||||||
|
<path d="M20.5 13.25C20.5 13.0926 20.5 13 20.5 13L18.5 11.499V19.5H20C20.2761 19.5 20 19.5 20.5 19.5V13.25ZM14.5 14V16H10.5V14H14.5ZM22.5 19C22.5 20.3807 21.3807 21.5 20 21.5H16.5V19.5V7.5C16.5 7.5 16.2761 7.5 16 7.5H9C8.72386 7.5 9 7.5 8.5 7.5V19.5C9 19.5 8.72386 19.5 9 19.5H12.5V21.5H9C7.61929 21.5 6.5 20.3807 6.5 19V8C6.5 6.61929 7.61929 5.5 9 5.5H16C17.3807 5.5 18.5 6.61929 18.5 8V9L21.5 11.25C22.1295 11.7221 22.5 12.4631 22.5 13.25V19Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 713 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M38.5714 0H15.4286C6.90761 0 0 6.90761 0 15.4286V38.5714C0 47.0924 6.90761 54 15.4286 54H38.5714C47.0924 54 54 47.0924 54 38.5714V15.4286C54 6.90761 47.0924 0 38.5714 0Z" fill="#0289F7" fill-opacity="0.1"/>
|
||||||
|
<path d="M19.2857 15.4286H22.1786C23.7762 15.4286 25.0714 16.7238 25.0714 18.3215V24.1072C25.0714 25.7049 23.7762 27 22.1786 27H19.2857V38.5715H15.4286V27H11.5714V23.1429H21.2143V19.2858H11.5714V15.4286H15.4286V11.5715H19.2857V15.4286ZM38.5714 38.5715H34.7143V34.7143H31.8214C30.2237 34.7143 28.9286 33.4192 28.9286 31.8215V26.0358C28.9286 24.4381 30.2237 23.1429 31.8214 23.1429H34.7143V11.5715H38.5714V23.1429H42.4286V27H32.7857V30.8572H42.4286V34.7143H38.5714V38.5715Z" fill="#0981E3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 809 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 0H8C3.58172 0 0 3.58172 0 8V20C0 24.4183 3.58172 28 8 28H20C24.4183 28 28 24.4183 28 20V8C28 3.58172 24.4183 0 20 0Z" fill="#0289F7" fill-opacity="0.1"/>
|
||||||
|
<path d="M20.5 13.25C20.5 13.0926 20.5 13 20.5 13L18.5 11.499V19.5H20C20.2761 19.5 20 19.5 20.5 19.5V13.25ZM14.5 14V16H10.5V14H14.5ZM22.5 19C22.5 20.3807 21.3807 21.5 20 21.5H16.5V19.5V7.5C16.5 7.5 16.2761 7.5 16 7.5H9C8.72386 7.5 9 7.5 8.5 7.5V19.5C9 19.5 8.72386 19.5 9 19.5H12.5V21.5H9C7.61929 21.5 6.5 20.3807 6.5 19V8C6.5 6.61929 7.61929 5.5 9 5.5H16C17.3807 5.5 18.5 6.61929 18.5 8V9L21.5 11.25C22.1295 11.7221 22.5 12.4631 22.5 13.25V19Z" fill="#0981E3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 733 B |
@@ -580,6 +580,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
this.validate_has_items();
|
this.validate_has_items();
|
||||||
erpnext.utils.view_serial_batch_nos(this.frm);
|
erpnext.utils.view_serial_batch_nos(this.frm);
|
||||||
this.set_route_options_for_new_doc();
|
this.set_route_options_for_new_doc();
|
||||||
|
erpnext.toggle_serial_batch_fields(this.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
set_route_options_for_new_doc() {
|
set_route_options_for_new_doc() {
|
||||||
@@ -1307,6 +1308,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
if (this.frm.doc.transaction_date) {
|
if (this.frm.doc.transaction_date) {
|
||||||
this.frm.transaction_date = this.frm.doc.transaction_date;
|
this.frm.transaction_date = this.frm.doc.transaction_date;
|
||||||
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
|
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
|
||||||
|
this.recalculate_terms();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2966,6 +2968,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||||
args: {
|
args: {
|
||||||
|
company: me.frm.doc.company,
|
||||||
doctype: me.frm.doc.doctype,
|
doctype: me.frm.doc.doctype,
|
||||||
docname: me.frm.doc.name,
|
docname: me.frm.doc.name,
|
||||||
items: selected_data,
|
items: selected_data,
|
||||||
|
|||||||
@@ -19,6 +19,71 @@ $.extend(erpnext, {
|
|||||||
return currency_list;
|
return currency_list;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggle_serial_batch_fields(frm) {
|
||||||
|
let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0;
|
||||||
|
let fields = ["serial_and_batch_bundle", "use_serial_batch_fields", "serial_no", "batch_no"];
|
||||||
|
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"Stock Entry",
|
||||||
|
"Purchase Receipt",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Stock Reconciliation",
|
||||||
|
"Subcontracting Receipt",
|
||||||
|
].includes(frm.doc.doctype)
|
||||||
|
) {
|
||||||
|
fields.push("add_serial_batch_bundle");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["Stock Reconciliation"].includes(frm.doc.doctype)) {
|
||||||
|
fields.push("reconcile_all_serial_batch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["Sales Invoice", "Delivery Note", "Pick List"].includes(frm.doc.doctype)) {
|
||||||
|
fields.push("pick_serial_and_batch");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"].includes(frm.doc.doctype)) {
|
||||||
|
fields.push("add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle");
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_name = "items";
|
||||||
|
if (frm.doc.doctype === "Pick List") {
|
||||||
|
child_name = "locations";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.doc.doctype === "Asset Capitalization") {
|
||||||
|
child_name = "stock_items";
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
frm.fields_dict[child_name].grid.update_docfield_property(field, "hidden", hide_fields);
|
||||||
|
|
||||||
|
frm.fields_dict[child_name].grid.update_docfield_property(
|
||||||
|
field,
|
||||||
|
"in_list_view",
|
||||||
|
hide_fields ? 0 : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
frm.doc.doctype === "Subcontracting Receipt" &&
|
||||||
|
!["add_serial_batch_for_rejected_qty", "rejected_serial_and_batch_bundle"].includes(field)
|
||||||
|
) {
|
||||||
|
frm.fields_dict["supplied_items"].grid.update_docfield_property(field, "hidden", hide_fields);
|
||||||
|
|
||||||
|
frm.fields_dict["supplied_items"].grid.update_docfield_property(
|
||||||
|
field,
|
||||||
|
"in_list_view",
|
||||||
|
hide_fields ? 0 : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
frm.fields_dict["supplied_items"].grid.reset_grid();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.fields_dict[child_name].grid.reset_grid();
|
||||||
|
},
|
||||||
|
|
||||||
toggle_naming_series: function () {
|
toggle_naming_series: function () {
|
||||||
if (
|
if (
|
||||||
cur_frm &&
|
cur_frm &&
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import flt, getdate, nowdate
|
from frappe.utils import cint, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
|
||||||
@@ -446,6 +446,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
|
|||||||
child_filter = d.name in filtered_items if filtered_items else True
|
child_filter = d.name in filtered_items if filtered_items else True
|
||||||
return child_filter
|
return child_filter
|
||||||
|
|
||||||
|
automatically_fetch_payment_terms = cint(
|
||||||
|
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
||||||
|
)
|
||||||
|
|
||||||
doclist = get_mapped_doc(
|
doclist = get_mapped_doc(
|
||||||
"Quotation",
|
"Quotation",
|
||||||
source_name,
|
source_name,
|
||||||
@@ -453,6 +457,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
|
|||||||
"Quotation": {
|
"Quotation": {
|
||||||
"doctype": "Sales Order",
|
"doctype": "Sales Order",
|
||||||
"validation": {"docstatus": ["=", 1]},
|
"validation": {"docstatus": ["=", 1]},
|
||||||
|
"field_no_map": ["payment_terms_template"],
|
||||||
},
|
},
|
||||||
"Quotation Item": {
|
"Quotation Item": {
|
||||||
"doctype": "Sales Order Item",
|
"doctype": "Sales Order Item",
|
||||||
@@ -462,13 +467,15 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
|
|||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||||
"Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True},
|
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
set_missing_values,
|
set_missing_values,
|
||||||
ignore_permissions=ignore_permissions,
|
ignore_permissions=ignore_permissions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if automatically_fetch_payment_terms:
|
||||||
|
doclist.set_payment_schedule()
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,22 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
qo.payment_schedule[0].due_date = add_days(qo.transaction_date, -2)
|
qo.payment_schedule[0].due_date = add_days(qo.transaction_date, -2)
|
||||||
self.assertRaises(frappe.ValidationError, qo.save)
|
self.assertRaises(frappe.ValidationError, qo.save)
|
||||||
|
|
||||||
def test_update_child_disallow_rate_change(self):
|
def test_update_child_rate_change(self):
|
||||||
qo = make_quotation(qty=4)
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
item_1 = make_item("_Test Item")
|
||||||
|
item_2 = make_item("_Test Item 1")
|
||||||
|
|
||||||
|
item_list = [
|
||||||
|
{"item_code": item_1.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 300},
|
||||||
|
{"item_code": item_2.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 5, "rate": 400},
|
||||||
|
]
|
||||||
|
|
||||||
|
qo = make_quotation(item_list=item_list)
|
||||||
|
so = make_sales_order(qo.name, args={"filtered_children": [qo.items[0].name]})
|
||||||
|
so.delivery_date = nowdate()
|
||||||
|
so.submit()
|
||||||
|
qo.reload()
|
||||||
trans_item = json.dumps(
|
trans_item = json.dumps(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -68,10 +82,35 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
"rate": 5000,
|
"rate": 5000,
|
||||||
"qty": qo.items[0].qty,
|
"qty": qo.items[0].qty,
|
||||||
"docname": qo.items[0].name,
|
"docname": qo.items[0].name,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"item_code": qo.items[1].item_code,
|
||||||
|
"rate": qo.items[1].rate,
|
||||||
|
"qty": qo.items[1].qty,
|
||||||
|
"docname": qo.items[1].name,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate, "Quotation", trans_item, qo.name)
|
||||||
|
trans_item = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_code": qo.items[0].item_code,
|
||||||
|
"rate": qo.items[0].rate,
|
||||||
|
"qty": qo.items[0].qty,
|
||||||
|
"docname": qo.items[0].name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code": qo.items[1].item_code,
|
||||||
|
"rate": 50,
|
||||||
|
"qty": qo.items[1].qty,
|
||||||
|
"docname": qo.items[1].name,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
update_child_qty_rate("Quotation", trans_item, qo.name)
|
||||||
|
qo.reload()
|
||||||
|
self.assertEqual(qo.items[1].rate, 50)
|
||||||
|
|
||||||
def test_update_child_removing_item(self):
|
def test_update_child_removing_item(self):
|
||||||
qo = make_quotation(qty=10)
|
qo = make_quotation(qty=10)
|
||||||
@@ -143,6 +182,10 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
|
|
||||||
self.assertTrue(quotation.payment_schedule)
|
self.assertTrue(quotation.payment_schedule)
|
||||||
|
|
||||||
|
@IntegrationTestCase.change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"automatically_fetch_payment_terms": 1},
|
||||||
|
)
|
||||||
def test_make_sales_order_terms_copied(self):
|
def test_make_sales_order_terms_copied(self):
|
||||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
|
||||||
@@ -285,7 +328,11 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
|
|
||||||
@IntegrationTestCase.change_settings(
|
@IntegrationTestCase.change_settings(
|
||||||
"Accounts Settings",
|
"Accounts Settings",
|
||||||
{"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0},
|
{
|
||||||
|
"add_taxes_from_item_tax_template": 0,
|
||||||
|
"add_taxes_from_taxes_and_charges_template": 0,
|
||||||
|
"automatically_fetch_payment_terms": 1,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
def test_make_sales_order_with_terms(self):
|
def test_make_sales_order_with_terms(self):
|
||||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
@@ -323,10 +370,13 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
sales_order.save()
|
sales_order.save()
|
||||||
|
|
||||||
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
|
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
|
||||||
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
|
self.assertEqual(
|
||||||
|
getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date)
|
||||||
|
)
|
||||||
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
|
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
|
getdate(sales_order.payment_schedule[1].due_date),
|
||||||
|
getdate(add_days(quotation.transaction_date, 30)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_valid_till_before_transaction_date(self):
|
def test_valid_till_before_transaction_date(self):
|
||||||
@@ -1026,6 +1076,56 @@ class TestQuotation(IntegrationTestCase):
|
|||||||
quotation.reload()
|
quotation.reload()
|
||||||
self.assertEqual(quotation.status, "Open")
|
self.assertEqual(quotation.status, "Open")
|
||||||
|
|
||||||
|
@IntegrationTestCase.change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"automatically_fetch_payment_terms": 1},
|
||||||
|
)
|
||||||
|
def test_make_sales_order_with_payment_terms(self):
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
|
||||||
|
template = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template",
|
||||||
|
"template_name": "_Test Payment Terms Template for Quotation",
|
||||||
|
"terms": [
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template Detail",
|
||||||
|
"invoice_portion": 50.00,
|
||||||
|
"credit_days_based_on": "Day(s) after invoice date",
|
||||||
|
"credit_days": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doctype": "Payment Terms Template Detail",
|
||||||
|
"invoice_portion": 50.00,
|
||||||
|
"credit_days_based_on": "Day(s) after invoice date",
|
||||||
|
"credit_days": 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
).save()
|
||||||
|
|
||||||
|
quotation = make_quotation(qty=10, rate=1000, do_not_submit=1)
|
||||||
|
quotation.transaction_date = add_days(nowdate(), -2)
|
||||||
|
quotation.valid_till = add_days(nowdate(), 10)
|
||||||
|
quotation.update({"payment_terms_template": template.name, "payment_schedule": []})
|
||||||
|
quotation.save()
|
||||||
|
quotation.submit()
|
||||||
|
|
||||||
|
self.assertEqual(quotation.payment_schedule[0].payment_amount, 5000)
|
||||||
|
self.assertEqual(quotation.payment_schedule[1].payment_amount, 5000)
|
||||||
|
self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date)
|
||||||
|
self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 10))
|
||||||
|
|
||||||
|
sales_order = make_sales_order(quotation.name)
|
||||||
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.delivery_date = nowdate()
|
||||||
|
sales_order.save()
|
||||||
|
|
||||||
|
self.assertEqual(sales_order.payment_schedule[0].due_date, sales_order.transaction_date)
|
||||||
|
self.assertEqual(sales_order.payment_schedule[1].due_date, add_days(sales_order.transaction_date, 10))
|
||||||
|
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 5000)
|
||||||
|
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 5000)
|
||||||
|
|
||||||
|
|
||||||
def enable_calculate_bundle_price(enable=1):
|
def enable_calculate_bundle_price(enable=1):
|
||||||
selling_settings = frappe.get_doc("Selling Settings")
|
selling_settings = frappe.get_doc("Selling Settings")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"column_break_7",
|
"column_break_7",
|
||||||
"order_type",
|
"order_type",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"transaction_time",
|
||||||
"delivery_date",
|
"delivery_date",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"tax_id",
|
"tax_id",
|
||||||
@@ -122,6 +123,7 @@
|
|||||||
"company_contact_person",
|
"company_contact_person",
|
||||||
"payment_schedule_section",
|
"payment_schedule_section",
|
||||||
"payment_terms_section",
|
"payment_terms_section",
|
||||||
|
"ignore_default_payment_terms_template",
|
||||||
"payment_terms_template",
|
"payment_terms_template",
|
||||||
"payment_schedule",
|
"payment_schedule",
|
||||||
"terms_section_break",
|
"terms_section_break",
|
||||||
@@ -1724,6 +1726,22 @@
|
|||||||
"fieldname": "utm_analytics_section",
|
"fieldname": "utm_analytics_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "UTM Analytics"
|
"label": "UTM Analytics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Now",
|
||||||
|
"depends_on": "is_internal_customer",
|
||||||
|
"fieldname": "transaction_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"label": "Time",
|
||||||
|
"mandatory_depends_on": "is_internal_customer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "ignore_default_payment_terms_template",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Ignore Default Payment Terms Template",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -1731,7 +1749,7 @@
|
|||||||
"idx": 105,
|
"idx": 105,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-10 11:55:52.796522",
|
"modified": "2026-03-04 18:04:05.873483",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ class SalesOrder(SellingController):
|
|||||||
grand_total: DF.Currency
|
grand_total: DF.Currency
|
||||||
group_same_items: DF.Check
|
group_same_items: DF.Check
|
||||||
has_unit_price_items: DF.Check
|
has_unit_price_items: DF.Check
|
||||||
|
ignore_default_payment_terms_template: DF.Check
|
||||||
ignore_pricing_rule: DF.Check
|
ignore_pricing_rule: DF.Check
|
||||||
in_words: DF.Data | None
|
in_words: DF.Data | None
|
||||||
incoterm: DF.Link | None
|
incoterm: DF.Link | None
|
||||||
@@ -186,6 +187,7 @@ class SalesOrder(SellingController):
|
|||||||
total_qty: DF.Float
|
total_qty: DF.Float
|
||||||
total_taxes_and_charges: DF.Currency
|
total_taxes_and_charges: DF.Currency
|
||||||
transaction_date: DF.Date
|
transaction_date: DF.Date
|
||||||
|
transaction_time: DF.Time | None
|
||||||
utm_campaign: DF.Link | None
|
utm_campaign: DF.Link | None
|
||||||
utm_content: DF.Data | None
|
utm_content: DF.Data | None
|
||||||
utm_medium: DF.Link | None
|
utm_medium: DF.Link | None
|
||||||
|
|||||||
@@ -2647,6 +2647,49 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
|
|||||||
si2 = make_sales_invoice(so.name)
|
si2 = make_sales_invoice(so.name)
|
||||||
self.assertEqual(si2.items[0].qty, 20)
|
self.assertEqual(si2.items[0].qty, 20)
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"validate_selling_price": 1})
|
||||||
|
def test_selling_price_validation_for_manufactured_item(self):
|
||||||
|
"""
|
||||||
|
Unit test to check the selling price validation for manufactured item, without last purchae rate in Item master.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
|
# create a FG Item and RM Item
|
||||||
|
rm_item = make_item(
|
||||||
|
"_Test RM Item for SO selling validation",
|
||||||
|
{"is_stock_item": 1, "valuation_rate": 100, "stock_uom": "Nos"},
|
||||||
|
).name
|
||||||
|
rm_warehouse = create_warehouse("_Test RM SPV Warehouse")
|
||||||
|
fg_item = make_item("_Test FG Item for SO selling validation", {"is_stock_item": 1}).name
|
||||||
|
fg_warehouse = create_warehouse("_Test FG SPV Warehouse")
|
||||||
|
|
||||||
|
# create BOM and inward entry for RM Item
|
||||||
|
bom_no = make_bom(item=fg_item, raw_materials=[rm_item]).name
|
||||||
|
make_stock_entry(item_code=rm_item, target=rm_warehouse, qty=10, rate=100)
|
||||||
|
|
||||||
|
# create a manufacture entry, so system won't update the last purchase rate in Item master.
|
||||||
|
se = make_stock_entry(item_code=fg_item, qty=10, purpose="Manufacture", do_not_save=True)
|
||||||
|
|
||||||
|
se.from_bom = 1
|
||||||
|
se.use_multi_level_bom = 1
|
||||||
|
se.bom_no = bom_no
|
||||||
|
se.fg_completed_qty = 1
|
||||||
|
se.from_warehouse = rm_warehouse
|
||||||
|
se.to_warehouse = fg_warehouse
|
||||||
|
|
||||||
|
se.get_items()
|
||||||
|
se.save()
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
# check valuation of FG Item
|
||||||
|
self.assertEqual(se.items[1].valuation_rate, 100)
|
||||||
|
|
||||||
|
# create a SO for FG Item with selling rate than valuation rate.
|
||||||
|
so = make_sales_order(item_code=fg_item, qty=10, rate=50, warehouse=fg_warehouse, do_not_save=1)
|
||||||
|
self.assertRaises(frappe.ValidationError, so.save)
|
||||||
|
|
||||||
|
|
||||||
def compare_payment_schedules(doc, doc1, doc2):
|
def compare_payment_schedules(doc, doc1, doc2):
|
||||||
for index, schedule in enumerate(doc1.get("payment_schedule")):
|
for index, schedule in enumerate(doc1.get("payment_schedule")):
|
||||||
|
|||||||
@@ -95,6 +95,7 @@
|
|||||||
"ordered_qty",
|
"ordered_qty",
|
||||||
"planned_qty",
|
"planned_qty",
|
||||||
"production_plan_qty",
|
"production_plan_qty",
|
||||||
|
"requested_qty",
|
||||||
"column_break_69",
|
"column_break_69",
|
||||||
"work_order_qty",
|
"work_order_qty",
|
||||||
"delivered_qty",
|
"delivered_qty",
|
||||||
@@ -1010,13 +1011,21 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Finished Good Qty",
|
"label": "Finished Good Qty",
|
||||||
"mandatory_depends_on": "eval:parent.is_subcontracted"
|
"mandatory_depends_on": "eval:parent.is_subcontracted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "requested_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Requested Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-20 16:39:00.200328",
|
"modified": "2026-02-21 16:39:00.200328",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class SalesOrderItem(Document):
|
|||||||
quotation_item: DF.Data | None
|
quotation_item: DF.Data | None
|
||||||
rate: DF.Currency
|
rate: DF.Currency
|
||||||
rate_with_margin: DF.Currency
|
rate_with_margin: DF.Currency
|
||||||
|
requested_qty: DF.Float
|
||||||
reserve_stock: DF.Check
|
reserve_stock: DF.Check
|
||||||
returned_qty: DF.Float
|
returned_qty: DF.Float
|
||||||
stock_qty: DF.Float
|
stock_qty: DF.Float
|
||||||
|
|||||||
@@ -329,7 +329,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-12 10:38:34.605126",
|
"modified": "2026-02-27 00:47:46.003305",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Selling Settings",
|
"name": "Selling Settings",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"allow_roles": [
|
||||||
|
{
|
||||||
|
"role": "System Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Accounts Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Manufacturing Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Stock Manager"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"creation": "2026-02-24 18:03:53.158438",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Module Onboarding",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"modified": "2026-02-24 18:07:36.808560",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Setup",
|
||||||
|
"name": "Organization Onboarding",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": "Setup Company"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Invite Users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Setup Email Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Setup Role Permissions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": "Review System Settings"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Setup Organization"
|
||||||
|
}
|
||||||
20
erpnext/setup/onboarding_step/invite_users/invite_users.json
Normal file
20
erpnext/setup/onboarding_step/invite_users/invite_users.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"action": "Create Entry",
|
||||||
|
"action_label": "Invite Users",
|
||||||
|
"creation": "2026-02-24 18:04:21.585575",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2026-02-24 18:04:21.585575",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Invite Users",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "User",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Invite Users",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"action": "Update Settings",
|
||||||
|
"action_label": "Review System Settings",
|
||||||
|
"creation": "2026-02-24 18:06:56.781335",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 1,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2026-02-24 18:06:56.781335",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Review System Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "System Settings",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Review System Settings",
|
||||||
|
"validate_action": 0,
|
||||||
|
"value_to_validate": ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"action": "Go to Page",
|
||||||
|
"action_label": "Setup Company",
|
||||||
|
"creation": "2026-02-20 11:12:50.373049",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 1,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2026-02-23 21:10:17.680053",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Setup Company",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"path": "company",
|
||||||
|
"reference_document": "Company",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Setup Company",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"action": "Create Entry",
|
||||||
|
"action_label": "Setup Email Account",
|
||||||
|
"creation": "2026-02-24 18:04:39.983155",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2026-02-24 18:04:39.983155",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Setup Email Account",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"reference_document": "Email Account",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Setup Email Account",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"action": "Go to Page",
|
||||||
|
"action_label": "Setup Role Permissions",
|
||||||
|
"creation": "2026-02-24 18:05:10.485778",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Onboarding Step",
|
||||||
|
"idx": 0,
|
||||||
|
"is_complete": 0,
|
||||||
|
"is_single": 0,
|
||||||
|
"is_skipped": 0,
|
||||||
|
"modified": "2026-02-24 18:05:10.485778",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Setup Role Permissions",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"path": "permission-manager",
|
||||||
|
"show_form_tour": 0,
|
||||||
|
"show_full_form": 0,
|
||||||
|
"title": "Setup Role Permissions",
|
||||||
|
"validate_action": 1
|
||||||
|
}
|
||||||
@@ -221,6 +221,8 @@ def set_defaults_for_tests():
|
|||||||
frappe.db.set_default(key, value)
|
frappe.db.set_default(key, value)
|
||||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "enable_serial_and_batch_no_for_item", 1)
|
||||||
|
|
||||||
|
|
||||||
def insert_record(records):
|
def insert_record(records):
|
||||||
from frappe.desk.page.setup_wizard.setup_wizard import make_records
|
from frappe.desk.page.setup_wizard.setup_wizard import make_records
|
||||||
|
|||||||
@@ -84,7 +84,25 @@ frappe.ui.form.on("Item", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggle_has_serial_batch_fields(frm) {
|
||||||
|
let hide_fields = cint(frappe.user_defaults?.enable_serial_and_batch_no_for_item) === 0 ? 1 : 0;
|
||||||
|
|
||||||
|
frm.toggle_display(["serial_no_series", "batch_number_series", "create_new_batch"], !hide_fields);
|
||||||
|
frm.toggle_enable(["has_serial_no", "has_batch_no"], !hide_fields);
|
||||||
|
|
||||||
|
if (hide_fields) {
|
||||||
|
let description = __(
|
||||||
|
"To enable the Serial No and Batch No feature, please check the 'Enable Serial / Batch No for Item' checkbox in Stock Settings."
|
||||||
|
);
|
||||||
|
|
||||||
|
frm.set_df_property("has_serial_no", "description", description);
|
||||||
|
frm.set_df_property("has_batch_no", "description", description);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
frm.trigger("toggle_has_serial_batch_fields");
|
||||||
|
|
||||||
if (frm.doc.is_stock_item) {
|
if (frm.doc.is_stock_item) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Stock Balance"),
|
__("Stock Balance"),
|
||||||
|
|||||||
@@ -452,6 +452,7 @@
|
|||||||
"fieldname": "batch_number_series",
|
"fieldname": "batch_number_series",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Batch Number Series",
|
"label": "Batch Number Series",
|
||||||
|
"show_description_on_click": 1,
|
||||||
"translatable": 1
|
"translatable": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -493,7 +494,8 @@
|
|||||||
"description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.",
|
"description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.",
|
||||||
"fieldname": "serial_no_series",
|
"fieldname": "serial_no_series",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Serial Number Series"
|
"label": "Serial Number Series",
|
||||||
|
"show_description_on_click": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
@@ -985,7 +987,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2026-02-05 17:20:35.605734",
|
"modified": "2026-03-05 16:29:31.653447",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ class Item(Document):
|
|||||||
self.validate_auto_reorder_enabled_in_stock_settings()
|
self.validate_auto_reorder_enabled_in_stock_settings()
|
||||||
self.cant_change()
|
self.cant_change()
|
||||||
self.validate_item_tax_net_rate_range()
|
self.validate_item_tax_net_rate_range()
|
||||||
|
self.validate_allow_to_set_serial_batch()
|
||||||
|
|
||||||
if not self.is_new():
|
if not self.is_new():
|
||||||
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
|
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
|
||||||
@@ -226,6 +227,18 @@ class Item(Document):
|
|||||||
self.update_variants()
|
self.update_variants()
|
||||||
self.update_item_price()
|
self.update_item_price()
|
||||||
|
|
||||||
|
def validate_allow_to_set_serial_batch(self):
|
||||||
|
if not self.has_serial_no and not self.has_batch_no:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.db.get_single_value("Stock Settings", "enable_serial_and_batch_no_for_item"):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to set Serial No or Batch No for the item."
|
||||||
|
).format(get_link_to_form("Stock Settings", "Stock Settings")),
|
||||||
|
title=_("Serial and Batch No for Item Disabled"),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_description(self):
|
def validate_description(self):
|
||||||
"""Clean HTML description if set"""
|
"""Clean HTML description if set"""
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
|||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
|
||||||
|
class IncorrectCompanyValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LandedCostVoucher(Document):
|
class LandedCostVoucher(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
@@ -75,6 +79,7 @@ class LandedCostVoucher(Document):
|
|||||||
self.check_mandatory()
|
self.check_mandatory()
|
||||||
self.validate_receipt_documents()
|
self.validate_receipt_documents()
|
||||||
self.validate_line_items()
|
self.validate_line_items()
|
||||||
|
self.validate_expense_accounts()
|
||||||
init_landed_taxes_and_totals(self)
|
init_landed_taxes_and_totals(self)
|
||||||
self.set_total_taxes_and_charges()
|
self.set_total_taxes_and_charges()
|
||||||
if not self.get("items"):
|
if not self.get("items"):
|
||||||
@@ -116,11 +121,28 @@ class LandedCostVoucher(Document):
|
|||||||
receipt_documents = []
|
receipt_documents = []
|
||||||
|
|
||||||
for d in self.get("purchase_receipts"):
|
for d in self.get("purchase_receipts"):
|
||||||
docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
|
docstatus, company = frappe.get_cached_value(
|
||||||
|
d.receipt_document_type, d.receipt_document, ["docstatus", "company"]
|
||||||
|
)
|
||||||
if docstatus != 1:
|
if docstatus != 1:
|
||||||
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
|
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
|
||||||
frappe.throw(_(msg), title=_("Invalid Document"))
|
frappe.throw(_(msg), title=_("Invalid Document"))
|
||||||
|
|
||||||
|
if company != self.company:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row {0}: {1} {2} is linked to company {3}. Please select a document belonging to company {4}."
|
||||||
|
).format(
|
||||||
|
d.idx,
|
||||||
|
d.receipt_document_type,
|
||||||
|
frappe.bold(d.receipt_document),
|
||||||
|
frappe.bold(company),
|
||||||
|
frappe.bold(self.company),
|
||||||
|
),
|
||||||
|
title=_("Incorrect Company"),
|
||||||
|
exc=IncorrectCompanyValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
if d.receipt_document_type == "Purchase Invoice":
|
if d.receipt_document_type == "Purchase Invoice":
|
||||||
update_stock = frappe.db.get_value(
|
update_stock = frappe.db.get_value(
|
||||||
d.receipt_document_type, d.receipt_document, "update_stock"
|
d.receipt_document_type, d.receipt_document, "update_stock"
|
||||||
@@ -152,6 +174,24 @@ class LandedCostVoucher(Document):
|
|||||||
_("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code)
|
_("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_expense_accounts(self):
|
||||||
|
for t in self.taxes:
|
||||||
|
company = frappe.get_cached_value("Account", t.expense_account, "company")
|
||||||
|
|
||||||
|
if company != self.company:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Row {0}: Expense Account {1} is linked to company {2}. Please select an account belonging to company {3}."
|
||||||
|
).format(
|
||||||
|
t.idx,
|
||||||
|
frappe.bold(t.expense_account),
|
||||||
|
frappe.bold(company),
|
||||||
|
frappe.bold(self.company),
|
||||||
|
),
|
||||||
|
title=_("Incorrect Account"),
|
||||||
|
exc=IncorrectCompanyValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
def set_total_taxes_and_charges(self):
|
def set_total_taxes_and_charges(self):
|
||||||
self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes"))
|
self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes"))
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,39 @@ class TestLandedCostVoucher(IntegrationTestCase):
|
|||||||
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
|
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
|
||||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||||
|
|
||||||
|
def test_lcv_validates_company(self):
|
||||||
|
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
|
||||||
|
IncorrectCompanyValidationError,
|
||||||
|
)
|
||||||
|
|
||||||
|
company_a = "_Test Company"
|
||||||
|
company_b = "_Test Company with perpetual inventory"
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
company=company_a,
|
||||||
|
warehouse="Stores - _TC",
|
||||||
|
qty=1,
|
||||||
|
rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
lcv = make_landed_cost_voucher(
|
||||||
|
company=company_b,
|
||||||
|
receipt_document_type="Purchase Receipt",
|
||||||
|
receipt_document=pr.name,
|
||||||
|
charges=50,
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(IncorrectCompanyValidationError, lcv.validate_receipt_documents)
|
||||||
|
lcv.company = company_a
|
||||||
|
|
||||||
|
self.assertRaises(IncorrectCompanyValidationError, lcv.validate_expense_accounts)
|
||||||
|
lcv.taxes[0].expense_account = get_expense_account(company_a)
|
||||||
|
|
||||||
|
lcv.save()
|
||||||
|
distribute_landed_cost_on_items(lcv)
|
||||||
|
lcv.submit()
|
||||||
|
|
||||||
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
||||||
"Test impact of LCV on future stock balances."
|
"Test impact of LCV on future stock balances."
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
@@ -1260,6 +1293,7 @@ def make_landed_cost_voucher(**args):
|
|||||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||||
lcv.company = args.company or "_Test Company"
|
lcv.company = args.company or "_Test Company"
|
||||||
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
|
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
|
||||||
|
expense_account = get_expense_account(args.company or "_Test Company")
|
||||||
|
|
||||||
lcv.set(
|
lcv.set(
|
||||||
"purchase_receipts",
|
"purchase_receipts",
|
||||||
@@ -1280,7 +1314,7 @@ def make_landed_cost_voucher(**args):
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"description": "Shipping Charges",
|
"description": "Shipping Charges",
|
||||||
"expense_account": args.expense_account or "Expenses Included In Valuation - TCP1",
|
"expense_account": args.expense_account or expense_account,
|
||||||
"amount": args.charges,
|
"amount": args.charges,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1300,6 +1334,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
|||||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||||
lcv.company = company
|
lcv.company = company
|
||||||
lcv.distribute_charges_based_on = "Amount"
|
lcv.distribute_charges_based_on = "Amount"
|
||||||
|
expense_account = get_expense_account(company)
|
||||||
|
|
||||||
lcv.set(
|
lcv.set(
|
||||||
"purchase_receipts",
|
"purchase_receipts",
|
||||||
@@ -1319,7 +1354,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"description": "Insurance Charges",
|
"description": "Insurance Charges",
|
||||||
"expense_account": "Expenses Included In Valuation - TCP1",
|
"expense_account": expense_account,
|
||||||
"amount": charges,
|
"amount": charges,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -1334,6 +1369,11 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
|||||||
return lcv
|
return lcv
|
||||||
|
|
||||||
|
|
||||||
|
def get_expense_account(company):
|
||||||
|
company_abbr = frappe.get_cached_value("Company", company, "abbr")
|
||||||
|
return f"Expenses Included In Valuation - {company_abbr}"
|
||||||
|
|
||||||
|
|
||||||
def distribute_landed_cost_on_items(lcv):
|
def distribute_landed_cost_on_items(lcv):
|
||||||
based_on = lcv.distribute_charges_based_on.lower()
|
based_on = lcv.distribute_charges_based_on.lower()
|
||||||
total = sum(flt(d.get(based_on)) for d in lcv.get("items"))
|
total = sum(flt(d.get(based_on)) for d in lcv.get("items"))
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class MaterialRequest(BuyingController):
|
|||||||
{
|
{
|
||||||
"source_dt": "Material Request Item",
|
"source_dt": "Material Request Item",
|
||||||
"target_dt": "Sales Order Item",
|
"target_dt": "Sales Order Item",
|
||||||
"target_field": "ordered_qty",
|
"target_field": "requested_qty",
|
||||||
"target_parent_dt": "Sales Order",
|
"target_parent_dt": "Sales Order",
|
||||||
"target_parent_field": "",
|
"target_parent_field": "",
|
||||||
"join_field": "sales_order_item",
|
"join_field": "sales_order_item",
|
||||||
@@ -280,6 +280,8 @@ class MaterialRequest(BuyingController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_requested_qty_in_production_plan(cancel=True)
|
self.update_requested_qty_in_production_plan(cancel=True)
|
||||||
self.update_requested_qty()
|
self.update_requested_qty()
|
||||||
|
if self.material_request_type == "Purchase":
|
||||||
|
self.update_prevdoc_status()
|
||||||
|
|
||||||
def get_mr_items_ordered_qty(self, mr_items):
|
def get_mr_items_ordered_qty(self, mr_items):
|
||||||
mr_items_ordered_qty = {}
|
mr_items_ordered_qty = {}
|
||||||
@@ -330,7 +332,8 @@ class MaterialRequest(BuyingController):
|
|||||||
|
|
||||||
if mr_qty_allowance:
|
if mr_qty_allowance:
|
||||||
allowed_qty = flt(
|
allowed_qty = flt(
|
||||||
(d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")
|
(d.stock_qty + (d.stock_qty * (mr_qty_allowance / 100))),
|
||||||
|
d.precision("ordered_qty"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision):
|
if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision):
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ frappe.ui.form.on("Pick List", {
|
|||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
frm.trigger("add_get_items_button");
|
frm.trigger("add_get_items_button");
|
||||||
frm.trigger("update_warehouse_property");
|
frm.trigger("update_warehouse_property");
|
||||||
|
erpnext.toggle_serial_batch_fields(frm);
|
||||||
|
|
||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
const status_completed = frm.doc.status === "Completed";
|
const status_completed = frm.doc.status === "Completed";
|
||||||
|
|
||||||
|
|||||||
@@ -1587,7 +1587,7 @@ def update_common_item_properties(item, location):
|
|||||||
item.item_code = location.item_code
|
item.item_code = location.item_code
|
||||||
item.s_warehouse = location.warehouse
|
item.s_warehouse = location.warehouse
|
||||||
item.transfer_qty = location.picked_qty
|
item.transfer_qty = location.picked_qty
|
||||||
item.qty = location.qty
|
item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty"))
|
||||||
item.uom = location.uom
|
item.uom = location.uom
|
||||||
item.conversion_factor = location.conversion_factor
|
item.conversion_factor = location.conversion_factor
|
||||||
item.stock_uom = location.stock_uom
|
item.stock_uom = location.stock_uom
|
||||||
|
|||||||
@@ -1246,7 +1246,9 @@ def get_billed_amount_against_po(po_items):
|
|||||||
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
|
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
|
||||||
# Update Billing % based on pending accepted qty
|
# Update Billing % based on pending accepted qty
|
||||||
buying_settings = frappe.get_single("Buying Settings")
|
buying_settings = frappe.get_single("Buying Settings")
|
||||||
over_billing_allowance = frappe.get_single_value("Accounts Settings", "over_billing_allowance")
|
over_billing_allowance, role_allowed_to_over_bill = frappe.get_single_value(
|
||||||
|
"Accounts Settings", ["over_billing_allowance", "role_allowed_to_over_bill"]
|
||||||
|
)
|
||||||
|
|
||||||
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
|
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
|
||||||
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
|
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
|
||||||
@@ -1304,7 +1306,10 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
|||||||
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
|
||||||
elif amount and item.billed_amt > amount:
|
elif amount and item.billed_amt > amount:
|
||||||
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
|
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
|
||||||
if per_over_billed > over_billing_allowance:
|
if (
|
||||||
|
per_over_billed > over_billing_allowance
|
||||||
|
and role_allowed_to_over_bill not in frappe.get_roles()
|
||||||
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
|
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
|
||||||
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
|
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ class TestQualityInspection(IntegrationTestCase):
|
|||||||
inspection_type = "Outgoing"
|
inspection_type = "Outgoing"
|
||||||
for item in dn.items:
|
for item in dn.items:
|
||||||
item.sample_size = item.qty
|
item.sample_size = item.qty
|
||||||
quality_inspections = make_quality_inspections(dn.doctype, dn.name, dn.items, inspection_type)
|
quality_inspections = make_quality_inspections(
|
||||||
|
dn.company, dn.doctype, dn.name, dn.items, inspection_type
|
||||||
|
)
|
||||||
self.assertEqual(len(dn.items), len(quality_inspections))
|
self.assertEqual(len(dn.items), len(quality_inspections))
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
|
|||||||
@@ -570,7 +570,20 @@ def run_parallel_reposting():
|
|||||||
|
|
||||||
riv_entries = get_repost_item_valuation_entries()
|
riv_entries = get_repost_item_valuation_entries()
|
||||||
|
|
||||||
|
rq_jobs = frappe.get_all(
|
||||||
|
"RQ Job",
|
||||||
|
fields=["arguments"],
|
||||||
|
filters={
|
||||||
|
"status": ("like", "%started%"),
|
||||||
|
"job_name": "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.execute_reposting_entry",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
for row in riv_entries:
|
for row in riv_entries:
|
||||||
|
if rq_jobs:
|
||||||
|
if job_running_for_entry(row.name, rq_jobs):
|
||||||
|
continue
|
||||||
|
|
||||||
if row.based_on != "Item and Warehouse" or row.repost_only_accounting_ledgers:
|
if row.based_on != "Item and Warehouse" or row.repost_only_accounting_ledgers:
|
||||||
execute_reposting_entry(row.name)
|
execute_reposting_entry(row.name)
|
||||||
continue
|
continue
|
||||||
@@ -719,3 +732,19 @@ def get_existing_reposting_only_gl_entries(reposting_reference):
|
|||||||
reposting_map[key] = d.reposting_reference
|
reposting_map[key] = d.reposting_reference
|
||||||
|
|
||||||
return reposting_map
|
return reposting_map
|
||||||
|
|
||||||
|
|
||||||
|
def job_running_for_entry(reposting_entry, rq_jobs):
|
||||||
|
for job in rq_jobs:
|
||||||
|
if not job.arguments:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
job_args = json.loads(job.arguments)
|
||||||
|
except (TypeError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(job_args, dict) and job_args.get("kwargs", {}).get("name") == reposting_entry:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class SerialandBatchBundle(Document):
|
|||||||
self.autoname()
|
self.autoname()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_allow_to_set_serial_batch()
|
||||||
if self.docstatus == 1 and self.voucher_detail_no:
|
if self.docstatus == 1 and self.voucher_detail_no:
|
||||||
self.validate_voucher_detail_no()
|
self.validate_voucher_detail_no()
|
||||||
|
|
||||||
@@ -143,6 +144,15 @@ class SerialandBatchBundle(Document):
|
|||||||
self.calculate_qty_and_amount()
|
self.calculate_qty_and_amount()
|
||||||
self.set_child_details()
|
self.set_child_details()
|
||||||
|
|
||||||
|
def validate_allow_to_set_serial_batch(self):
|
||||||
|
if not frappe.db.get_single_value("Stock Settings", "enable_serial_and_batch_no_for_item"):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Please check the 'Enable Serial and Batch No for Item' checkbox in the {0} to make Serial and Batch Bundle for the item."
|
||||||
|
).format(get_link_to_form("Stock Settings", "Stock Settings")),
|
||||||
|
title=_("Serial and Batch No for Item Disabled"),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_serial_no_status(self):
|
def validate_serial_no_status(self):
|
||||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
invalid_serial_nos = frappe.get_all(
|
invalid_serial_nos = frappe.get_all(
|
||||||
@@ -717,10 +727,13 @@ class SerialandBatchBundle(Document):
|
|||||||
if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]:
|
if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]:
|
||||||
rate = frappe.db.get_value(
|
rate = frappe.db.get_value(
|
||||||
"Packed Item",
|
"Packed Item",
|
||||||
self.voucher_detail_no,
|
{"parent_detail_docname": self.voucher_detail_no, "item_code": self.item_code},
|
||||||
"incoming_rate",
|
"incoming_rate",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if rate is None:
|
||||||
|
rate = frappe.db.get_value("Packed Item", self.voucher_detail_no, "incoming_rate")
|
||||||
|
|
||||||
if rate is not None:
|
if rate is not None:
|
||||||
is_packed_item = True
|
is_packed_item = True
|
||||||
|
|
||||||
@@ -787,6 +800,9 @@ class SerialandBatchBundle(Document):
|
|||||||
if not self.voucher_detail_no or self.voucher_detail_no != row.name:
|
if not self.voucher_detail_no or self.voucher_detail_no != row.name:
|
||||||
values_to_set["voucher_detail_no"] = row.name
|
values_to_set["voucher_detail_no"] = row.name
|
||||||
|
|
||||||
|
if row.get("doctype") == "Packed Item" and row.get("parent_detail_docname"):
|
||||||
|
values_to_set["voucher_detail_no"] = row.get("parent_detail_docname")
|
||||||
|
|
||||||
if parent.get("posting_date") and parent.get("posting_time"):
|
if parent.get("posting_date") and parent.get("posting_time"):
|
||||||
posting_datetime = combine_datetime(parent.posting_date, parent.posting_time)
|
posting_datetime = combine_datetime(parent.posting_date, parent.posting_time)
|
||||||
if not self.posting_datetime or self.posting_datetime != posting_datetime:
|
if not self.posting_datetime or self.posting_datetime != posting_datetime:
|
||||||
@@ -1325,7 +1341,21 @@ class SerialandBatchBundle(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not vouchers and self.voucher_type == "Delivery Note":
|
if not vouchers and self.voucher_type == "Delivery Note":
|
||||||
|
if frappe.db.exists("Packed Item", self.voucher_detail_no):
|
||||||
frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
|
frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
|
||||||
|
else:
|
||||||
|
packed_items = frappe.get_all(
|
||||||
|
"Packed Item",
|
||||||
|
filters={
|
||||||
|
"parent_detail_docname": self.voucher_detail_no,
|
||||||
|
"serial_and_batch_bundle": self.name,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
for packed_item in packed_items:
|
||||||
|
frappe.db.set_value("Packed Item", packed_item, "serial_and_batch_bundle", None)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for voucher in vouchers:
|
for voucher in vouchers:
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
frm.trigger("get_items_from_transit_entry");
|
frm.trigger("get_items_from_transit_entry");
|
||||||
frm.trigger("toggle_warehouse_fields");
|
frm.trigger("toggle_warehouse_fields");
|
||||||
|
erpnext.toggle_serial_batch_fields(frm);
|
||||||
|
|
||||||
if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) {
|
if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) {
|
||||||
frm.trigger("validate_purpose_consumption");
|
frm.trigger("validate_purpose_consumption");
|
||||||
@@ -930,10 +931,6 @@ frappe.ui.form.on("Stock Entry Detail", {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
qty(frm, cdt, cdn) {
|
|
||||||
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
|
|
||||||
},
|
|
||||||
|
|
||||||
conversion_factor(frm, cdt, cdn) {
|
conversion_factor(frm, cdt, cdn) {
|
||||||
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
|
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
|||||||
self.validate_uom_is_integer("uom", "qty")
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
|
self.validate_warehouse_of_sabb()
|
||||||
self.validate_work_order()
|
self.validate_work_order()
|
||||||
self.validate_bom()
|
self.validate_bom()
|
||||||
self.set_process_loss_qty()
|
self.set_process_loss_qty()
|
||||||
|
|||||||
@@ -2415,6 +2415,54 @@ class TestStockEntry(IntegrationTestCase):
|
|||||||
frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit()
|
frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit()
|
||||||
frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit()
|
frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit()
|
||||||
|
|
||||||
|
def test_qi_creation_with_naming_rule_company_condition(self):
|
||||||
|
"""
|
||||||
|
Unit test case to check the document naming rule with company condition
|
||||||
|
For Quality Inspection, when created from Stock Entry.
|
||||||
|
"""
|
||||||
|
from erpnext.accounts.report.trial_balance.test_trial_balance import create_company
|
||||||
|
from erpnext.controllers.stock_controller import make_quality_inspections
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
|
# create a separate company to handle document naming rule with company condition
|
||||||
|
qc_company = create_company(company_name="Test Quality Company")
|
||||||
|
|
||||||
|
# create document naming rule based on that for Quality Inspection Doctype
|
||||||
|
qc_naming_rule = frappe.new_doc(
|
||||||
|
"Document Naming Rule", document_type="Quality Inspection", prefix="NQC.-ST-", prefix_digits=5
|
||||||
|
)
|
||||||
|
qc_naming_rule.append("conditions", {"field": "company", "condition": "=", "value": qc_company})
|
||||||
|
qc_naming_rule.save()
|
||||||
|
|
||||||
|
warehouse = create_warehouse(warehouse_name="Test QI Warehouse", company=qc_company)
|
||||||
|
item = create_item(
|
||||||
|
item_code="Test QI DNR Item",
|
||||||
|
is_stock_item=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# create inward stock entry
|
||||||
|
stock_entry = make_stock_entry(
|
||||||
|
item_code=item.item_code,
|
||||||
|
target=warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
inspection_required=True,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# create QI from Stock Entry and check the naming series generated.
|
||||||
|
qi = make_quality_inspections(
|
||||||
|
stock_entry.company,
|
||||||
|
stock_entry.doctype,
|
||||||
|
stock_entry.name,
|
||||||
|
stock_entry.as_dict().get("items"),
|
||||||
|
"Incoming",
|
||||||
|
)
|
||||||
|
self.assertEqual(qi[0], "NQC-ST-00001")
|
||||||
|
|
||||||
|
# delete naming rule
|
||||||
|
frappe.delete_doc("Document Naming Rule", qc_naming_rule.name)
|
||||||
|
|
||||||
|
|
||||||
def make_serialized_item(self, **args):
|
def make_serialized_item(self, **args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -498,7 +498,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Reference Purchase Receipt",
|
"label": "Reference Purchase Receipt",
|
||||||
"options": "Purchase Receipt",
|
"options": "Purchase Receipt",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
@@ -660,7 +661,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-16 11:50:50.573443",
|
"modified": "2026-03-02 14:05:23.116017",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ frappe.ui.form.on("Stock Reconciliation", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
erpnext.toggle_serial_batch_fields(frm);
|
||||||
|
|
||||||
if (frm.doc.docstatus < 1) {
|
if (frm.doc.docstatus < 1) {
|
||||||
frm.add_custom_button(__("Fetch Items from Warehouse"), function () {
|
frm.add_custom_button(__("Fetch Items from Warehouse"), function () {
|
||||||
frm.events.get_items(frm);
|
frm.events.get_items(frm);
|
||||||
|
|||||||
@@ -523,9 +523,9 @@ class StockReconciliation(StockController):
|
|||||||
if abs(difference_amount) > 0:
|
if abs(difference_amount) > 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
float_precision = frappe.db.get_default("float_precision") or 3
|
rate_precision = item.precision("valuation_rate")
|
||||||
item_dict["rate"] = flt(item_dict.get("rate"), float_precision)
|
item_dict["rate"] = flt(item_dict.get("rate"), rate_precision)
|
||||||
item.valuation_rate = flt(item.valuation_rate, float_precision) if item.valuation_rate else None
|
item.valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
|
||||||
if (
|
if (
|
||||||
(item.qty is None or item.qty == item_dict.get("qty"))
|
(item.qty is None or item.qty == item_dict.get("qty"))
|
||||||
and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
|
and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"allow_internal_transfer_at_arms_length_price",
|
"allow_internal_transfer_at_arms_length_price",
|
||||||
"validate_material_transfer_warehouses",
|
"validate_material_transfer_warehouses",
|
||||||
"serial_and_batch_item_settings_tab",
|
"serial_and_batch_item_settings_tab",
|
||||||
|
"enable_serial_and_batch_no_for_item",
|
||||||
"section_break_7",
|
"section_break_7",
|
||||||
"allow_existing_serial_no",
|
"allow_existing_serial_no",
|
||||||
"do_not_use_batchwise_valuation",
|
"do_not_use_batchwise_valuation",
|
||||||
@@ -48,9 +49,8 @@
|
|||||||
"use_serial_batch_fields",
|
"use_serial_batch_fields",
|
||||||
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
"do_not_update_serial_batch_on_creation_of_auto_bundle",
|
||||||
"allow_negative_stock_for_batch",
|
"allow_negative_stock_for_batch",
|
||||||
"serial_and_batch_bundle_section",
|
|
||||||
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
|
||||||
"section_break_gnhq",
|
"section_break_gnhq",
|
||||||
|
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||||
"use_naming_series",
|
"use_naming_series",
|
||||||
"column_break_wslv",
|
"column_break_wslv",
|
||||||
"naming_series_prefix",
|
"naming_series_prefix",
|
||||||
@@ -158,6 +158,7 @@
|
|||||||
"label": "Convert Item Description to Clean HTML in Transactions"
|
"label": "Convert Item Description to Clean HTML in Transactions"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "enable_serial_and_batch_no_for_item",
|
||||||
"fieldname": "section_break_7",
|
"fieldname": "section_break_7",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Serial & Batch Item Settings"
|
"label": "Serial & Batch Item Settings"
|
||||||
@@ -487,11 +488,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Auto Reserve Stock"
|
"label": "Auto Reserve Stock"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "serial_and_batch_bundle_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Serial and Batch Bundle"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
|
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||||
@@ -499,6 +495,7 @@
|
|||||||
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
|
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "enable_serial_and_batch_no_for_item",
|
||||||
"fieldname": "section_break_gnhq",
|
"fieldname": "section_break_gnhq",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
@@ -554,6 +551,11 @@
|
|||||||
"fieldname": "allow_negative_stock_for_batch",
|
"fieldname": "allow_negative_stock_for_batch",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Negative Stock for Batch"
|
"label": "Allow Negative Stock for Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "enable_serial_and_batch_no_for_item",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Serial / Batch No for Item"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
@@ -562,7 +564,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-02-25 09:56:34.105949",
|
"modified": "2026-02-25 10:56:34.105949",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class StockSettings(Document):
|
|||||||
disable_serial_no_and_batch_selector: DF.Check
|
disable_serial_no_and_batch_selector: DF.Check
|
||||||
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
|
do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check
|
||||||
do_not_use_batchwise_valuation: DF.Check
|
do_not_use_batchwise_valuation: DF.Check
|
||||||
|
enable_serial_and_batch_no_for_item: DF.Check
|
||||||
enable_stock_reservation: DF.Check
|
enable_stock_reservation: DF.Check
|
||||||
item_group: DF.Link | None
|
item_group: DF.Link | None
|
||||||
item_naming_by: DF.Literal["Item Code", "Naming Series"]
|
item_naming_by: DF.Literal["Item Code", "Naming Series"]
|
||||||
@@ -82,6 +83,7 @@ class StockSettings(Document):
|
|||||||
"default_warehouse",
|
"default_warehouse",
|
||||||
"set_qty_in_transactions_based_on_serial_no_input",
|
"set_qty_in_transactions_based_on_serial_no_input",
|
||||||
"use_serial_batch_fields",
|
"use_serial_batch_fields",
|
||||||
|
"enable_serial_and_batch_no_for_item",
|
||||||
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
"set_serial_and_batch_bundle_naming_based_on_naming_series",
|
||||||
]:
|
]:
|
||||||
frappe.db.set_default(key, self.get(key, ""))
|
frappe.db.set_default(key, self.get(key, ""))
|
||||||
@@ -104,6 +106,7 @@ class StockSettings(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.validate_warehouses()
|
self.validate_warehouses()
|
||||||
|
self.validate_serial_and_batch_no_settings()
|
||||||
self.cant_change_valuation_method()
|
self.cant_change_valuation_method()
|
||||||
self.validate_clean_description_html()
|
self.validate_clean_description_html()
|
||||||
self.validate_pending_reposts()
|
self.validate_pending_reposts()
|
||||||
@@ -112,6 +115,25 @@ class StockSettings(Document):
|
|||||||
self.change_precision_for_for_sales()
|
self.change_precision_for_for_sales()
|
||||||
self.change_precision_for_purchase()
|
self.change_precision_for_purchase()
|
||||||
|
|
||||||
|
def validate_serial_and_batch_no_settings(self):
|
||||||
|
doc_before_save = self.get_doc_before_save()
|
||||||
|
if not doc_before_save:
|
||||||
|
return
|
||||||
|
|
||||||
|
if doc_before_save.enable_serial_and_batch_no_for_item == self.enable_serial_and_batch_no_for_item:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
doc_before_save.enable_serial_and_batch_no_for_item
|
||||||
|
and not self.enable_serial_and_batch_no_for_item
|
||||||
|
):
|
||||||
|
if frappe.get_all("Serial and Batch Bundle", filters={"docstatus": 1}, limit=1, pluck="name"):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Cannot disable Serial and Batch No for Item, as there are existing records for serial / batch."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_warehouses(self):
|
def validate_warehouses(self):
|
||||||
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
|
||||||
for field in warehouse_fields:
|
for field in warehouse_fields:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any, TypedDict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Coalesce
|
from frappe.query_builder.functions import Coalesce, Count
|
||||||
from frappe.utils import add_days, cint, date_diff, flt, getdate
|
from frappe.utils import add_days, cint, date_diff, flt, getdate
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
@@ -165,6 +165,7 @@ class StockBalanceReport:
|
|||||||
sle.serial_no,
|
sle.serial_no,
|
||||||
sle.serial_and_batch_bundle,
|
sle.serial_and_batch_bundle,
|
||||||
sle.has_serial_no,
|
sle.has_serial_no,
|
||||||
|
sle.voucher_detail_no,
|
||||||
item_table.item_group,
|
item_table.item_group,
|
||||||
item_table.stock_uom,
|
item_table.stock_uom,
|
||||||
item_table.item_name,
|
item_table.item_name,
|
||||||
@@ -190,6 +191,8 @@ class StockBalanceReport:
|
|||||||
if self.filters.get("show_stock_ageing_data"):
|
if self.filters.get("show_stock_ageing_data"):
|
||||||
self.sle_entries = self.sle_query.run(as_dict=True)
|
self.sle_entries = self.sle_query.run(as_dict=True)
|
||||||
|
|
||||||
|
self.prepare_stock_reco_voucher_wise_count()
|
||||||
|
|
||||||
# HACK: This is required to avoid causing db query in flt
|
# HACK: This is required to avoid causing db query in flt
|
||||||
_system_settings = frappe.get_cached_doc("System Settings")
|
_system_settings = frappe.get_cached_doc("System Settings")
|
||||||
with frappe.db.unbuffered_cursor():
|
with frappe.db.unbuffered_cursor():
|
||||||
@@ -207,6 +210,71 @@ class StockBalanceReport:
|
|||||||
self.item_warehouse_map, self.float_precision, self.inventory_dimensions
|
self.item_warehouse_map, self.float_precision, self.inventory_dimensions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def prepare_stock_reco_voucher_wise_count(self):
|
||||||
|
self.stock_reco_voucher_wise_count = frappe._dict()
|
||||||
|
|
||||||
|
doctype = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
item = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.inner_join(item)
|
||||||
|
.on(doctype.item_code == item.name)
|
||||||
|
.select(doctype.voucher_detail_no, Count(doctype.name).as_("count"))
|
||||||
|
.where(
|
||||||
|
(doctype.voucher_type == "Stock Reconciliation")
|
||||||
|
& (doctype.docstatus < 2)
|
||||||
|
& (doctype.is_cancelled == 0)
|
||||||
|
& (item.has_serial_no == 1)
|
||||||
|
)
|
||||||
|
.groupby(doctype.voucher_detail_no)
|
||||||
|
)
|
||||||
|
|
||||||
|
if items := self.filters.item_code:
|
||||||
|
if isinstance(items, str):
|
||||||
|
items = [items]
|
||||||
|
|
||||||
|
query = query.where(item.name.isin(items))
|
||||||
|
|
||||||
|
if self.filters.item_group:
|
||||||
|
childrens = []
|
||||||
|
childrens.append(self.filters.item_group)
|
||||||
|
if item_group_childrens := get_descendants_of(
|
||||||
|
"Item Group", self.filters.item_group, ignore_permissions=True
|
||||||
|
):
|
||||||
|
childrens.extend(item_group_childrens)
|
||||||
|
|
||||||
|
if childrens:
|
||||||
|
query = query.where(item.item_group.isin(childrens))
|
||||||
|
|
||||||
|
if warehouses := self.filters.get("warehouse"):
|
||||||
|
if isinstance(warehouses, str):
|
||||||
|
warehouses = [warehouses]
|
||||||
|
|
||||||
|
childrens = []
|
||||||
|
for warehouse in warehouses:
|
||||||
|
childrens.append(warehouse)
|
||||||
|
if warehouse_childrens := get_descendants_of("Warehouse", warehouse, ignore_permissions=True):
|
||||||
|
childrens.extend(warehouse_childrens)
|
||||||
|
|
||||||
|
if childrens:
|
||||||
|
query = query.where(doctype.warehouse.isin(childrens))
|
||||||
|
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
if row.count != 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sr_item = frappe.db.get_value(
|
||||||
|
"Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if sr_item.qty and sr_item.current_qty:
|
||||||
|
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty
|
||||||
|
|
||||||
def prepare_new_data(self):
|
def prepare_new_data(self):
|
||||||
if self.filters.get("show_stock_ageing_data"):
|
if self.filters.get("show_stock_ageing_data"):
|
||||||
self.filters["show_warehouse_wise_stock"] = True
|
self.filters["show_warehouse_wise_stock"] = True
|
||||||
@@ -283,8 +351,13 @@ class StockBalanceReport:
|
|||||||
qty_dict[field] = entry.get(field)
|
qty_dict[field] = entry.get(field)
|
||||||
|
|
||||||
if entry.voucher_type == "Stock Reconciliation" and (
|
if entry.voucher_type == "Stock Reconciliation" and (
|
||||||
not entry.batch_no and not entry.serial_no and not entry.serial_and_batch_bundle
|
not entry.batch_no or entry.serial_no or entry.serial_and_batch_bundle
|
||||||
):
|
):
|
||||||
|
if entry.serial_no and entry.voucher_detail_no in self.stock_reco_voucher_wise_count:
|
||||||
|
qty_dict.opening_qty -= self.stock_reco_voucher_wise_count.get(entry.voucher_detail_no, 0)
|
||||||
|
qty_dict.bal_qty = 0.0
|
||||||
|
qty_diff = flt(entry.actual_qty)
|
||||||
|
else:
|
||||||
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
||||||
else:
|
else:
|
||||||
qty_diff = flt(entry.actual_qty)
|
qty_diff = flt(entry.actual_qty)
|
||||||
|
|||||||
@@ -27,10 +27,23 @@ def execute(filters=None):
|
|||||||
items = get_items(filters)
|
items = get_items(filters)
|
||||||
sl_entries = get_stock_ledger_entries(filters, items)
|
sl_entries = get_stock_ledger_entries(filters, items)
|
||||||
item_details = get_item_details(items, sl_entries, include_uom)
|
item_details = get_item_details(items, sl_entries, include_uom)
|
||||||
|
|
||||||
|
inv_dimension_key = []
|
||||||
|
inv_dimension_wise_value = get_inv_dimension_wise_value(filters)
|
||||||
|
if inv_dimension_wise_value:
|
||||||
|
for key in inv_dimension_wise_value:
|
||||||
|
value = inv_dimension_wise_value[key]
|
||||||
|
if isinstance(value, list):
|
||||||
|
inv_dimension_key.extend(value)
|
||||||
|
else:
|
||||||
|
inv_dimension_key.append(value)
|
||||||
|
|
||||||
if filters.get("batch_no"):
|
if filters.get("batch_no"):
|
||||||
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries)
|
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries)
|
||||||
|
elif inv_dimension_wise_value:
|
||||||
|
opening_row = get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value)
|
||||||
else:
|
else:
|
||||||
opening_row = get_opening_balance(filters, columns, sl_entries)
|
opening_row = get_opening_balance(filters, columns, sl_entries, inv_dimension_wise_value)
|
||||||
|
|
||||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||||
bundle_details = {}
|
bundle_details = {}
|
||||||
@@ -50,12 +63,16 @@ def execute(filters=None):
|
|||||||
stock_value = opening_row.get("stock_value")
|
stock_value = opening_row.get("stock_value")
|
||||||
|
|
||||||
available_serial_nos = {}
|
available_serial_nos = {}
|
||||||
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
|
|
||||||
|
|
||||||
batch_balance_dict = frappe._dict({})
|
batch_balance_dict = frappe._dict({})
|
||||||
if actual_qty and filters.get("batch_no"):
|
if actual_qty and filters.get("batch_no"):
|
||||||
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value]
|
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value]
|
||||||
|
|
||||||
|
inv_dimension_wise_dict = frappe._dict({})
|
||||||
|
set_opening_row_for_inv_dimension(
|
||||||
|
inv_dimension_wise_dict, filters, inv_dimension_key=inv_dimension_key, opening_row=opening_row
|
||||||
|
)
|
||||||
|
|
||||||
for sle in sl_entries:
|
for sle in sl_entries:
|
||||||
item_detail = item_details[sle.item_code]
|
item_detail = item_details[sle.item_code]
|
||||||
|
|
||||||
@@ -64,7 +81,10 @@ def execute(filters=None):
|
|||||||
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict, filters))
|
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict, filters))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if filters.get("batch_no") or inventory_dimension_filters_applied:
|
if inv_dimension_key:
|
||||||
|
set_balance_value_for_inv_dimesion(inv_dimension_key, inv_dimension_wise_dict, sle)
|
||||||
|
|
||||||
|
if filters.get("batch_no"):
|
||||||
actual_qty += flt(sle.actual_qty, precision)
|
actual_qty += flt(sle.actual_qty, precision)
|
||||||
stock_value += sle.stock_value_difference
|
stock_value += sle.stock_value_difference
|
||||||
if sle.batch_no:
|
if sle.batch_no:
|
||||||
@@ -103,6 +123,50 @@ def execute(filters=None):
|
|||||||
return columns, data
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def set_opening_row_for_inv_dimension(
|
||||||
|
inv_dimension_wise_dict, filters, inv_dimension_key=None, opening_row=None
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
not inv_dimension_key
|
||||||
|
or not opening_row
|
||||||
|
or not filters.get("item_code")
|
||||||
|
or not filters.get("warehouse")
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(filters.get("item_code")) > 1 or len(filters.get("warehouse")) > 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
if inv_dimension_key and opening_row and filters.get("item_code") and filters.get("warehouse"):
|
||||||
|
new_key = copy.deepcopy(inv_dimension_key)
|
||||||
|
new_key.extend([filters.item_code[0], filters.warehouse[0]])
|
||||||
|
|
||||||
|
opening_key = tuple(new_key)
|
||||||
|
inv_dimension_wise_dict[opening_key] = {
|
||||||
|
"qty_after_transaction": flt(opening_row.get("qty_after_transaction")),
|
||||||
|
"dimension_stock_value": flt(opening_row.get("stock_value")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_balance_value_for_inv_dimesion(inv_dimension_key, inv_dimension_wise_dict, sle):
|
||||||
|
new_key = copy.deepcopy(inv_dimension_key)
|
||||||
|
new_key.extend([sle.item_code, sle.warehouse])
|
||||||
|
new_key = tuple(new_key)
|
||||||
|
|
||||||
|
if new_key not in inv_dimension_wise_dict:
|
||||||
|
inv_dimension_wise_dict[new_key] = {"qty_after_transaction": 0, "dimension_stock_value": 0}
|
||||||
|
|
||||||
|
inv_dimesion_value = inv_dimension_wise_dict[new_key]
|
||||||
|
inv_dimesion_value["qty_after_transaction"] += sle.actual_qty
|
||||||
|
inv_dimesion_value["dimension_stock_value"] += sle.stock_value_difference
|
||||||
|
sle.update(
|
||||||
|
{
|
||||||
|
"qty_after_transaction": inv_dimesion_value["qty_after_transaction"],
|
||||||
|
"stock_value": inv_dimesion_value["dimension_stock_value"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict, filters):
|
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict, filters):
|
||||||
segregated_entries = []
|
segregated_entries = []
|
||||||
qty_before_transaction = sle.qty_after_transaction - sle.actual_qty
|
qty_before_transaction = sle.qty_after_transaction - sle.actual_qty
|
||||||
@@ -605,19 +669,26 @@ def get_opening_balance_from_batch(filters, columns, sl_entries):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_opening_balance(filters, columns, sl_entries):
|
def get_opening_balance(filters, columns, sl_entries, inv_dimension_wise_value=None):
|
||||||
if not (filters.item_code and filters.warehouse and filters.from_date):
|
if not (filters.item_code and filters.warehouse and filters.from_date):
|
||||||
return
|
return
|
||||||
|
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
|
|
||||||
|
project = None
|
||||||
|
if filters.get("project") and not frappe.get_all(
|
||||||
|
"Inventory Dimension", filters={"reference_document": "Project"}
|
||||||
|
):
|
||||||
|
project = filters.get("project")
|
||||||
|
|
||||||
last_entry = get_previous_sle(
|
last_entry = get_previous_sle(
|
||||||
{
|
{
|
||||||
"item_code": filters.item_code,
|
"item_code": filters.item_code,
|
||||||
"warehouse_condition": get_warehouse_condition(filters.warehouse),
|
"warehouse_condition": get_warehouse_condition(filters.warehouse),
|
||||||
"posting_date": filters.from_date,
|
"posting_date": filters.from_date,
|
||||||
"posting_time": "00:00:00",
|
"posting_time": "00:00:00",
|
||||||
}
|
"project": project,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# check if any SLEs are actually Opening Stock Reconciliation
|
# check if any SLEs are actually Opening Stock Reconciliation
|
||||||
@@ -689,9 +760,75 @@ def get_item_group_condition(item_group, item_table=None):
|
|||||||
where ig.lft >= {item_group_details.lft} and ig.rgt <= {item_group_details.rgt} and item.item_group = ig.name)"
|
where ig.lft >= {item_group_details.lft} and ig.rgt <= {item_group_details.rgt} and item.item_group = ig.name)"
|
||||||
|
|
||||||
|
|
||||||
def check_inventory_dimension_filters_applied(filters) -> bool:
|
def get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value):
|
||||||
|
if not filters.item_code or not filters.warehouse or not filters.from_date:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(filters.get("item_code")) > 1 or len(filters.get("warehouse")) > 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
sl_doctype = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sl_doctype)
|
||||||
|
.select(
|
||||||
|
sl_doctype.item_code,
|
||||||
|
sl_doctype.warehouse,
|
||||||
|
Sum(sl_doctype.actual_qty).as_("qty_after_transaction"),
|
||||||
|
Sum(sl_doctype.stock_value_difference).as_("stock_value"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(sl_doctype.posting_date < filters.from_date)
|
||||||
|
& (sl_doctype.docstatus < 2)
|
||||||
|
& (sl_doctype.is_cancelled == 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters.get("item_code"):
|
||||||
|
if isinstance(filters.item_code, list | tuple):
|
||||||
|
query = query.where(sl_doctype.item_code.isin(filters.item_code))
|
||||||
|
else:
|
||||||
|
query = query.where(sl_doctype.item_code == filters.item_code)
|
||||||
|
|
||||||
|
if filters.get("warehouse"):
|
||||||
|
if isinstance(filters.warehouse, list | tuple):
|
||||||
|
query = query.where(sl_doctype.warehouse.isin(filters.warehouse))
|
||||||
|
else:
|
||||||
|
query = query.where(sl_doctype.warehouse == filters.warehouse)
|
||||||
|
|
||||||
|
for key, value in inv_dimension_wise_value.items():
|
||||||
|
if isinstance(value, list | tuple):
|
||||||
|
query = query.where(sl_doctype[key].isin(value))
|
||||||
|
else:
|
||||||
|
query = query.where(sl_doctype[key] == value)
|
||||||
|
|
||||||
|
opening_data = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if opening_data:
|
||||||
|
return frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": _("'Opening'"),
|
||||||
|
"qty_after_transaction": opening_data[0].qty_after_transaction,
|
||||||
|
"stock_value": opening_data[0].stock_value,
|
||||||
|
"valuation_rate": flt(opening_data[0].stock_value)
|
||||||
|
/ flt(opening_data[0].qty_after_transaction)
|
||||||
|
if opening_data[0].qty_after_transaction
|
||||||
|
else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return frappe._dict({})
|
||||||
|
|
||||||
|
|
||||||
|
def get_inv_dimension_wise_value(filters) -> list:
|
||||||
|
inv_dimension_key = frappe._dict({})
|
||||||
for dimension in get_inventory_dimensions():
|
for dimension in get_inventory_dimensions():
|
||||||
if dimension.fieldname in filters and filters.get(dimension.fieldname):
|
if dimension.fieldname in filters and filters.get(dimension.fieldname):
|
||||||
return True
|
inv_dimension_key[dimension.fieldname] = filters.get(dimension.fieldname)
|
||||||
|
|
||||||
return False
|
if filters.get("project") and not frappe.get_all(
|
||||||
|
"Inventory Dimension", filters={"reference_document": "Project"}
|
||||||
|
):
|
||||||
|
inv_dimension_key["project"] = filters.get("project")
|
||||||
|
|
||||||
|
return inv_dimension_key
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ frappe.query_reports["Stock Ledger Invariant Check"] = {
|
|||||||
options: "Item",
|
options: "Item",
|
||||||
get_query: function () {
|
get_query: function () {
|
||||||
return {
|
return {
|
||||||
filters: { is_stock_item: 1, has_serial_no: 0 },
|
filters: { is_stock_item: 1 },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -401,6 +401,9 @@ class SerialBatchBundle:
|
|||||||
|
|
||||||
def submit_serial_and_batch_bundle(self):
|
def submit_serial_and_batch_bundle(self):
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
|
||||||
|
if self.sle.voucher_detail_no and doc.voucher_detail_no != self.sle.voucher_detail_no:
|
||||||
|
doc.voucher_detail_no = self.sle.voucher_detail_no
|
||||||
|
|
||||||
self.validate_actual_qty(doc)
|
self.validate_actual_qty(doc)
|
||||||
|
|
||||||
doc.flags.ignore_voucher_validation = True
|
doc.flags.ignore_voucher_validation = True
|
||||||
@@ -460,6 +463,11 @@ class SerialBatchBundle:
|
|||||||
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
|
||||||
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
|
||||||
|
|
||||||
|
if sle.voucher_type in ["Stock Entry"] and sle.actual_qty < 0:
|
||||||
|
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||||
|
if purpose in ["Disassemble", "Material Receipt"]:
|
||||||
|
status = "Inactive"
|
||||||
|
|
||||||
sn_table = frappe.qb.DocType("Serial No")
|
sn_table = frappe.qb.DocType("Serial No")
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
|
|||||||
@@ -1305,7 +1305,7 @@ class update_entries_after:
|
|||||||
else:
|
else:
|
||||||
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||||
ref_doctype = "Packed Item"
|
ref_doctype = "Packed Item"
|
||||||
elif sle == "Subcontracting Receipt":
|
elif sle.voucher_type == "Subcontracting Receipt":
|
||||||
ref_doctype = "Subcontracting Receipt Supplied Item"
|
ref_doctype = "Subcontracting Receipt Supplied Item"
|
||||||
else:
|
else:
|
||||||
ref_doctype = "Purchase Receipt Item Supplied"
|
ref_doctype = "Purchase Receipt Item Supplied"
|
||||||
@@ -1862,6 +1862,9 @@ def get_stock_ledger_entries(
|
|||||||
if extra_cond:
|
if extra_cond:
|
||||||
conditions += f"{extra_cond}"
|
conditions += f"{extra_cond}"
|
||||||
|
|
||||||
|
if previous_sle.get("project"):
|
||||||
|
conditions += " and project = %(project)s"
|
||||||
|
|
||||||
# nosemgrep
|
# nosemgrep
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
|||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||||
|
|
||||||
|
erpnext.toggle_serial_batch_fields(frm);
|
||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Stock Ledger"),
|
__("Stock Ledger"),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<form action="/search_help" style="display: flex;">
|
<form action="/search_help" style="display: flex;">
|
||||||
<input name='q' class='form-control' type='text'
|
<input name='q' class='form-control' type='text'
|
||||||
style='max-width: 400px; display: inline-block; margin-right: 10px;'
|
style='max-width: 400px; display: inline-block; margin-right: 10px;'
|
||||||
value='{{ frappe.form_dict.q or ''}}'
|
value='{{ (frappe.form_dict.q or '') | e }}'
|
||||||
{% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
|
{% if not frappe.form_dict.q%}placeholder="{{ _("What do you need help with?") }}"{% endif %}>
|
||||||
<input type='submit'
|
<input type='submit'
|
||||||
class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}">
|
class='btn btn-sm btn-light btn-search' value="{{ _("Search") }}">
|
||||||
|
|||||||
@@ -341,6 +341,7 @@ class TransactionBase(StatusUpdater):
|
|||||||
args.update(
|
args.update(
|
||||||
{
|
{
|
||||||
"posting_date": self.transaction_date,
|
"posting_date": self.transaction_date,
|
||||||
|
"posting_time": self.transaction_time,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
102
erpnext/workspace_sidebar/organization.json
Normal file
102
erpnext/workspace_sidebar/organization.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"app": "erpnext",
|
||||||
|
"creation": "2026-02-24 17:39:43.793115",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workspace Sidebar",
|
||||||
|
"header_icon": "organization",
|
||||||
|
"idx": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "organization",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "Company",
|
||||||
|
"link_to": "Company",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "book-text",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "Letter Head",
|
||||||
|
"link_to": "Letter Head",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "file-user",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "Department",
|
||||||
|
"link_to": "Department",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "book-user",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "Branch",
|
||||||
|
"link_to": "Branch",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "users",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "User",
|
||||||
|
"link_to": "User",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "user-round-check",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "Role Permissions",
|
||||||
|
"link_to": "permission-manager",
|
||||||
|
"link_type": "Page",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"child": 0,
|
||||||
|
"collapsible": 1,
|
||||||
|
"icon": "mail",
|
||||||
|
"indent": 0,
|
||||||
|
"keep_closed": 0,
|
||||||
|
"label": "Email Account",
|
||||||
|
"link_to": "Email Account",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"show_arrow": 0,
|
||||||
|
"type": "Link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2026-02-24 18:08:00.796746",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Setup",
|
||||||
|
"module_onboarding": "Organization Onboarding",
|
||||||
|
"name": "Organization",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"standard": 1,
|
||||||
|
"title": "Organization"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user