mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 11:55:11 +00:00
Merge pull request #53116 from frappe/version-16-hotfix
This commit is contained in:
@@ -33,6 +33,12 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
def on_update(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
if self.is_short_year:
|
||||
|
||||
@@ -303,10 +303,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
onload_post_render() {
|
||||
this.frm.get_field("accounts").grid.set_multiple_add("account");
|
||||
}
|
||||
|
||||
load_defaults() {
|
||||
//this.frm.show_print_first = true;
|
||||
if (this.frm.doc.__islocal && this.frm.doc.company) {
|
||||
|
||||
@@ -10,18 +10,15 @@
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
"company",
|
||||
"is_system_generated",
|
||||
"title",
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"process_deferred_accounting",
|
||||
"reversal_of",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"naming_series",
|
||||
"posting_date",
|
||||
"finance_book",
|
||||
"multi_currency",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"is_system_generated",
|
||||
"amended_from",
|
||||
"section_break_tcvw",
|
||||
"for_all_stock_asset_accounts",
|
||||
"column_break_wpau",
|
||||
@@ -30,52 +27,60 @@
|
||||
"get_balance_for_periodic_accounting",
|
||||
"2_add_edit_gl_entries",
|
||||
"accounts",
|
||||
"section_break99",
|
||||
"cheque_no",
|
||||
"cheque_date",
|
||||
"user_remark",
|
||||
"column_break99",
|
||||
"section_break_ouaq",
|
||||
"total_debit",
|
||||
"column_break_cixu",
|
||||
"total_credit",
|
||||
"difference",
|
||||
"get_balance",
|
||||
"multi_currency",
|
||||
"total_amount_currency",
|
||||
"total_amount",
|
||||
"total_amount_in_words",
|
||||
"section_break99",
|
||||
"cheque_no",
|
||||
"cheque_date",
|
||||
"clearance_date",
|
||||
"column_break_oizh",
|
||||
"user_remark",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"tax_withholding_tab",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"more_info_tab",
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
"inter_company_journal_entry_reference",
|
||||
"column_break98",
|
||||
"bill_no",
|
||||
"bill_date",
|
||||
"due_date",
|
||||
"column_break_isfa",
|
||||
"inter_company_journal_entry_reference",
|
||||
"process_deferred_accounting",
|
||||
"reversal_of",
|
||||
"payment_order",
|
||||
"stock_entry",
|
||||
"printing_settings",
|
||||
"pay_to_recd_from",
|
||||
"letter_head",
|
||||
"select_print_heading",
|
||||
"column_break_35",
|
||||
"total_amount_currency",
|
||||
"total_amount",
|
||||
"total_amount_in_words",
|
||||
"write_off",
|
||||
"write_off_based_on",
|
||||
"get_outstanding_invoices",
|
||||
"column_break_30",
|
||||
"write_off_amount",
|
||||
"printing_settings",
|
||||
"pay_to_recd_from",
|
||||
"column_break_35",
|
||||
"letter_head",
|
||||
"select_print_heading",
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from"
|
||||
"finance_book",
|
||||
"from_template",
|
||||
"title",
|
||||
"column_break3",
|
||||
"remark",
|
||||
"mode_of_payment",
|
||||
"party_not_required"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -155,6 +160,7 @@
|
||||
{
|
||||
"fieldname": "2_add_edit_gl_entries",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-table"
|
||||
},
|
||||
@@ -202,10 +208,6 @@
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break99",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_debit",
|
||||
"fieldtype": "Currency",
|
||||
@@ -429,7 +431,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "addtional_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information",
|
||||
"label": "Additional Info",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text"
|
||||
},
|
||||
@@ -476,7 +478,7 @@
|
||||
{
|
||||
"fieldname": "subscription_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Section"
|
||||
"label": "Subscription"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -593,12 +595,10 @@
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.apply_tds && doc.docstatus == 0",
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "section_tax_withholding_entry",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Withholding Entry"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_group",
|
||||
@@ -624,6 +624,33 @@
|
||||
"label": "Tax Withholding Entries",
|
||||
"options": "Tax Withholding Entry",
|
||||
"read_only_depends_on": "eval: !doc.override_tax_withholding_entries"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ouaq",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_cixu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oizh",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_isfa",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.apply_tds",
|
||||
"fieldname": "tax_withholding_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Tax Withholding"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -638,7 +665,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-03 14:40:39.944524",
|
||||
"modified": "2026-02-16 16:06:10.468482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 2,
|
||||
"columns": 4,
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
@@ -191,7 +191,6 @@
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type",
|
||||
@@ -294,7 +293,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-19 17:01:22.642454",
|
||||
"modified": "2026-02-16 16:04:16.022407",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -1065,8 +1065,12 @@ class PaymentEntry(AccountsController):
|
||||
total_allocated_amount += flt(d.allocated_amount)
|
||||
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
self.total_allocated_amount = abs(total_allocated_amount)
|
||||
self.base_total_allocated_amount = abs(base_total_allocated_amount)
|
||||
self.total_allocated_amount = flt(
|
||||
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):
|
||||
self.unallocated_amount = 0
|
||||
|
||||
@@ -4,19 +4,6 @@
|
||||
frappe.ui.form.on("POS Closing Entry", {
|
||||
onload: async function (frm) {
|
||||
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) {
|
||||
return { filters: { status: "Open", docstatus: 1 } };
|
||||
});
|
||||
|
||||
@@ -159,15 +159,16 @@
|
||||
"language",
|
||||
"column_break_84",
|
||||
"select_print_heading",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_bhao",
|
||||
"utm_campaign",
|
||||
"more_information",
|
||||
"inter_company_invoice_reference",
|
||||
"customer_group",
|
||||
"is_discounted",
|
||||
"col_break23",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"column_break_gpiw",
|
||||
"status",
|
||||
"more_info",
|
||||
"debit_to",
|
||||
@@ -1541,10 +1542,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gpiw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"fieldtype": "Link",
|
||||
@@ -1612,12 +1609,22 @@
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "UTM Analytics"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_bhao",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:20:51.376875",
|
||||
"modified": "2026-02-10 14:23:07.181782",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
"disabled",
|
||||
"column_break_9",
|
||||
"warehouse",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"company_address",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
@@ -61,7 +58,13 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
"project",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"column_break_tvls",
|
||||
"utm_campaign",
|
||||
"column_break_xygw",
|
||||
"utm_medium"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -430,6 +433,20 @@
|
||||
"fieldname": "allow_partial_payment",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Partial Payment"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_tvls",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xygw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Campaign"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -458,7 +475,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-24 11:19:19.834905",
|
||||
"modified": "2026-02-10 14:24:48.597412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -346,8 +346,7 @@ def apply_pricing_rule(args, doc=None):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not args.transaction_type:
|
||||
set_transaction_type(args)
|
||||
set_transaction_type(args)
|
||||
|
||||
# list of dictionaries
|
||||
out = []
|
||||
@@ -683,23 +682,23 @@ def remove_pricing_rules(item_list):
|
||||
return out
|
||||
|
||||
|
||||
def set_transaction_type(args):
|
||||
if args.transaction_type:
|
||||
def set_transaction_type(pricing_ctx: frappe._dict) -> None:
|
||||
if pricing_ctx.transaction_type in ["buying", "selling"]:
|
||||
return
|
||||
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
args.transaction_type = "selling"
|
||||
elif args.doctype in (
|
||||
if pricing_ctx.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
pricing_ctx.transaction_type = "selling"
|
||||
elif pricing_ctx.doctype in (
|
||||
"Material Request",
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
):
|
||||
args.transaction_type = "buying"
|
||||
elif args.customer:
|
||||
args.transaction_type = "selling"
|
||||
pricing_ctx.transaction_type = "buying"
|
||||
elif pricing_ctx.customer:
|
||||
pricing_ctx.transaction_type = "selling"
|
||||
else:
|
||||
args.transaction_type = "buying"
|
||||
pricing_ctx.transaction_type = "buying"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -219,16 +219,17 @@
|
||||
"column_break_140",
|
||||
"to_date",
|
||||
"update_auto_repeat_reference",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_ixxw",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"more_information",
|
||||
"status",
|
||||
"remarks",
|
||||
"customer_group",
|
||||
"column_break_imbx",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"inter_company_invoice_reference",
|
||||
@@ -270,6 +271,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Customer",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -798,8 +800,7 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheets",
|
||||
"options": "Sales Invoice Timesheet",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1632,13 +1633,6 @@
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break23",
|
||||
"fieldtype": "Column Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Draft",
|
||||
"fieldname": "status",
|
||||
@@ -2313,6 +2307,16 @@
|
||||
{
|
||||
"fieldname": "column_break_rdks",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ixxw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "UTM Analytics"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -2326,7 +2330,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-23 14:29:00.301842",
|
||||
"modified": "2026-02-28 17:58:56.453076",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -119,7 +119,7 @@ class SalesInvoice(SellingController):
|
||||
cost_center: DF.Link | None
|
||||
coupon_code: DF.Link | None
|
||||
currency: DF.Link
|
||||
customer: DF.Link | None
|
||||
customer: DF.Link
|
||||
customer_address: DF.Link | None
|
||||
customer_group: DF.Link | None
|
||||
customer_name: DF.SmallText | None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
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.report.accounts_payable.accounts_payable import execute
|
||||
@@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, IntegrationTestCase):
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
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.customer = qb.DocType("Customer")
|
||||
|
||||
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 = (
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
@@ -1049,14 +1048,18 @@ class ReceivablePayableReport:
|
||||
self.get_hierarchical_filters("Territory", "territory")
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
customer_ptt = self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.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"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
@@ -1081,14 +1084,53 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.payment_terms == self.filters.get("supplier_group"))
|
||||
)
|
||||
supplier_ptt = self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.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):
|
||||
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 []
|
||||
|
||||
|
||||
def get_customer_group_with_children(customer_groups):
|
||||
if not isinstance(customer_groups, list):
|
||||
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
|
||||
def get_party_group_with_children(party, party_groups):
|
||||
if party not in ("Customer", "Supplier"):
|
||||
return []
|
||||
|
||||
all_customer_groups = []
|
||||
for d in customer_groups:
|
||||
if frappe.db.exists("Customer Group", d):
|
||||
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_customer_groups += [c.name for c in children]
|
||||
group_dtype = f"{party} Group"
|
||||
if not isinstance(party_groups, list):
|
||||
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
|
||||
|
||||
all_party_groups = []
|
||||
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:
|
||||
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:
|
||||
|
||||
@@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
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.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.controllers.trends import get_period_date_ranges
|
||||
|
||||
@@ -13,6 +14,8 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
if 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
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
validate_budget_dimensions(filters)
|
||||
|
||||
|
||||
def get_budget_records(filters, dimensions):
|
||||
budget_against_field = frappe.scrub(filters["budget_against"])
|
||||
|
||||
@@ -51,7 +58,7 @@ def get_budget_records(filters, dimensions):
|
||||
b.company = %s
|
||||
AND b.docstatus = 1
|
||||
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 (
|
||||
b.from_fiscal_year <= %s
|
||||
AND b.to_fiscal_year >= %s
|
||||
@@ -404,6 +411,17 @@ def get_budget_dimensions(filters):
|
||||
) # 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):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
@@ -86,6 +86,12 @@ frappe.query_reports["Consolidated Trial Balance"] = {
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_net_values",
|
||||
label: __("Show net values in opening and closing columns"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "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 (
|
||||
accumulate_values_into_parents,
|
||||
calculate_total_row,
|
||||
calculate_values,
|
||||
get_opening_balances,
|
||||
hide_group_accounts,
|
||||
@@ -44,7 +45,6 @@ def execute(filters: dict | None = None):
|
||||
|
||||
def validate_filters(filters):
|
||||
validate_companies(filters)
|
||||
filters.show_net_values = True
|
||||
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)
|
||||
consolidate_trial_balance_data(data, tb_data)
|
||||
|
||||
for d in data:
|
||||
prepare_opening_closing(d)
|
||||
|
||||
total_row = calculate_total_row(data, reporting_currency)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
if filters.get("show_net_values"):
|
||||
prepare_opening_closing_for_ctb(data)
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
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"):
|
||||
update_to_presentation_currency(
|
||||
data,
|
||||
@@ -207,10 +211,6 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
|
||||
data = []
|
||||
|
||||
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
|
||||
row = {
|
||||
"account": d.name,
|
||||
@@ -242,35 +242,9 @@ def prepare_companywise_tb_data(accounts, filters, parent_children_map, reportin
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, reporting_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"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):
|
||||
def calculate_foreign_currency_translation_reserve(total_row, data, filters):
|
||||
if not data or not total_row:
|
||||
return
|
||||
opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_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"),
|
||||
"account_type": "Equity",
|
||||
"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,
|
||||
"currency": total_row.get("currency"),
|
||||
}
|
||||
@@ -297,7 +271,8 @@ def calculate_foreign_currency_translation_reserve(total_row, data):
|
||||
fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"]
|
||||
fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"]
|
||||
|
||||
prepare_opening_closing(fctr_row)
|
||||
if filters.get("show_net_values"):
|
||||
prepare_opening_closing(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)
|
||||
|
||||
|
||||
def prepare_opening_closing_for_ctb(data):
|
||||
for d in data:
|
||||
prepare_opening_closing(d)
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -390,7 +390,7 @@ def calculate_values(
|
||||
prepare_opening_closing(d)
|
||||
|
||||
|
||||
def calculate_total_row(accounts, company_currency):
|
||||
def calculate_total_row(data, company_currency, show_group_accounts=True):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
@@ -407,10 +407,16 @@ def calculate_total_row(accounts, company_currency):
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
if not d.parent_account:
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
def sum_value_fields(row):
|
||||
for field in value_fields:
|
||||
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
|
||||
|
||||
@@ -456,11 +462,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
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])
|
||||
|
||||
return data
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"order_confirmation_date",
|
||||
"column_break_7",
|
||||
"transaction_date",
|
||||
"transaction_time",
|
||||
"schedule_date",
|
||||
"column_break1",
|
||||
"company",
|
||||
@@ -1311,6 +1312,14 @@
|
||||
{
|
||||
"fieldname": "section_break_tnkm",
|
||||
"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,
|
||||
@@ -1318,7 +1327,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-23 14:22:33.323946",
|
||||
"modified": "2026-03-02 00:40:47.119584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -166,6 +166,7 @@ class PurchaseOrder(BuyingController):
|
||||
total_qty: DF.Float
|
||||
total_taxes_and_charges: DF.Currency
|
||||
transaction_date: DF.Date
|
||||
transaction_time: DF.Time | None
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -250,10 +250,17 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
"subject",
|
||||
])
|
||||
.then((r) => {
|
||||
frm.set_value(
|
||||
"message_for_supplier",
|
||||
r.message.use_html ? r.message.response_html : r.message.response
|
||||
);
|
||||
if (r.message.use_html) {
|
||||
frm.set_value({
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
"send_document_print",
|
||||
"sec_break_email_2",
|
||||
"subject",
|
||||
"use_html",
|
||||
"message_for_supplier",
|
||||
"mfs_html",
|
||||
"terms_section_break",
|
||||
"incoterm",
|
||||
"named_place",
|
||||
@@ -142,12 +144,13 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Please supply the specified items at the best possible rates",
|
||||
"depends_on": "eval:doc.use_html == 0",
|
||||
"fieldname": "message_for_supplier",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Message for Supplier",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:doc.use_html == 0",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -324,6 +327,22 @@
|
||||
"label": "Subject",
|
||||
"not_nullable": 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,
|
||||
@@ -331,7 +350,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-06 10:31:08.747043",
|
||||
"modified": "2026-03-01 23:38:48.079274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -47,7 +47,8 @@ class RequestforQuotation(BuyingController):
|
||||
incoterm: DF.Link | None
|
||||
items: DF.Table[RequestforQuotationItem]
|
||||
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
|
||||
naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
|
||||
opportunity: DF.Link | None
|
||||
@@ -61,6 +62,7 @@ class RequestforQuotation(BuyingController):
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
transaction_date: DF.Date
|
||||
use_html: DF.Check
|
||||
vendor: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -100,8 +102,16 @@ class RequestforQuotation(BuyingController):
|
||||
["use_html", "response", "response_html", "subject"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not self.message_for_supplier:
|
||||
self.message_for_supplier = data.response_html if data.use_html else data.response
|
||||
|
||||
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:
|
||||
self.message_for_supplier = data.response
|
||||
|
||||
if not self.subject:
|
||||
self.subject = data.subject
|
||||
|
||||
@@ -304,7 +314,10 @@ class RequestforQuotation(BuyingController):
|
||||
else:
|
||||
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 = (
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
|
||||
@@ -165,7 +165,7 @@ def get_data(filters):
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"requestor": mr_record.get("owner", po.owner),
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
|
||||
@@ -1012,7 +1012,14 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
|
||||
|
||||
if doctype == "Packed Item":
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
key = frappe.get_cached_value(
|
||||
"Packed Item",
|
||||
{"parent_detail_docname": row.voucher_detail_no, "item_code": row.item_code},
|
||||
field,
|
||||
)
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
|
||||
if row.voucher_type == "Delivery Note":
|
||||
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
||||
elif row.voucher_type == "Sales Invoice":
|
||||
|
||||
@@ -333,9 +333,10 @@ class SellingController(StockController):
|
||||
if is_internal_customer or not is_stock_item:
|
||||
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(
|
||||
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(
|
||||
|
||||
@@ -63,6 +63,8 @@ class StockController(AccountsController):
|
||||
|
||||
if not self.get("is_return"):
|
||||
self.validate_inspection()
|
||||
|
||||
self.validate_warehouse_of_sabb()
|
||||
self.validate_serialized_batch()
|
||||
self.clean_serial_nos()
|
||||
self.validate_customer_provided_item()
|
||||
@@ -75,6 +77,45 @@ class StockController(AccountsController):
|
||||
super().on_update()
|
||||
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):
|
||||
for row in self.get("items"):
|
||||
if row.uom != row.stock_uom:
|
||||
@@ -2087,7 +2128,9 @@ def check_item_quality_inspection(doctype, items):
|
||||
|
||||
|
||||
@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):
|
||||
items = json.loads(items)
|
||||
|
||||
@@ -2106,6 +2149,7 @@ def make_quality_inspections(doctype, docname, items, inspection_type):
|
||||
|
||||
quality_inspection = frappe.get_doc(
|
||||
{
|
||||
"company": company,
|
||||
"doctype": "Quality Inspection",
|
||||
"inspection_type": inspection_type,
|
||||
"inspected_by": frappe.session.user,
|
||||
|
||||
@@ -52,13 +52,12 @@
|
||||
"country",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"section_break_analytics",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_content",
|
||||
"utm_medium",
|
||||
"column_break_gkxo",
|
||||
"utm_campaign",
|
||||
"column_break_gqka",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"qualification_tab",
|
||||
"qualification_status",
|
||||
"column_break_64",
|
||||
@@ -504,10 +503,6 @@
|
||||
"fieldname": "column_break_gkxo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gqka",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_content",
|
||||
"fieldtype": "Data",
|
||||
@@ -537,7 +532,8 @@
|
||||
"options": "UTM Campaign"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_analytics",
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Analytics"
|
||||
}
|
||||
@@ -547,7 +543,7 @@
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2025-06-26 11:02:01.158901",
|
||||
"modified": "2026-02-10 14:36:37.157961",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
if (me.frm.doc.opportunity_from == "Lead") {
|
||||
|
||||
@@ -44,13 +44,12 @@
|
||||
"column_break_17",
|
||||
"opportunity_amount",
|
||||
"base_opportunity_amount",
|
||||
"section_break_analytics",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_content",
|
||||
"utm_medium",
|
||||
"column_break_emai",
|
||||
"utm_campaign",
|
||||
"column_break_whcu",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"more_info",
|
||||
"company",
|
||||
"transaction_date",
|
||||
@@ -629,15 +628,6 @@
|
||||
"options": "UTM Campaign",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_analytics",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Analytics"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_whcu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"fieldtype": "Link",
|
||||
@@ -650,13 +640,19 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Content",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Analytics"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2025-08-11 13:35:39.476016",
|
||||
"modified": "2026-02-10 14:36:01.387984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
||||
@@ -59,7 +59,9 @@ def create_prospect_against_crm_deal():
|
||||
)
|
||||
pass
|
||||
|
||||
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
||||
if doc.contacts and len(doc.contacts):
|
||||
create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name)
|
||||
|
||||
create_address("Prospect", prospect_name, doc.address)
|
||||
frappe.response["message"] = prospect_name
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -225,7 +225,12 @@ class WorkOrder(Document):
|
||||
frappe.throw(_("Actual End Date cannot be before Actual Start Date"))
|
||||
|
||||
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(
|
||||
"Sales Order Item",
|
||||
filters={"parent": self.sales_order, "item_code": self.production_item},
|
||||
@@ -413,39 +418,52 @@ class WorkOrder(Document):
|
||||
)
|
||||
|
||||
def validate_sales_order(self):
|
||||
if self.production_plan_sub_assembly_item:
|
||||
return
|
||||
|
||||
if self.sales_order:
|
||||
self.check_sales_order_on_hold_or_close()
|
||||
so = frappe.db.sql(
|
||||
"""
|
||||
select so.name, so_item.delivery_date, so.project
|
||||
from `tabSales Order` so
|
||||
inner join `tabSales Order Item` so_item on so_item.parent = so.name
|
||||
left join `tabProduct Bundle Item` pk_item on so_item.item_code = pk_item.parent
|
||||
where so.name=%s and so.docstatus = 1
|
||||
and so.skip_delivery_note = 0 and (
|
||||
so_item.item_code=%s or
|
||||
pk_item.item_code=%s )
|
||||
""",
|
||||
(self.sales_order, self.production_item, self.production_item),
|
||||
as_dict=1,
|
||||
|
||||
SalesOrder = frappe.qb.DocType("Sales Order")
|
||||
SalesOrderItem = frappe.qb.DocType("Sales Order Item")
|
||||
PackedItem = frappe.qb.DocType("Packed Item")
|
||||
ProductBundleItem = frappe.qb.DocType("Product Bundle Item")
|
||||
|
||||
so = (
|
||||
frappe.qb.from_(SalesOrder)
|
||||
.inner_join(SalesOrderItem)
|
||||
.on(SalesOrderItem.parent == SalesOrder.name)
|
||||
.left_join(ProductBundleItem)
|
||||
.on(ProductBundleItem.parent == SalesOrderItem.item_code)
|
||||
.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:
|
||||
so = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
so.name, so_item.delivery_date, so.project
|
||||
from
|
||||
`tabSales Order` so, `tabSales Order Item` so_item, `tabPacked Item` packed_item
|
||||
where so.name=%s
|
||||
and so.name=so_item.parent
|
||||
and so.name=packed_item.parent
|
||||
and so.skip_delivery_note = 0
|
||||
and so_item.item_code = packed_item.parent_item
|
||||
and so.docstatus = 1 and packed_item.item_code=%s
|
||||
""",
|
||||
(self.sales_order, self.production_item),
|
||||
as_dict=1,
|
||||
so = (
|
||||
frappe.qb.from_(SalesOrder)
|
||||
.inner_join(SalesOrderItem)
|
||||
.on(SalesOrderItem.parent == SalesOrder.name)
|
||||
.inner_join(PackedItem)
|
||||
.on(PackedItem.parent == SalesOrder.name)
|
||||
.select(SalesOrder.name, SalesOrder.project, SalesOrderItem.delivery_date)
|
||||
.where(
|
||||
(SalesOrder.name == self.sales_order)
|
||||
& (SalesOrder.skip_delivery_note == 0)
|
||||
& (SalesOrderItem.item_code == PackedItem.parent_item)
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (PackedItem.item_code == self.production_item)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
if self.production_plan:
|
||||
@@ -1159,7 +1177,7 @@ class WorkOrder(Document):
|
||||
doc.db_set("status", doc.status)
|
||||
|
||||
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
|
||||
|
||||
total_bundle_qty = 1
|
||||
|
||||
@@ -191,6 +191,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
tax.item_wise_tax_detail = {};
|
||||
}
|
||||
var tax_fields = [
|
||||
"net_amount",
|
||||
"total",
|
||||
"tax_amount_after_discount_amount",
|
||||
"tax_amount_for_current_item",
|
||||
@@ -400,9 +401,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||
$.each(doc.taxes, function (i, tax) {
|
||||
// tax_amount represents the amount of tax for the current step
|
||||
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
|
||||
var [current_net_amount, current_tax_amount] = me.get_current_tax_amount(
|
||||
item,
|
||||
tax,
|
||||
item_tax_map
|
||||
);
|
||||
if (frappe.flags.round_row_wise_tax) {
|
||||
current_tax_amount = flt(current_tax_amount, precision("tax_amount", tax));
|
||||
current_net_amount = flt(current_net_amount, precision("net_amount", tax));
|
||||
}
|
||||
|
||||
// Adjust divisional loss to the last item
|
||||
@@ -419,6 +425,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
!(me.discount_amount_applied && me.frm.doc.apply_discount_on == "Grand Total")
|
||||
) {
|
||||
tax.tax_amount += current_tax_amount;
|
||||
tax.net_amount += current_net_amount;
|
||||
}
|
||||
|
||||
// store tax_amount for current item as it will be used for
|
||||
@@ -480,7 +487,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
for (const [i, tax] of doc.taxes.entries()) {
|
||||
me.round_off_totals(tax);
|
||||
me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]);
|
||||
me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]);
|
||||
|
||||
me.round_off_base_values(tax);
|
||||
|
||||
@@ -555,7 +562,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
current_tax_amount = tax_rate * item.qty;
|
||||
}
|
||||
|
||||
return current_tax_amount;
|
||||
return [current_net_amount, current_tax_amount];
|
||||
}
|
||||
|
||||
round_off_totals(tax) {
|
||||
@@ -565,6 +572,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
tax.tax_amount = flt(tax.tax_amount, precision("tax_amount", tax));
|
||||
tax.net_amount = flt(tax.net_amount, precision("net_amount", tax));
|
||||
tax.tax_amount_after_discount_amount = flt(
|
||||
tax.tax_amount_after_discount_amount,
|
||||
precision("tax_amount", tax)
|
||||
|
||||
@@ -2966,6 +2966,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||
args: {
|
||||
company: me.frm.doc.company,
|
||||
doctype: me.frm.doc.doctype,
|
||||
docname: me.frm.doc.name,
|
||||
items: selected_data,
|
||||
|
||||
@@ -118,12 +118,37 @@ class Customer(TransactionBase):
|
||||
def get_customer_name(self):
|
||||
self.customer_name = self.customer_name.strip()
|
||||
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
|
||||
count = frappe.db.sql(
|
||||
"""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer
|
||||
where name like %s""",
|
||||
f"%{self.customer_name} - %",
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
name_prefix = f"{self.customer_name} - %"
|
||||
|
||||
if frappe.db.db_type == "postgres":
|
||||
# Postgres: extract trailing digits (e.g. "Customer - 3") and cast to int.
|
||||
# NOTE: PostgreSQL is strict about types; MySQL's UNSIGNED cast does not exist.
|
||||
count = frappe.db.sql(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
MAX(CAST(SUBSTRING(name FROM '\\d+$') AS INTEGER)),
|
||||
0
|
||||
)
|
||||
FROM tabCustomer
|
||||
WHERE name LIKE %(name_prefix)s
|
||||
""",
|
||||
{"name_prefix": name_prefix},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
else:
|
||||
# MariaDB/MySQL: keep existing behavior.
|
||||
count = frappe.db.sql(
|
||||
"""
|
||||
SELECT COALESCE(
|
||||
MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)),
|
||||
0
|
||||
)
|
||||
FROM tabCustomer
|
||||
WHERE name LIKE %(name_prefix)s
|
||||
""",
|
||||
{"name_prefix": name_prefix},
|
||||
as_list=1,
|
||||
)[0][0]
|
||||
count = cint(count) + 1
|
||||
|
||||
new_customer_name = f"{self.customer_name} - {cstr(count)}"
|
||||
|
||||
@@ -123,19 +123,20 @@
|
||||
"competitors",
|
||||
"column_break_117",
|
||||
"order_lost_reason",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_fozg",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"additional_info_section",
|
||||
"status",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"column_break_108",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"column_break4",
|
||||
"opportunity",
|
||||
"supplier_quotation",
|
||||
"enq_det",
|
||||
"supplier_quotation",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -862,13 +863,6 @@
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break4",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"print_hide": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Draft",
|
||||
"fieldname": "status",
|
||||
@@ -1117,13 +1111,23 @@
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_fozg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "UTM Analytics"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-29 21:18:48.836168",
|
||||
"modified": "2026-02-06 17:34:22.170032",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"column_break_7",
|
||||
"order_type",
|
||||
"transaction_date",
|
||||
"transaction_time",
|
||||
"delivery_date",
|
||||
"column_break1",
|
||||
"tax_id",
|
||||
@@ -161,17 +162,18 @@
|
||||
"column_break4",
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_ijxt",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"additional_info_section",
|
||||
"is_internal_customer",
|
||||
"po_no",
|
||||
"po_date",
|
||||
"represents_company",
|
||||
"column_break_yvzv",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"column_break_152",
|
||||
"inter_company_order_reference",
|
||||
"party_account_currency",
|
||||
"connections_tab"
|
||||
@@ -1567,10 +1569,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_152",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "incoterm",
|
||||
"fieldtype": "Link",
|
||||
@@ -1717,6 +1715,24 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Additional Discount"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ijxt",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "UTM Analytics"
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"depends_on": "is_internal_customer",
|
||||
"fieldname": "transaction_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Time",
|
||||
"mandatory_depends_on": "is_internal_customer"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1724,7 +1740,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-23 14:25:56.665392",
|
||||
"modified": "2026-03-02 00:42:18.834823",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -186,6 +186,7 @@ class SalesOrder(SellingController):
|
||||
total_qty: DF.Float
|
||||
total_taxes_and_charges: DF.Currency
|
||||
transaction_date: DF.Date
|
||||
transaction_time: DF.Time | None
|
||||
utm_campaign: DF.Link | None
|
||||
utm_content: DF.Data | None
|
||||
utm_medium: DF.Link | None
|
||||
|
||||
@@ -2647,6 +2647,49 @@ class TestSalesOrder(AccountsTestMixin, IntegrationTestCase):
|
||||
si2 = make_sales_invoice(so.name)
|
||||
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):
|
||||
for index, schedule in enumerate(doc1.get("payment_schedule")):
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
"allow_zero_qty_in_quotation",
|
||||
"allow_zero_qty_in_sales_order",
|
||||
"set_zero_rate_for_expired_batch",
|
||||
"section_break_avhb",
|
||||
"enable_utm",
|
||||
"experimental_section",
|
||||
"use_legacy_js_reactivity",
|
||||
"subcontracting_inward_tab",
|
||||
@@ -305,6 +307,19 @@
|
||||
"fieldname": "enable_tracking_sales_commissions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable tracking sales commissions"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_avhb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Analytics"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable Urchin Tracking Module parameters in Quotation, Sales Order, Sales Invoice, POS Invoice, Lead, and Delivery Note.",
|
||||
"documentation_url": "https://en.wikipedia.org/wiki/UTM_parameters",
|
||||
"fieldname": "enable_utm",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable UTM"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -314,7 +329,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-04 16:16:57.618127",
|
||||
"modified": "2026-02-12 10:38:34.605126",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
||||
@@ -10,6 +10,17 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
UTM_DOCTYPES = [
|
||||
"Lead",
|
||||
"Quotation",
|
||||
"POS Invoice",
|
||||
"POS Profile",
|
||||
"Opportunity",
|
||||
"Sales Order",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
]
|
||||
|
||||
|
||||
class SellingSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -38,6 +49,7 @@ class SellingSettings(Document):
|
||||
enable_cutoff_date_on_bulk_delivery_note_creation: DF.Check
|
||||
enable_discount_accounting: DF.Check
|
||||
enable_tracking_sales_commissions: DF.Check
|
||||
enable_utm: DF.Check
|
||||
fallback_to_default_price_list: DF.Check
|
||||
hide_tax_id: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
@@ -84,6 +96,9 @@ class SellingSettings(Document):
|
||||
if old_doc.enable_tracking_sales_commissions != self.enable_tracking_sales_commissions:
|
||||
toggle_tracking_sales_commissions_section(not self.enable_tracking_sales_commissions)
|
||||
|
||||
if old_doc.enable_utm != self.enable_utm:
|
||||
toggle_utm_analytics_section(not self.enable_utm)
|
||||
|
||||
def validate_fallback_to_default_price_list(self):
|
||||
if (
|
||||
self.fallback_to_default_price_list
|
||||
@@ -195,3 +210,14 @@ def toggle_tracking_sales_commissions_section(hide):
|
||||
create_property_setter_for_hiding_field(doctype, "commission_section", hide)
|
||||
if meta.has_field("sales_team_section"):
|
||||
create_property_setter_for_hiding_field(doctype, "sales_team_section", hide)
|
||||
|
||||
|
||||
def toggle_utm_analytics_section(hide):
|
||||
from erpnext.accounts.doctype.accounts_settings.accounts_settings import (
|
||||
create_property_setter_for_hiding_field,
|
||||
)
|
||||
|
||||
for doctype in UTM_DOCTYPES:
|
||||
meta = frappe.get_meta(doctype)
|
||||
if meta.has_field("utm_analytics_section"):
|
||||
create_property_setter_for_hiding_field(doctype, "utm_analytics_section", hide)
|
||||
|
||||
@@ -156,17 +156,18 @@
|
||||
"column_break_88",
|
||||
"select_print_heading",
|
||||
"language",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_neoj",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"more_info",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"inter_company_reference",
|
||||
"customer_group",
|
||||
"territory",
|
||||
"column_break_pxls",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"column_break5",
|
||||
"excise_page",
|
||||
"instructions",
|
||||
@@ -1366,10 +1367,6 @@
|
||||
"options": "Delivery Trip",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_pxls",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"fieldtype": "Link",
|
||||
@@ -1438,7 +1435,13 @@
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ydwe",
|
||||
"collapsible": 1,
|
||||
"fieldname": "utm_analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "UTM Analytics"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_neoj",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
@@ -1446,7 +1449,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-23 23:05:39.097097",
|
||||
"modified": "2026-02-10 14:35:08.523130",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
class IncorrectCompanyValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class LandedCostVoucher(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -75,6 +79,7 @@ class LandedCostVoucher(Document):
|
||||
self.check_mandatory()
|
||||
self.validate_receipt_documents()
|
||||
self.validate_line_items()
|
||||
self.validate_expense_accounts()
|
||||
init_landed_taxes_and_totals(self)
|
||||
self.set_total_taxes_and_charges()
|
||||
if not self.get("items"):
|
||||
@@ -116,11 +121,28 @@ class LandedCostVoucher(Document):
|
||||
receipt_documents = []
|
||||
|
||||
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:
|
||||
msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
|
||||
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":
|
||||
update_stock = frappe.db.get_value(
|
||||
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)
|
||||
)
|
||||
|
||||
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):
|
||||
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_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):
|
||||
"Test impact of LCV on future stock balances."
|
||||
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.company = args.company or "_Test Company"
|
||||
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
|
||||
expense_account = get_expense_account(args.company or "_Test Company")
|
||||
|
||||
lcv.set(
|
||||
"purchase_receipts",
|
||||
@@ -1280,7 +1314,7 @@ def make_landed_cost_voucher(**args):
|
||||
[
|
||||
{
|
||||
"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,
|
||||
}
|
||||
],
|
||||
@@ -1300,6 +1334,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||
lcv.company = company
|
||||
lcv.distribute_charges_based_on = "Amount"
|
||||
expense_account = get_expense_account(company)
|
||||
|
||||
lcv.set(
|
||||
"purchase_receipts",
|
||||
@@ -1319,7 +1354,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
||||
[
|
||||
{
|
||||
"description": "Insurance Charges",
|
||||
"expense_account": "Expenses Included In Valuation - TCP1",
|
||||
"expense_account": expense_account,
|
||||
"amount": charges,
|
||||
}
|
||||
],
|
||||
@@ -1334,6 +1369,11 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company,
|
||||
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):
|
||||
based_on = lcv.distribute_charges_based_on.lower()
|
||||
total = sum(flt(d.get(based_on)) for d in lcv.get("items"))
|
||||
|
||||
@@ -330,7 +330,8 @@ class MaterialRequest(BuyingController):
|
||||
|
||||
if mr_qty_allowance:
|
||||
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):
|
||||
|
||||
@@ -1587,7 +1587,7 @@ def update_common_item_properties(item, location):
|
||||
item.item_code = location.item_code
|
||||
item.s_warehouse = location.warehouse
|
||||
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.conversion_factor = location.conversion_factor
|
||||
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):
|
||||
# Update Billing % based on pending accepted qty
|
||||
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
|
||||
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)
|
||||
elif amount and item.billed_amt > amount:
|
||||
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(
|
||||
_("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
|
||||
|
||||
@@ -142,7 +142,9 @@ class TestQualityInspection(IntegrationTestCase):
|
||||
inspection_type = "Outgoing"
|
||||
for item in dn.items:
|
||||
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))
|
||||
|
||||
# cleanup
|
||||
|
||||
@@ -237,7 +237,8 @@
|
||||
"fieldname": "reposting_reference",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reposting Reference",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -252,7 +253,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-24 14:59:15.512898",
|
||||
"modified": "2026-02-25 14:22:21.681549",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Repost Item Valuation",
|
||||
|
||||
@@ -470,7 +470,15 @@ def repost_gl_entries(doc):
|
||||
repost_affected_transaction = get_affected_transactions(doc)
|
||||
|
||||
transactions = directly_dependent_transactions + list(repost_affected_transaction)
|
||||
if doc.based_on == "Item and Warehouse" and not doc.repost_only_accounting_ledgers:
|
||||
enable_separate_reposting_for_gl = frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "enable_separate_reposting_for_gl"
|
||||
)
|
||||
|
||||
if (
|
||||
enable_separate_reposting_for_gl
|
||||
and doc.based_on == "Item and Warehouse"
|
||||
and not doc.repost_only_accounting_ledgers
|
||||
):
|
||||
make_reposting_for_accounting_ledgers(
|
||||
transactions,
|
||||
doc.company,
|
||||
@@ -562,7 +570,20 @@ def run_parallel_reposting():
|
||||
|
||||
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:
|
||||
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:
|
||||
execute_reposting_entry(row.name)
|
||||
continue
|
||||
@@ -671,25 +692,59 @@ def execute_repost_item_valuation():
|
||||
|
||||
|
||||
def make_reposting_for_accounting_ledgers(transactions, company, repost_doc):
|
||||
reposting_map = get_existing_reposting_only_gl_entries(repost_doc.name)
|
||||
|
||||
for voucher_type, voucher_no in transactions:
|
||||
if frappe.db.exists(
|
||||
"Repost Item Valuation",
|
||||
{
|
||||
"voucher_type": voucher_type,
|
||||
"voucher_no": voucher_no,
|
||||
"docstatus": 1,
|
||||
"reposting_reference": repost_doc.name,
|
||||
"repost_only_accounting_ledgers": 1,
|
||||
"status": "Queued",
|
||||
},
|
||||
):
|
||||
if reposting_map.get((voucher_type, voucher_no)):
|
||||
continue
|
||||
|
||||
new_repost_doc = frappe.new_doc("Repost Item Valuation")
|
||||
new_repost_doc.company = company
|
||||
new_repost_doc.voucher_type = voucher_type
|
||||
new_repost_doc.voucher_no = voucher_no
|
||||
new_repost_doc.repost_only_accounting_ledgers = 1
|
||||
new_repost_doc.reposting_reference = repost_doc.name
|
||||
new_repost_doc.flags.ignore_permissions = True
|
||||
new_repost_doc.submit()
|
||||
try:
|
||||
new_repost_doc = frappe.new_doc("Repost Item Valuation")
|
||||
new_repost_doc.company = company
|
||||
new_repost_doc.voucher_type = voucher_type
|
||||
new_repost_doc.voucher_no = voucher_no
|
||||
new_repost_doc.repost_only_accounting_ledgers = 1
|
||||
new_repost_doc.reposting_reference = repost_doc.name
|
||||
new_repost_doc.flags.ignore_permissions = True
|
||||
new_repost_doc.submit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def get_existing_reposting_only_gl_entries(reposting_reference):
|
||||
existing_reposting = frappe.get_all(
|
||||
"Repost Item Valuation",
|
||||
filters={
|
||||
"reposting_reference": reposting_reference,
|
||||
"docstatus": 1,
|
||||
"status": "Queued",
|
||||
"repost_only_accounting_ledgers": 1,
|
||||
},
|
||||
fields=["reposting_reference", "voucher_type", "voucher_no"],
|
||||
)
|
||||
|
||||
if not existing_reposting:
|
||||
return frappe._dict()
|
||||
|
||||
reposting_map = {}
|
||||
for d in existing_reposting:
|
||||
key = (d.voucher_type, d.voucher_no)
|
||||
reposting_map[key] = d.reposting_reference
|
||||
|
||||
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
|
||||
|
||||
@@ -717,10 +717,13 @@ class SerialandBatchBundle(Document):
|
||||
if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]:
|
||||
rate = frappe.db.get_value(
|
||||
"Packed Item",
|
||||
self.voucher_detail_no,
|
||||
{"parent_detail_docname": self.voucher_detail_no, "item_code": self.item_code},
|
||||
"incoming_rate",
|
||||
)
|
||||
|
||||
if rate is None:
|
||||
rate = frappe.db.get_value("Packed Item", self.voucher_detail_no, "incoming_rate")
|
||||
|
||||
if rate is not None:
|
||||
is_packed_item = True
|
||||
|
||||
@@ -787,6 +790,9 @@ class SerialandBatchBundle(Document):
|
||||
if not self.voucher_detail_no or self.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"):
|
||||
posting_datetime = combine_datetime(parent.posting_date, parent.posting_time)
|
||||
if not self.posting_datetime or self.posting_datetime != posting_datetime:
|
||||
@@ -1325,7 +1331,21 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
if not vouchers and self.voucher_type == "Delivery Note":
|
||||
frappe.db.set_value("Packed Item", self.voucher_detail_no, "serial_and_batch_bundle", None)
|
||||
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)
|
||||
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
|
||||
|
||||
for voucher in vouchers:
|
||||
|
||||
@@ -236,6 +236,7 @@ class StockEntry(StockController, SubcontractingInwardController):
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
||||
self.validate_warehouse()
|
||||
self.validate_warehouse_of_sabb()
|
||||
self.validate_work_order()
|
||||
self.validate_bom()
|
||||
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, "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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -498,7 +498,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Purchase Receipt",
|
||||
"options": "Purchase Receipt",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -660,7 +661,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-16 11:50:50.573443",
|
||||
"modified": "2026-03-02 14:05:23.116017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -523,9 +523,9 @@ class StockReconciliation(StockController):
|
||||
if abs(difference_amount) > 0:
|
||||
return True
|
||||
|
||||
float_precision = frappe.db.get_default("float_precision") or 3
|
||||
item_dict["rate"] = flt(item_dict.get("rate"), float_precision)
|
||||
item.valuation_rate = flt(item.valuation_rate, float_precision) if item.valuation_rate else None
|
||||
rate_precision = item.precision("valuation_rate")
|
||||
item_dict["rate"] = flt(item_dict.get("rate"), rate_precision)
|
||||
item.valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
|
||||
if (
|
||||
(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"))
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
"end_time",
|
||||
"limits_dont_apply_on",
|
||||
"item_based_reposting",
|
||||
"section_break_dxuf",
|
||||
"enable_parallel_reposting",
|
||||
"no_of_parallel_reposting",
|
||||
"column_break_itvd",
|
||||
"enable_separate_reposting_for_gl",
|
||||
"errors_notification_section",
|
||||
"notify_reposting_error_to_role"
|
||||
],
|
||||
@@ -81,13 +84,28 @@
|
||||
"fieldname": "no_of_parallel_reposting",
|
||||
"fieldtype": "Int",
|
||||
"label": "No of Parallel Reposting (Per Item)"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_dxuf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_itvd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "item_based_reposting",
|
||||
"fieldname": "enable_separate_reposting_for_gl",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Separate Reposting for GL"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-02 18:18:57.115176",
|
||||
"modified": "2026-02-25 14:11:33.461173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
|
||||
@@ -17,6 +17,7 @@ class StockRepostingSettings(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
enable_parallel_reposting: DF.Check
|
||||
enable_separate_reposting_for_gl: DF.Check
|
||||
end_time: DF.Time | None
|
||||
item_based_reposting: DF.Check
|
||||
limit_reposting_timeslot: DF.Check
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any, TypedDict
|
||||
|
||||
import frappe
|
||||
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.nestedset import get_descendants_of
|
||||
|
||||
@@ -165,6 +165,7 @@ class StockBalanceReport:
|
||||
sle.serial_no,
|
||||
sle.serial_and_batch_bundle,
|
||||
sle.has_serial_no,
|
||||
sle.voucher_detail_no,
|
||||
item_table.item_group,
|
||||
item_table.stock_uom,
|
||||
item_table.item_name,
|
||||
@@ -190,6 +191,8 @@ class StockBalanceReport:
|
||||
if self.filters.get("show_stock_ageing_data"):
|
||||
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
|
||||
_system_settings = frappe.get_cached_doc("System Settings")
|
||||
with frappe.db.unbuffered_cursor():
|
||||
@@ -207,6 +210,39 @@ class StockBalanceReport:
|
||||
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)
|
||||
)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
if not data:
|
||||
return
|
||||
|
||||
for row in data:
|
||||
if row.count != 1:
|
||||
continue
|
||||
|
||||
current_qty = frappe.db.get_value(
|
||||
"Stock Reconciliation Item", row.voucher_detail_no, "current_qty"
|
||||
)
|
||||
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = current_qty
|
||||
|
||||
def prepare_new_data(self):
|
||||
if self.filters.get("show_stock_ageing_data"):
|
||||
self.filters["show_warehouse_wise_stock"] = True
|
||||
@@ -283,9 +319,14 @@ class StockBalanceReport:
|
||||
qty_dict[field] = entry.get(field)
|
||||
|
||||
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
|
||||
):
|
||||
qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty)
|
||||
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)
|
||||
else:
|
||||
qty_diff = flt(entry.actual_qty)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ frappe.query_reports["Stock Ledger Invariant Check"] = {
|
||||
options: "Item",
|
||||
get_query: function () {
|
||||
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):
|
||||
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)
|
||||
|
||||
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:
|
||||
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")
|
||||
|
||||
query = (
|
||||
|
||||
@@ -1305,7 +1305,7 @@ class update_entries_after:
|
||||
else:
|
||||
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||
ref_doctype = "Packed Item"
|
||||
elif sle == "Subcontracting Receipt":
|
||||
elif sle.voucher_type == "Subcontracting Receipt":
|
||||
ref_doctype = "Subcontracting Receipt Supplied Item"
|
||||
else:
|
||||
ref_doctype = "Purchase Receipt Item Supplied"
|
||||
|
||||
@@ -341,6 +341,7 @@ class TransactionBase(StatusUpdater):
|
||||
args.update(
|
||||
{
|
||||
"posting_date": self.transaction_date,
|
||||
"posting_time": self.transaction_time,
|
||||
}
|
||||
)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user