Merge pull request #52925 from frappe/version-16-hotfix

chore: release v16
This commit is contained in:
ruthra kumar
2026-02-25 12:01:35 +05:30
committed by GitHub
201 changed files with 5658 additions and 1417 deletions

View File

@@ -20,6 +20,10 @@
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date", "confirm_before_resetting_posting_date",
"analytics_section",
"enable_accounting_dimensions",
"column_break_vtnr",
"enable_discounts_and_margin",
"journals_section", "journals_section",
"merge_similar_account_heads", "merge_similar_account_heads",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@@ -51,12 +55,16 @@
"allow_pegged_currencies_exchange_rates", "allow_pegged_currencies_exchange_rates",
"column_break_yuug", "column_break_yuug",
"stale_days", "stale_days",
"payments_tab",
"section_break_jpd0", "section_break_jpd0",
"auto_reconcile_payments", "auto_reconcile_payments",
"auto_reconciliation_job_trigger", "auto_reconciliation_job_trigger",
"reconciliation_queue_size", "reconciliation_queue_size",
"column_break_resa", "column_break_resa",
"exchange_gain_loss_posting_date", "exchange_gain_loss_posting_date",
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
"invoicing_settings_tab", "invoicing_settings_tab",
"accounts_transactions_settings_section", "accounts_transactions_settings_section",
"over_billing_allowance", "over_billing_allowance",
@@ -281,7 +289,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>", "description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\" rel=\"noopener noreferrer\">Common Party</a>",
"fieldname": "enable_common_party_accounting", "fieldname": "enable_common_party_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Common Party Accounting" "label": "Enable Common Party Accounting"
@@ -637,6 +645,49 @@
"fieldname": "budget_section", "fieldname": "budget_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Budget" "label": "Budget"
},
{
"fieldname": "analytics_section",
"fieldtype": "Section Break",
"label": "Analytical Accounting"
},
{
"fieldname": "column_break_vtnr",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Apply discounts and margins on products",
"fieldname": "enable_discounts_and_margin",
"fieldtype": "Check",
"label": "Enable Discounts and Margin"
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"fieldname": "payment_options_section",
"fieldtype": "Section Break",
"label": "Payment Options"
},
{
"default": "0",
"fieldname": "enable_loyalty_point_program",
"fieldtype": "Check",
"label": "Enable Loyalty Point Program"
},
{
"fieldname": "column_break_ctam",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable cost center, projects and other custom accounting dimensions",
"fieldname": "enable_accounting_dimensions",
"fieldtype": "Check",
"label": "Enable Accounting Dimensions"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -646,7 +697,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-01-11 18:30:45.968531", "modified": "2026-02-04 17:15:38.609327",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -12,6 +12,28 @@ from frappe.utils import cint
from erpnext.accounts.utils import sync_auto_reconcile_config from erpnext.accounts.utils import sync_auto_reconcile_config
SELLING_DOCTYPES = [
"Sales Invoice",
"Sales Order",
"Delivery Note",
"Quotation",
"Sales Invoice Item",
"Sales Order Item",
"Delivery Note Item",
"Quotation Item",
"POS Invoice",
"POS Invoice Item",
]
BUYING_DOCTYPES = [
"Purchase Invoice",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice Item",
"Purchase Order Item",
"Purchase Receipt Item",
]
class AccountsSettings(Document): class AccountsSettings(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -43,9 +65,12 @@ class AccountsSettings(Document):
default_ageing_range: DF.Data | None default_ageing_range: DF.Data | None
delete_linked_ledger_entries: DF.Check delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
enable_accounting_dimensions: DF.Check
enable_common_party_accounting: DF.Check enable_common_party_accounting: DF.Check
enable_discounts_and_margin: DF.Check
enable_fuzzy_matching: DF.Check enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_valuation_rate_for_internal_transaction: DF.Check fetch_valuation_rate_for_internal_transaction: DF.Check
@@ -98,6 +123,18 @@ class AccountsSettings(Document):
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print: if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
if old_doc.enable_accounting_dimensions != self.enable_accounting_dimensions:
toggle_accounting_dimension_sections(not self.enable_accounting_dimensions)
clear_cache = True
if old_doc.enable_discounts_and_margin != self.enable_discounts_and_margin:
toggle_sales_discount_section(not self.enable_discounts_and_margin)
clear_cache = True
if old_doc.enable_loyalty_point_program != self.enable_loyalty_point_program:
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
clear_cache = True
if clear_cache: if clear_cache:
frappe.clear_cache() frappe.clear_cache()
@@ -154,3 +191,36 @@ class AccountsSettings(Document):
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
def toggle_accounting_dimension_sections(hide):
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
for doctype in accounting_dimension_doctypes:
create_property_setter_for_hiding_field(doctype, "accounting_dimensions_section", hide)
def toggle_sales_discount_section(hide):
for doctype in SELLING_DOCTYPES + BUYING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("additional_discount_section"):
create_property_setter_for_hiding_field(doctype, "additional_discount_section", hide)
if meta.has_field("discount_and_margin"):
create_property_setter_for_hiding_field(doctype, "discount_and_margin", hide)
def toggle_loyalty_point_program_section(hide):
for doctype in SELLING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("loyalty_points_redemption"):
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,
field_name,
"hidden",
hide,
"Check",
validate_fields_for_doctype=False,
)

View File

@@ -139,6 +139,8 @@ class BankTransaction(Document):
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry) self.delink_payment_entry(payment_entry)
@@ -373,11 +375,12 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
("unallocated_amount", "bank_account"), ("unallocated_amount", "bank_account"),
as_dict=True, as_dict=True,
) )
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
if bt.bank_account != gl_bank_account: if bt_bank_account != gl_bank_account:
frappe.throw( frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format( _("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
bt.bank_account, payment_entry.payment_entry, gl_bank_account bt_bank_account, payment_entry.payment_entry, gl_bank_account
) )
) )

View File

@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cstr, date_diff, flt, getdate from frappe.utils import cstr, date_diff, flt, getdate
from pypika.terms import LiteralValue from pypika.terms import Bracket, LiteralValue
from erpnext import get_company_currency from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -732,7 +732,7 @@ class FinancialQueryBuilder:
user_conditions = build_match_conditions(doctype) user_conditions = build_match_conditions(doctype)
if user_conditions: if user_conditions:
query = query.where(LiteralValue(user_conditions)) query = query.where(Bracket(LiteralValue(user_conditions)))
return query.run(as_dict=True) return query.run(as_dict=True)

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe import _ from frappe import _, cint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate from frappe.utils import add_days, add_years, cstr, getdate
@@ -33,24 +33,6 @@ class FiscalYear(Document):
self.validate_dates() self.validate_dates()
self.validate_overlap() self.validate_overlap()
if not self.is_new():
year_start_end_dates = frappe.db.sql(
"""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""",
(self.name),
)
if year_start_end_dates:
if (
getdate(self.year_start_date) != year_start_end_dates[0][0]
or getdate(self.year_end_date) != year_start_end_dates[0][1]
):
frappe.throw(
_(
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
)
)
def validate_dates(self): def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date") self.validate_from_to_dates("year_start_date", "year_end_date")
if self.is_short_year: if self.is_short_year:
@@ -66,28 +48,20 @@ class FiscalYear(Document):
frappe.exceptions.InvalidDates, frappe.exceptions.InvalidDates,
) )
def on_update(self):
check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years")
def on_trash(self):
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self): def validate_overlap(self):
existing_fiscal_years = frappe.db.sql( fy = frappe.qb.DocType("Fiscal Year")
"""select name from `tabFiscal Year`
where ( name = self.name or self.year
(%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date) existing_fiscal_years = (
or (year_start_date between %(year_start_date)s and %(year_end_date)s) frappe.qb.from_(fy)
or (year_end_date between %(year_start_date)s and %(year_end_date)s) .select(fy.name)
) and name!=%(name)s""", .where(
{ (fy.year_start_date <= self.year_end_date)
"year_start_date": self.year_start_date, & (fy.year_end_date >= self.year_start_date)
"year_end_date": self.year_end_date, & (fy.name != name)
"name": self.name or "No Name", )
}, .run(as_dict=True)
as_dict=True,
) )
if existing_fiscal_years: if existing_fiscal_years:
@@ -110,37 +84,30 @@ class FiscalYear(Document):
frappe.throw( frappe.throw(
_( _(
"Year start date or end date is overlapping with {0}. To avoid please set company" "Year start date or end date is overlapping with {0}. To avoid please set company"
).format(existing.name), ).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
frappe.NameError, frappe.NameError,
) )
@frappe.whitelist()
def check_duplicate_fiscal_year(doc):
year_start_end_dates = frappe.db.sql(
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
(doc.name),
)
for fiscal_year, ysd, yed in year_start_end_dates:
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
not frappe.in_test
):
frappe.throw(
_(
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
).format(fiscal_year)
)
@frappe.whitelist()
def auto_create_fiscal_year(): def auto_create_fiscal_year():
for d in frappe.db.sql( fy = frappe.qb.DocType("Fiscal Year")
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
): # Skipped auto-creating Short Year, as it has very rare use case.
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
follow_up_date = add_days(getdate(), days=3)
fiscal_year = (
frappe.qb.from_(fy)
.select(fy.name)
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
.run()
)
for d in fiscal_year:
try: try:
current_fy = frappe.get_doc("Fiscal Year", d[0]) current_fy = frappe.get_doc("Fiscal Year", d[0])
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False) new_fy = frappe.new_doc("Fiscal Year")
new_fy.disabled = cint(current_fy.disabled)
new_fy.year_start_date = add_days(current_fy.year_end_date, 1) new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
new_fy.year_end_date = add_years(current_fy.year_end_date, 1) new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
@@ -148,6 +115,10 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year) start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year) end_year = cstr(new_fy.year_end_date.year)
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year) new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
for row in current_fy.companies:
new_fy.append("companies", {"company": row.company})
new_fy.auto_created = 1 new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True) new_fy.insert(ignore_permissions=True)

View File

@@ -15,20 +15,22 @@
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company" "options": "Company",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:09:44.659251", "modified": "2026-02-20 23:02:26.193606",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Fiscal Year Company", "name": "Fiscal Year Company",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
company: DF.Link | None company: DF.Link
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View File

@@ -185,7 +185,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Reference Type", "label": "Reference Type",
"no_copy": 1, "no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry", "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
"search_index": 1 "search_index": 1
}, },
{ {
@@ -198,7 +198,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])", "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
"fieldname": "reference_due_date", "fieldname": "reference_due_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Reference Due Date", "label": "Reference Due Date",
@@ -294,7 +294,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-11-27 12:23:33.157655", "modified": "2026-02-19 17:01:22.642454",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
"Fees", "Fees",
"Full and Final Statement", "Full and Final Statement",
"Payment Entry", "Payment Entry",
"Bank Transaction",
] ]
user_remark: DF.SmallText | None user_remark: DF.SmallText | None
# end: auto-generated types # end: auto-generated types

View File

@@ -512,12 +512,16 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("contact_email", ""); frm.set_value("contact_email", "");
frm.set_value("contact_person", ""); frm.set_value("contact_person", "");
} }
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) { if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
if (!frm.doc.posting_date) { if (!frm.doc.posting_date) {
frappe.msgprint(__("Please select Posting Date before selecting Party")); frappe.msgprint(__("Please select Posting Date before selecting Party"));
frm.set_value("party", ""); frm.set_value("party", "");
return; return;
} }
erpnext.utils.get_employee_contact_details(frm);
frm.set_party_account_based_on_party = true; frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
@@ -1450,16 +1454,15 @@ frappe.ui.form.on("Payment Entry", {
callback: function (r) { callback: function (r) {
if (!r.exc && r.message) { if (!r.exc && r.message) {
// set taxes table // set taxes table
if (r.message) { let taxes = r.message;
for (let tax of r.message) { taxes.forEach((tax) => {
if (tax.charge_type === "On Net Total") { if (tax.charge_type === "On Net Total") {
tax.charge_type = "On Paid Amount"; tax.charge_type = "On Paid Amount";
}
frm.add_child("taxes", tax);
} }
frm.events.apply_taxes(frm); });
frm.events.set_unallocated_amount(frm); frm.set_value("taxes", taxes);
} frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
} }
}, },
}); });

View File

@@ -701,7 +701,6 @@
"fetch_from": "company.book_advance_payments_in_separate_party_account", "fetch_from": "company.book_advance_payments_in_separate_party_account",
"fieldname": "book_advance_payments_in_separate_party_account", "fieldname": "book_advance_payments_in_separate_party_account",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account", "label": "Book Advance Payments in Separate Party Account",
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
@@ -793,7 +792,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2025-12-18 13:56:40.206038", "modified": "2026-02-03 16:08:49.800381",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@@ -0,0 +1,88 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-02 17:50:08.648006",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_term",
"column_break_lnjp",
"payment_schedule",
"section_break_fjhh",
"description",
"section_break_mjlv",
"due_date",
"column_break_qghl",
"amount"
],
"fields": [
{
"fieldname": "payment_term",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Term",
"options": "Payment Term"
},
{
"collapsible": 1,
"fieldname": "section_break_fjhh",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"fieldname": "section_break_mjlv",
"fieldtype": "Section Break"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date"
},
{
"fieldname": "column_break_qghl",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"precision": "2"
},
{
"fieldname": "column_break_lnjp",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "payment_schedule",
"fieldtype": "Link",
"label": "Payment Schedule",
"options": "Payment Schedule",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-19 02:21:36.455830",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PaymentReference(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Currency
description: DF.SmallText | None
due_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_schedule: DF.Link | None
payment_term: DF.Link | None
# end: auto-generated types
pass

View File

@@ -105,3 +105,29 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
}); });
} }
}); });
frappe.ui.form.on("Payment Request", "calculate_total_amount_by_selected_rows", function (frm) {
if (frm.doc.docstatus !== 0) {
frappe.msgprint(__("Cannot fetch selected rows for submitted Payment Request"));
return;
}
const selected = frm.get_selected()?.payment_reference || [];
if (!selected.length) {
frappe.throw(__("No rows selected"));
}
let total = 0;
selected.forEach((name) => {
const row = frm.doc.payment_reference.find((d) => d.name === name);
if (row) {
row.manually_selected = 1;
total += row.amount;
}
});
frm.doc.payment_reference.forEach((row) => {
row.auto_selected = 0;
});
frm.set_value("grand_total", total);
frm.refresh_field("grand_total");
frm.save();
});

View File

@@ -19,6 +19,8 @@
"column_break_4", "column_break_4",
"reference_doctype", "reference_doctype",
"reference_name", "reference_name",
"payment_reference_section",
"payment_reference",
"transaction_details", "transaction_details",
"grand_total", "grand_total",
"currency", "currency",
@@ -157,6 +159,7 @@
"label": "Amount", "label": "Amount",
"non_negative": 1, "non_negative": 1,
"options": "currency", "options": "currency",
"read_only_depends_on": "eval:doc.payment_reference.length>0",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -457,6 +460,17 @@
"fieldname": "phone_number", "fieldname": "phone_number",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Phone Number" "label": "Phone Number"
},
{
"fieldname": "payment_reference_section",
"fieldtype": "Section Break"
},
{
"fieldname": "payment_reference",
"fieldtype": "Table",
"label": "Payment Reference",
"options": "Payment Reference",
"read_only": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -464,7 +478,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-08-29 11:52:48.555415", "modified": "2026-01-13 12:53:00.963274",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",

View File

@@ -45,6 +45,7 @@ class PaymentRequest(Document):
if TYPE_CHECKING: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.accounts.doctype.payment_reference.payment_reference import PaymentReference
from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import ( from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
SubscriptionPlanDetail, SubscriptionPlanDetail,
) )
@@ -78,6 +79,7 @@ class PaymentRequest(Document):
payment_gateway: DF.ReadOnly | None payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None payment_gateway_account: DF.Link | None
payment_order: DF.Link | None payment_order: DF.Link | None
payment_reference: DF.Table[PaymentReference]
payment_request_type: DF.Literal["Outward", "Inward"] payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None payment_url: DF.Data | None
phone_number: DF.Data | None phone_number: DF.Data | None
@@ -109,15 +111,36 @@ class PaymentRequest(Document):
if self.get("__islocal"): if self.get("__islocal"):
self.status = "Draft" self.status = "Draft"
self.validate_reference_document() self.validate_reference_document()
self.validate_against_payment_reference()
self.validate_payment_request_amount() self.validate_payment_request_amount()
# self.validate_currency() # self.validate_currency()
self.validate_subscription_details() self.validate_subscription_details()
def validate_against_payment_reference(self):
if not self.payment_reference:
return
expected = sum(flt(r.amount) for r in self.payment_reference)
if flt(expected, self.precision("grand_total")) != flt(self.grand_total):
frappe.throw(_("Grand Total must match sum of Payment References"))
seen = set()
for r in self.payment_reference:
if not r.payment_schedule:
continue # legacy mode → skip
if r.payment_schedule in seen:
frappe.throw(_("Duplicate Payment Schedule selected"))
seen.add(r.payment_schedule)
def validate_reference_document(self): def validate_reference_document(self):
if not self.reference_doctype or not self.reference_name: if not self.reference_doctype or not self.reference_name:
frappe.throw(_("To create a Payment Request reference document is required")) frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self): def validate_payment_request_amount(self):
if self.payment_reference:
return
if self.grand_total == 0: if self.grand_total == 0:
frappe.throw( frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")), _("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
@@ -535,7 +558,7 @@ class PaymentRequest(Document):
row_number += TO_SKIP_NEW_ROW row_number += TO_SKIP_NEW_ROW
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def make_payment_request(**args): def make_payment_request(**args):
"""Make payment request""" """Make payment request"""
@@ -552,9 +575,63 @@ def make_payment_request(**args):
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn) ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"): if not args.get("company"):
args.company = ref_doc.company args.company = ref_doc.company
gateway_account = get_gateway_details(args) or frappe._dict() gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) # Schedule-based PRs are allowed only if no Payment Entry exists for this document.
# Any existing Payment Entry forces legacy (amount-based) flow.
selected_payment_schedules = json.loads(args.get("schedules")) if args.get("schedules") else []
# Backend guard:
# If any Payment Entry exists, schedule-based PRs are not allowed.
if selected_payment_schedules and get_existing_payment_entry(ref_doc.name):
frappe.throw(
_(
"Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document."
)
)
has_payment_entry = bool(get_existing_payment_entry(ref_doc.name))
payment_reference = []
if selected_payment_schedules:
existing_payment_references = get_existing_payment_references(ref_doc.name)
if existing_payment_references:
existing_ids = {r["payment_schedule"] for r in existing_payment_references}
selected_ids = {r["name"] for r in selected_payment_schedules}
duplicate_ids = existing_ids & selected_ids
if duplicate_ids:
duplicate_schedules = []
for row in selected_payment_schedules:
if row["name"] in duplicate_ids:
existing_ref = next(
(r for r in existing_payment_references if r["payment_schedule"] == row["name"]),
{},
)
existing_pr = existing_ref.get("parent")
duplicate_schedules.append(
f"Payment Term: {row.get('payment_term')}, "
f"Due Date: {row.get('due_date')}, "
f"Amount: {row.get('payment_amount')} "
f"(already requested in PR {existing_pr})"
)
frappe.throw(
_("The following payment schedule(s) already exist:\n{0}").format(
"\n".join(duplicate_schedules)
)
)
payment_reference = set_payment_references(args.get("schedules"))
# Determine grand_total
if selected_payment_schedules and not has_payment_entry:
grand_total = sum(row.get("payment_amount") for row in selected_payment_schedules)
else:
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if not grand_total: if not grand_total:
frappe.throw(_("Payment Entry is already created")) frappe.throw(_("Payment Entry is already created"))
@@ -564,7 +641,6 @@ def make_payment_request(**args):
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) # sets fields on ref_doc loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) # sets fields on ref_doc
ref_doc.db_update() ref_doc.db_update()
grand_total = grand_total - loyalty_amount grand_total = grand_total - loyalty_amount
# fetches existing payment request `grand_total` amount # fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc) existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
@@ -584,19 +660,20 @@ def make_payment_request(**args):
else: else:
# If PR's are processed, cancel all of them. # If PR's are processed, cancel all of them.
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name) cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
else: elif not selected_payment_schedules:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
draft_payment_request = frappe.db.get_value( draft_payment_request = frappe.db.get_value(
"Payment Request", "Payment Request",
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0}, {"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
) )
if draft_payment_request: if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", draft_payment_request) pr = frappe.get_doc("Payment Request", draft_payment_request)
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
pr.save()
else: else:
bank_account = ( bank_account = (
get_party_bank_account(args.get("party_type"), args.get("party")) get_party_bank_account(args.get("party_type"), args.get("party"))
@@ -651,7 +728,10 @@ def make_payment_request(**args):
} }
) )
# Update dimensions if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
# Dimensions
pr.update( pr.update(
{ {
"cost_center": ref_doc.get("cost_center"), "cost_center": ref_doc.get("cost_center"),
@@ -680,6 +760,51 @@ def make_payment_request(**args):
return pr.as_dict() return pr.as_dict()
def apply_payment_references(pr, payment_reference):
existing_refs = pr.get("payment_reference") or []
existing_ids = {r.get("payment_schedule") for r in existing_refs if r.get("payment_schedule")}
new_refs = [r for r in (payment_reference or []) if r.get("payment_schedule") not in existing_ids]
pr.set("payment_reference", existing_refs + new_refs)
pr.set("grand_total", sum(flt(r.get("amount")) for r in pr.get("payment_reference")))
def set_payment_references(payment_schedules):
payment_schedules = json.loads(payment_schedules) if payment_schedules else []
payment_reference = []
for row in payment_schedules:
payment_reference.append(
{
"payment_term": row.get("payment_term"),
"payment_schedule": row.get("name"),
"description": row.get("description"),
"due_date": row.get("due_date"),
"amount": row.get("payment_amount"),
}
)
return payment_reference
def get_existing_payment_entry(ref_docname):
pe = frappe.qb.DocType("Payment Entry")
per = frappe.qb.DocType("Payment Entry Reference")
existing_pe = (
frappe.qb.from_(pe)
.join(per)
.on(per.parent == pe.name)
.select(pe.name)
.where(pe.docstatus < 2)
.where(per.reference_name == ref_docname)
.limit(1)
.run()
)
return existing_pe
def get_amount(ref_doc, payment_account=None): def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype""" """get amount based on doctype"""
grand_total = 0 grand_total = 0
@@ -1024,3 +1149,44 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
}, },
) )
return res return res
@frappe.whitelist()
def get_available_payment_schedules(reference_doctype, reference_name):
ref_doc = frappe.get_doc(reference_doctype, reference_name)
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return []
if get_existing_payment_entry(reference_name):
return []
existing_refs = get_existing_payment_references(reference_name)
existing_ids = {r["payment_schedule"] for r in existing_refs if r.get("payment_schedule")}
return [r for r in ref_doc.payment_schedule if r.name not in existing_ids]
def get_existing_payment_references(reference_name):
PR = frappe.qb.DocType("Payment Request")
PRF = frappe.qb.DocType("Payment Reference")
result = (
frappe.qb.from_(PR)
.join(PRF)
.on(PR.name == PRF.parent)
.select(
PRF.payment_term,
PRF.due_date,
PRF.amount.as_("payment_amount"),
PRF.payment_schedule,
PRF.parent,
)
.where(PR.reference_name == reference_name)
.where(PR.docstatus < 2)
.where(
PR.status.isin(["Draft", "Requested", "Initiated", "Partially Paid", "Payment Ordered", "Paid"])
)
).run(as_dict=True)
return result

View File

@@ -1,12 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import json
import re import re
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import frappe import frappe
from frappe.tests import IntegrationTestCase from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
@@ -851,3 +853,130 @@ class TestPaymentRequest(IntegrationTestCase):
pr.load_from_db() pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount) self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_request_grand_total_from_selected_schedules(self):
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 30})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 30})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 40})
po.save()
po.submit()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[0], po.payment_schedule[2]]
]
)
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
pr.submit()
self.assertEqual(pr.grand_total, 70)
self.assertEqual(len(pr.payment_reference), 2)
def test_draft_pr_reuse_merges_payment_references(self):
from frappe.utils import add_days, nowdate
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 50})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 50})
po.save()
po.submit()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[0]]
]
)
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
pr.save()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[1]]
]
)
# call make_payment_request again → reuse draft
pr_reused = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
self.assertEqual(pr.name, pr_reused.name)
self.assertEqual(pr_reused.grand_total, 100)
self.assertEqual(len(pr_reused.payment_reference), 2)
def test_schedule_pr_not_allowed_if_payment_entry_exists(self):
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
row = po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 100})
po.save()
po.submit()
# create PE first
pr = make_payment_request(dt="Purchase Order", dn=po.name, mute_email=1, submit_doc=1, return_doc=1)
pr.create_payment_entry()
schedules = json.dumps(
[
{
"name": row.name,
"payment_term": row.payment_term,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
]
)
with self.assertRaises(frappe.ValidationError):
make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)

View File

@@ -18,8 +18,19 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
letterhead.is_default = 0
letterhead.save()
cls.enterClassContext(cls.change_settings("Selling Settings", validate_selling_price=0)) cls.enterClassContext(cls.change_settings("Selling Settings", validate_selling_price=0))
@classmethod
def tearDownClass(cls):
super().tearDownClass()
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
letterhead.is_default = 1
letterhead.save()
frappe.db.commit() # nosemgrep
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()

View File

@@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.frm.add_custom_button( this.frm.add_custom_button(
__("Payment Request"), __("Payment Request"),
function () { function () {
me.make_payment_request(); me.make_payment_request_with_schedule();
}, },
__("Create") __("Create")
); );

View File

@@ -8,12 +8,12 @@
"email_append_to": 1, "email_append_to": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"company",
"title", "title",
"naming_series", "naming_series",
"supplier", "supplier",
"supplier_name", "supplier_name",
"tax_id", "tax_id",
"company",
"column_break_6", "column_break_6",
"posting_date", "posting_date",
"posting_time", "posting_time",
@@ -85,20 +85,24 @@
"taxes_and_charges_added", "taxes_and_charges_added",
"taxes_and_charges_deducted", "taxes_and_charges_deducted",
"total_taxes_and_charges", "total_taxes_and_charges",
"section_break_49", "totals_section",
"use_company_roundoff_cost_center",
"grand_total",
"in_words",
"column_break8",
"disable_rounded_total",
"rounding_adjustment",
"rounded_total",
"base_totals_section",
"base_grand_total", "base_grand_total",
"base_in_words",
"column_break_hcca",
"base_rounding_adjustment", "base_rounding_adjustment",
"base_rounded_total", "base_rounded_total",
"base_in_words", "section_break_ttrv",
"column_break8",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance", "total_advance",
"column_break_peap",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total",
"section_tax_withholding_entry", "section_tax_withholding_entry",
"tax_withholding_group", "tax_withholding_group",
"ignore_tax_withholding_threshold", "ignore_tax_withholding_threshold",
@@ -883,15 +887,10 @@
"options": "currency", "options": "currency",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "section_break_49",
"fieldtype": "Section Break",
"label": "Totals"
},
{ {
"fieldname": "base_grand_total", "fieldname": "base_grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total (Company Currency)", "label": "Grand Total",
"oldfieldname": "grand_total", "oldfieldname": "grand_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
@@ -902,7 +901,7 @@
"depends_on": "eval:!doc.disable_rounded_total", "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
@@ -912,7 +911,7 @@
"depends_on": "eval:!doc.disable_rounded_total", "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounded Total (Company Currency)", "label": "Rounded Total",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
@@ -921,7 +920,7 @@
{ {
"fieldname": "base_in_words", "fieldname": "base_in_words",
"fieldtype": "Data", "fieldtype": "Data",
"label": "In Words (Company Currency)", "label": "In Words",
"length": 240, "length": 240,
"oldfieldname": "in_words", "oldfieldname": "in_words",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -1626,8 +1625,7 @@
"hidden": 1, "hidden": 1,
"label": "Item Wise Tax Details", "label": "Item Wise Tax Details",
"no_copy": 1, "no_copy": 1,
"options": "Item Wise Tax Detail", "options": "Item Wise Tax Detail"
"print_hide": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -1662,6 +1660,28 @@
"fieldname": "override_tax_withholding_entries", "fieldname": "override_tax_withholding_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Edit Tax Withholding Entries" "label": "Edit Tax Withholding Entries"
},
{
"fieldname": "column_break_hcca",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ttrv",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_peap",
"fieldtype": "Column Break"
},
{
"fieldname": "base_totals_section",
"fieldtype": "Section Break",
"label": "Totals (Company Currency)"
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break",
"label": "Totals"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -1669,7 +1689,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-02-05 20:45:16.964500", "modified": "2026-02-23 14:23:57.269770",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -1745,10 +1745,6 @@ class PurchaseInvoice(BuyingController):
project_doc.db_update() project_doc.db_update()
def validate_supplier_invoice(self): def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
if self.bill_no: if self.bill_no:
if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")): if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True) fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)

View File

@@ -138,7 +138,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.frm.add_custom_button( this.frm.add_custom_button(
__("Payment Request"), __("Payment Request"),
function () { function () {
me.make_payment_request(); me.make_payment_request_with_schedule();
}, },
__("Create") __("Create")
); );

View File

@@ -77,34 +77,38 @@
"base_total_taxes_and_charges", "base_total_taxes_and_charges",
"column_break_47", "column_break_47",
"total_taxes_and_charges", "total_taxes_and_charges",
"totals", "totals_section",
"use_company_roundoff_cost_center",
"grand_total",
"in_words",
"column_break5",
"disable_rounded_total",
"rounding_adjustment",
"rounded_total",
"base_totals_section",
"base_grand_total", "base_grand_total",
"base_in_words",
"column_break_xjag",
"base_rounding_adjustment", "base_rounding_adjustment",
"base_rounded_total", "base_rounded_total",
"base_in_words", "section_break_vacb",
"column_break5",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance", "total_advance",
"column_break_rdks",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total",
"section_tax_withholding_entry", "section_tax_withholding_entry",
"tax_withholding_group", "tax_withholding_group",
"ignore_tax_withholding_threshold", "ignore_tax_withholding_threshold",
"override_tax_withholding_entries", "override_tax_withholding_entries",
"tax_withholding_entries", "tax_withholding_entries",
"section_break_49", "additional_discount_section",
"apply_discount_on", "apply_discount_on",
"base_discount_amount", "base_discount_amount",
"coupon_code", "coupon_code",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"column_break_51", "column_break_51",
"additional_discount_percentage", "additional_discount_percentage",
"discount_amount", "discount_amount",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"sec_tax_breakup", "sec_tax_breakup",
"other_charges_calculation", "other_charges_calculation",
"item_wise_tax_details", "item_wise_tax_details",
@@ -194,13 +198,13 @@
"column_break8", "column_break8",
"unrealized_profit_loss_account", "unrealized_profit_loss_account",
"against_income_account", "against_income_account",
"sales_team_section_break", "commission_section",
"sales_partner", "sales_partner",
"amount_eligible_for_commission", "amount_eligible_for_commission",
"column_break10", "column_break10",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break2", "sales_team_section",
"sales_team", "sales_team",
"edit_printing_settings", "edit_printing_settings",
"letter_head", "letter_head",
@@ -217,8 +221,7 @@
"update_auto_repeat_reference", "update_auto_repeat_reference",
"more_information", "more_information",
"status", "status",
"inter_company_invoice_reference", "remarks",
"represents_company",
"customer_group", "customer_group",
"column_break_imbx", "column_break_imbx",
"utm_source", "utm_source",
@@ -227,8 +230,9 @@
"utm_content", "utm_content",
"col_break23", "col_break23",
"is_internal_customer", "is_internal_customer",
"represents_company",
"inter_company_invoice_reference",
"is_discounted", "is_discounted",
"remarks",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -794,7 +798,8 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Time Sheets", "label": "Time Sheets",
"options": "Sales Invoice Timesheet", "options": "Sales Invoice Timesheet",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@@ -1073,14 +1078,6 @@
"no_copy": 1, "no_copy": 1,
"options": "Cost Center" "options": "Cost Center"
}, },
{
"collapsible": 1,
"fieldname": "section_break_49",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Additional Discount"
},
{ {
"default": "Grand Total", "default": "Grand Total",
"fieldname": "apply_discount_on", "fieldname": "apply_discount_on",
@@ -1125,22 +1122,12 @@
"options": "currency", "options": "currency",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "totals",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Totals",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
"print_hide": 1
},
{ {
"fieldname": "base_grand_total", "fieldname": "base_grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Grand Total (Company Currency)", "label": "Grand Total (Company Currency",
"oldfieldname": "grand_total", "oldfieldname": "grand_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
@@ -1154,9 +1141,8 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -1166,10 +1152,9 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Rounded Total (Company Currency)", "label": "Rounded Total",
"oldfieldname": "rounded_total", "oldfieldname": "rounded_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -1179,7 +1164,7 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "In Words (Company Currency)", "label": "In Words",
"length": 240, "length": 240,
"oldfieldname": "in_words", "oldfieldname": "in_words",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -1272,7 +1257,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"collapsible_depends_on": "advances", "collapsible_depends_on": "advances",
"fieldname": "advances_section", "fieldname": "advances_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -1706,10 +1690,10 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"default": "No", "default": "No",
"fieldname": "is_opening", "fieldname": "is_opening",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1,
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Is Opening Entry", "label": "Is Opening Entry",
@@ -1738,18 +1722,6 @@
"oldfieldtype": "Text", "oldfieldtype": "Text",
"print_hide": 1 "print_hide": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "sales_partner",
"fieldname": "sales_team_section_break",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group",
"print_hide": 1
},
{ {
"fieldname": "sales_partner", "fieldname": "sales_partner",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1793,16 +1765,6 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1 "print_hide": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "sales_team",
"fieldname": "section_break2",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team",
"print_hide": 1
},
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "sales_team", "fieldname": "sales_team",
@@ -2293,6 +2255,64 @@
"fieldname": "override_tax_withholding_entries", "fieldname": "override_tax_withholding_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Edit Tax Withholding Entries" "label": "Edit Tax Withholding Entries"
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Totals",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
"print_hide": 1
},
{
"fieldname": "base_totals_section",
"fieldtype": "Section Break",
"label": "Totals (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_xjag",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "additional_discount_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Additional Discount"
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_team",
"fieldname": "sales_team_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_partner",
"fieldname": "commission_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "section_break_vacb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdks",
"fieldtype": "Column Break"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -2306,7 +2326,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2026-02-06 20:43:44.732805", "modified": "2026-02-23 14:29:00.301842",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
get_account_currency, get_account_currency,
update_voucher_outstanding, update_voucher_outstanding,
) )
from erpnext.assets.doctype.asset.asset import split_asset
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset, depreciate_asset,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
@@ -480,6 +481,8 @@ class SalesInvoice(SellingController):
self.update_stock_reservation_entries() self.update_stock_reservation_entries()
self.update_stock_ledger() self.update_stock_ledger()
self.split_asset_based_on_sale_qty()
self.process_asset_depreciation() self.process_asset_depreciation()
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
@@ -1402,6 +1405,51 @@ class SalesInvoice(SellingController):
): ):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def split_asset_based_on_sale_qty(self):
asset_qty_map = self.get_asset_qty()
for asset, qty in asset_qty_map.items():
if qty["actual_qty"] < qty["sale_qty"]:
frappe.throw(
_(
"Sell quantity cannot exceed the asset quantity. Asset {0} has only {1} item(s)."
).format(asset, qty["actual_qty"])
)
remaining_qty = qty["actual_qty"] - qty["sale_qty"]
if remaining_qty > 0:
split_asset(asset, remaining_qty)
def get_asset_qty(self):
asset_qty_map = {}
assets = {row.asset for row in self.items if row.is_fixed_asset and row.asset}
if not assets or self.is_return:
return asset_qty_map
asset_actual_qty = dict(
frappe.db.get_all(
"Asset",
{"name": ["in", list(assets)]},
["name", "asset_quantity"],
as_list=True,
)
)
for row in self.items:
if row.is_fixed_asset and row.asset:
actual_qty = asset_actual_qty.get(row.asset)
if row.asset in asset_qty_map.keys():
asset_qty_map[row.asset]["sale_qty"] += flt(row.qty)
else:
asset_qty_map.setdefault(
row.asset,
{
"sale_qty": flt(row.qty),
"actual_qty": flt(actual_qty),
},
)
return asset_qty_map
def process_asset_depreciation(self): def process_asset_depreciation(self):
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1): if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale() self.depreciate_asset_on_sale()

View File

@@ -843,6 +843,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Incoming Rate (Costing)", "label": "Incoming Rate (Costing)",
"no_copy": 1, "no_copy": 1,
"non_negative": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1 "print_hide": 1
}, },
@@ -1009,7 +1010,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-02-15 21:08:57.341638", "modified": "2026-02-23 14:37:14.853941",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -0,0 +1,41 @@
{
"allow_roles": [
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
],
"creation": "2026-02-22 18:26:42.015787",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 4,
"is_complete": 0,
"modified": "2026-02-23 22:51:34.267812",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Chart of Accounts"
},
{
"step": "Setup Sales taxes"
},
{
"step": "Create Sales Invoice"
},
{
"step": "Create Payment Entry"
},
{
"step": "View Balance Sheet"
},
{
"step": "Review Accounts Settings"
}
],
"title": "Accounting Onboarding"
}

View File

@@ -1,3 +1,43 @@
<h3>{{ _("Fiscal Year") }}</h3> <h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
<p>{{ _("New fiscal year created :- ") }} {{ doc.name }}</p> <p>{{ _("A new fiscal year has been automatically created.") }}</p>
<p>{{ _("Fiscal Year Details") }}</p>
<table style="margin-bottom: 1rem; width: 70%">
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
<td>{{ doc.name }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
</tr>
{% if doc.companies|length > 0 %}
<tr>
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
{% if doc.companies|length < 2 %}
{{ _("Company") }}
{% else %}
{{ _("Companies") }}
{% endif %}
</td>
<td>{{ doc.companies[0].company }}</td>
</tr>
{% for idx in range(1, doc.companies|length) %}
<tr>
<td>{{ doc.companies[idx].company }}</td>
</tr>
{% endfor %}
{% endif %}
</table>
{% if doc.disabled %}
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
{% endif %}
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>

View File

@@ -1,7 +1,8 @@
{ {
"attach_print": 0, "attach_print": 0,
"channel": "Email", "channel": "Email",
"condition": "doc.auto_created", "condition": "doc.auto_created == 1",
"condition_type": "Python",
"creation": "2018-04-25 14:19:05.440361", "creation": "2018-04-25 14:19:05.440361",
"days_in_advance": 0, "days_in_advance": 0,
"docstatus": 0, "docstatus": 0,
@@ -11,8 +12,10 @@
"event": "New", "event": "New",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
"message_type": "HTML", "message_type": "HTML",
"modified": "2023-11-17 08:54:51.532104", "minutes_offset": 0,
"modified": "2026-02-23 17:37:03.755394",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Notification for new fiscal year", "name": "Notification for new fiscal year",
@@ -27,5 +30,5 @@
], ],
"send_system_notification": 0, "send_system_notification": 0,
"send_to_all_assignees": 0, "send_to_all_assignees": 0,
"subject": "Notification for new fiscal year {{ doc.name }}" "subject": "New Fiscal Year {{ doc.name }} - Review Required"
} }

View File

@@ -0,0 +1,20 @@
{
"action": "Go to Page",
"action_label": "Configure Chart of Accounts",
"creation": "2026-02-22 18:28:15.401383",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:45.540780",
"modified_by": "Administrator",
"name": "Chart of Accounts",
"owner": "Administrator",
"path": "Tree/Account",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Chart of Accounts",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Create Entry",
"action_label": "Create Payment Entry",
"creation": "2026-02-23 19:22:12.005360",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:19:56.482245",
"modified_by": "Administrator",
"name": "Create Payment Entry",
"owner": "Administrator",
"reference_document": "Payment Entry",
"route_options": "{\n \"payment_type\": \"Receive\",\n \"party_type\": \"Customer\"\n}",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Payment Entry",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Sales Invoice",
"creation": "2026-02-20 13:42:38.439574",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 2,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.931428",
"modified_by": "Administrator",
"name": "Create Sales Invoice",
"owner": "Administrator",
"reference_document": "Sales Invoice",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Sales Invoice",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Update Settings",
"action_label": "Review Accounts Settings",
"creation": "2026-02-23 19:27:06.055104",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.855407",
"modified_by": "Administrator",
"name": "Review Accounts Settings",
"owner": "Administrator",
"path": "desk/accounts-settings",
"reference_document": "Accounts Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Accounts Settings",
"validate_action": 0
}

View File

@@ -0,0 +1,22 @@
{
"action": "Go to Page",
"action_label": "Setup Sales Taxes",
"creation": "2026-02-22 18:30:18.750391",
"docstatus": 0,
"doctype": "Onboarding Step",
"form_tour": "",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:42.373227",
"modified_by": "Administrator",
"name": "Setup Sales taxes",
"owner": "Administrator",
"path": "/desk/sales-taxes-and-charges-template",
"reference_document": "Sales Taxes and Charges Template",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Sales taxes",
"validate_action": 1
}

View File

@@ -0,0 +1,23 @@
{
"action": "View Report",
"action_label": "View Balance Sheet",
"creation": "2026-02-23 19:22:57.651194",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:39.178107",
"modified_by": "Administrator",
"name": "View Balance Sheet",
"owner": "Administrator",
"reference_report": "Balance Sheet",
"report_description": "View Balance Sheet",
"report_reference_doctype": "GL Entry",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "View Balance Sheet",
"validate_action": 1
}

View File

@@ -7,18 +7,16 @@ from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Count, Date, Sum from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_months, add_months,
add_years,
cint, cint,
cstr, cstr,
date_diff, date_diff,
flt, flt,
formatdate, formatdate,
get_last_day, get_last_day,
get_timestamp,
getdate, getdate,
nowdate, nowdate,
) )
@@ -298,19 +296,9 @@ def complete_contact_details(party_details):
contact_details = frappe._dict() contact_details = frappe._dict()
if party_details.party_type == "Employee": if party_details.party_type == "Employee":
contact_details = frappe.db.get_value( from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
"Employee",
party_details.party,
[
"employee_name as contact_display",
"prefered_email as contact_email",
"cell_number as contact_mobile",
"designation as contact_designation",
"department as contact_department",
],
as_dict=True,
)
contact_details = get_employee_contact(party_details.party)
contact_details.update({"contact_person": None, "contact_phone": None}) contact_details.update({"contact_person": None, "contact_phone": None})
elif party_details.contact_person: elif party_details.contact_person:
contact_details = frappe.db.get_value( contact_details = frappe.db.get_value(

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -102,7 +102,7 @@ def execute(filters=None):
filters.periodicity, period_list, filters.accumulated_values, company=filters.company filters.periodicity, period_list, filters.accumulated_values, company=filters.company
) )
chart = get_chart_data(filters, columns, asset, liability, equity, currency) chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
report_summary, primitive_summary = get_report_summary( report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters period_list, asset, liability, equity, provisional_profit_loss, currency, filters
@@ -231,18 +231,19 @@ def get_report_summary(
], (net_asset - net_liability + net_equity) ], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, equity, currency): def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
labels = [d.get("label") for d in columns[4:]] labels = [col.get("label") for col in chart_columns]
asset_data, liability_data, equity_data = [], [], [] asset_data, liability_data, equity_data = [], [], []
for p in columns[4:]: for col in chart_columns:
key = col.get("key") or col.get("fieldname")
if asset: if asset:
asset_data.append(asset[-2].get(p.get("fieldname"))) asset_data.append(asset[-2].get(key))
if liability: if liability:
liability_data.append(liability[-2].get(p.get("fieldname"))) liability_data.append(liability[-2].get(key))
if equity: if equity:
equity_data.append(equity[-2].get(p.get("fieldname"))) equity_data.append(equity[-2].get(key))
datasets = [] datasets = []
if asset_data: if asset_data:

View File

@@ -145,7 +145,7 @@ def execute(filters=None):
True, True,
) )
chart = get_chart_data(columns, data, company_currency) chart = get_chart_data(period_list, data, company_currency)
report_summary = get_report_summary(summary_data, company_currency) report_summary = get_report_summary(summary_data, company_currency)
@@ -417,12 +417,12 @@ def get_report_summary(summary_data, currency):
return report_summary return report_summary
def get_chart_data(columns, data, currency): def get_chart_data(period_list, data, currency):
labels = [d.get("label") for d in columns[2:]] labels = [period.get("label") for period in period_list]
datasets = [ datasets = [
{ {
"name": section.get("section").replace("'", ""), "name": section.get("section").replace("'", ""),
"values": [section.get(d.get("fieldname")) for d in columns[2:]], "values": [section.get(period.get("key")) for period in period_list],
} }
for section in data for section in data
if section.get("parent_section") is None and section.get("currency") if section.get("parent_section") is None and section.get("currency")

View File

@@ -48,22 +48,25 @@ def execute(filters=None):
return columns, data, message, chart return columns, data, message, chart
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year")) fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
companies_column, companies = get_companies(filters) company_list, companies = get_companies(filters)
columns = get_columns(companies_column, filters) company_columns = get_company_columns(company_list, filters)
columns = get_columns(company_columns)
if filters.get("report") == "Balance Sheet": if filters.get("report") == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data( data, message, chart, report_summary = get_balance_sheet_data(
fiscal_year, companies, columns, filters fiscal_year, companies, company_columns, filters
) )
elif filters.get("report") == "Profit and Loss Statement": elif filters.get("report") == "Profit and Loss Statement":
data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters) data, message, chart, report_summary = get_profit_loss_data(
fiscal_year, companies, company_columns, filters
)
else: else:
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters) data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
return columns, data, message, chart, report_summary return columns, data, message, chart, report_summary
def get_balance_sheet_data(fiscal_year, companies, columns, filters): def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters) asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters) liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters)
@@ -116,7 +119,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
True, True,
) )
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency) chart = get_chart_data(filters, company_columns, asset, liability, equity, company_currency)
return data, message, chart, report_summary return data, message, chart, report_summary
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
return root_account[0][0] return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters) income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters) company_currency = get_company_currency(filters)
@@ -174,7 +177,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
if net_profit_loss: if net_profit_loss:
data.append(net_profit_loss) data.append(net_profit_loss)
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency) chart = get_pl_chart_data(filters, company_columns, income, expense, net_profit_loss, company_currency)
report_summary, primitive_summary = get_pl_summary( report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True companies, "", income, expense, net_profit_loss, company_currency, filters, True
@@ -280,7 +283,30 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
return data return data
def get_columns(companies, filters): def get_company_columns(companies, filters):
company_columns = []
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
company_columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
return company_columns
def get_columns(company_columns):
columns = [ columns = [
{ {
"fieldname": "account", "fieldname": "account",
@@ -298,23 +324,7 @@ def get_columns(companies, filters):
}, },
] ]
for company in companies: columns.extend(company_columns)
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
return columns return columns

View File

@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
from frappe.query_builder.functions import IfNull from frappe.query_builder.functions import IfNull
from frappe.utils import getdate, nowdate from frappe.utils import getdate, nowdate
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from pypika.terms import LiteralValue from pypika.terms import Bracket, LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions(party_type) if match_conditions := build_match_conditions(party_type):
query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query = query.where(LiteralValue(match_conditions))
party_details = query.run(as_dict=True) party_details = query.run(as_dict=True)

View File

@@ -11,7 +11,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Max, Min, Sum from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from pypika.terms import ExistsCriterion from pypika.terms import Bracket, ExistsCriterion, LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@@ -564,18 +564,15 @@ def get_accounting_entries(
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry) account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
query = query.where(ExistsCriterion(account_filter_query)) query = query.where(ExistsCriterion(account_filter_query))
if group_by_account:
query = query.groupby("account")
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: return query.run(as_dict=True)
query += "and" + match_conditions
if group_by_account:
query += " GROUP BY `account`"
return frappe.db.sql(query, params, as_dict=True)
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry): def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):

View File

@@ -324,10 +324,8 @@ def get_conditions(filters):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions("GL Entry") if match_conditions := build_match_conditions("GL Entry"):
conditions.append(f"({match_conditions})")
if match_conditions:
conditions.append(match_conditions)
accounting_dimensions = get_accounting_dimensions(as_list=False) accounting_dimensions = get_accounting_dimensions(as_list=False)

View File

@@ -444,6 +444,7 @@ class TestGrossProfit(IntegrationTestCase):
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
) )
sinv.is_return = 1 sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
sinv = sinv.save().submit() sinv = sinv.save().submit()
filters = frappe._dict( filters = frappe._dict(

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from pypika.terms import Bracket, LiteralValue
import erpnext import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import ( from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -361,15 +362,12 @@ def get_items(filters, additional_table_columns):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(doctype, query, filters) query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True) return query.run(as_dict=True)
def get_aii_accounts(): def get_aii_accounts():

View File

@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
from frappe.utils import flt from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html from frappe.utils.xlsxutils import handle_html
from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_values_for_columns from erpnext.accounts.report.utils import get_values_for_columns
@@ -390,20 +391,21 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
def apply_order_by_conditions(doctype, query, filters): def apply_order_by_conditions(doctype, query, filters):
invoice = f"`tab{doctype}`" invoice = frappe.qb.DocType(doctype)
invoice_item = f"`tab{doctype} Item`" invoice_item = frappe.qb.DocType(f"{doctype} Item")
if not filters.get("group_by"): if not filters.get("group_by"):
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc" query = query.orderby(invoice.posting_date, order=Order.desc)
query = query.orderby(invoice_item.item_group, order=Order.desc)
elif filters.get("group_by") == "Invoice": elif filters.get("group_by") == "Invoice":
query += f" order by {invoice_item}.parent desc" query = query.orderby(invoice_item.parent, order=Order.desc)
elif filters.get("group_by") == "Item": elif filters.get("group_by") == "Item":
query += f" order by {invoice_item}.item_code" query = query.orderby(invoice_item.item_code)
elif filters.get("group_by") == "Item Group": elif filters.get("group_by") == "Item Group":
query += f" order by {invoice_item}.item_group" query = query.orderby(invoice_item.item_group)
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by")) filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc" query = query.orderby(filter_field, order=Order.desc)
return query return query
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(doctype, query, filters) query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True) return query.run(as_dict=True)
def get_delivery_notes_against_sales_order(item_list): def get_delivery_notes_against_sales_order(item_list):

View File

@@ -68,7 +68,7 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value( currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency" "Company", filters.company, "default_currency"
) )
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency) chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
report_summary, primitive_summary = get_report_summary( report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
@@ -162,18 +162,19 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
return net_profit_loss return net_profit_loss
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency): def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[4:]] labels = [col.get("label") for col in chart_columns]
income_data, expense_data, net_profit = [], [], [] income_data, expense_data, net_profit = [], [], []
for p in columns[4:]: for col in chart_columns:
key = col.get("key") or col.get("fieldname")
if income: if income:
income_data.append(income[-2].get(p.get("fieldname"))) income_data.append(income[-2].get(key))
if expense: if expense:
expense_data.append(expense[-2].get(p.get("fieldname"))) expense_data.append(expense[-2].get(key))
if net_profit_loss: if net_profit_loss:
net_profit.append(net_profit_loss.get(p.get("fieldname"))) net_profit.append(net_profit_loss.get(key))
datasets = [] datasets = []
if income_data: if income_data:

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, getdate from frappe.utils import flt, getdate
from pypika import Order from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import ( from erpnext.accounts.report.utils import (
@@ -422,15 +422,13 @@ def get_invoices(filters, additional_query_columns):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions("Purchase Invoice"):
match_conditions = build_match_conditions("Purchase Invoice") query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: query = query.orderby("posting_date", order=Order.desc)
query += " and " + match_conditions query = query.orderby("name", order=Order.desc)
query += " order by posting_date desc, name desc" return query.run(as_dict=True)
return frappe.db.sql(query, params, as_dict=True)
def get_conditions(filters, query, doctype): def get_conditions(filters, query, doctype):

View File

@@ -7,7 +7,7 @@ from frappe import _, msgprint
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, getdate from frappe.utils import flt, getdate
from pypika import Order from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import ( from erpnext.accounts.report.utils import (
@@ -458,15 +458,13 @@ def get_invoices(filters, additional_query_columns):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions("Sales Invoice"):
match_conditions = build_match_conditions("Sales Invoice") query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: query = query.orderby("posting_date", order=Order.desc)
query += " and " + match_conditions query = query.orderby("name", order=Order.desc)
query += " order by posting_date desc, name desc" return query.run(as_dict=True)
return frappe.db.sql(query, params, as_dict=True)
def get_conditions(filters, query, doctype): def get_conditions(filters, query, doctype):

View File

@@ -500,7 +500,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
dimensions_dict = frappe._dict() dimensions_dict = frappe._dict()
if entry and active_dimensions: if entry and active_dimensions:
for dim in active_dimensions: for dim in active_dimensions:
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname) if entry_dimension := entry.get(dim.fieldname):
dimensions_dict[dim.fieldname] = entry_dimension
return dimensions_dict return dimensions_dict

View File

@@ -111,7 +111,7 @@ frappe.ui.form.on("Asset", {
frm.add_custom_button( frm.add_custom_button(
__("Sell Asset"), __("Sell Asset"),
function () { function () {
frm.trigger("make_sales_invoice"); frm.trigger("sell_asset");
}, },
__("Manage") __("Manage")
); );
@@ -523,22 +523,6 @@ frappe.ui.form.on("Asset", {
} }
}, },
make_sales_invoice: function (frm) {
frappe.call({
args: {
asset: frm.doc.name,
item_code: frm.doc.item_code,
company: frm.doc.company,
serial_no: frm.doc.serial_no,
},
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
},
});
},
create_asset_maintenance: function (frm) { create_asset_maintenance: function (frm) {
frappe.call({ frappe.call({
args: { args: {
@@ -587,6 +571,69 @@ frappe.ui.form.on("Asset", {
}); });
}, },
sell_asset: function (frm) {
const make_sales_invoice = (sell_qty) => {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_sales_invoice",
args: {
asset: frm.doc.name,
item_code: frm.doc.item_code,
company: frm.doc.company,
serial_no: frm.doc.serial_no,
sell_qty: sell_qty,
},
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
},
});
};
let dialog = new frappe.ui.Dialog({
title: __("Sell Asset"),
fields: [
{
fieldname: "sell_qty",
fieldtype: "Int",
label: __("Sell Qty"),
reqd: 1,
},
],
});
dialog.set_primary_action(__("Sell"), function () {
const dialog_data = dialog.get_values();
const sell_qty = cint(dialog_data.sell_qty);
const asset_qty = cint(frm.doc.asset_quantity);
if (sell_qty <= 0) {
frappe.throw(__("Sell quantity must be greater than zero"));
}
if (sell_qty > asset_qty) {
frappe.throw(__("Sell quantity cannot exceed the asset quantity"));
}
if (sell_qty < asset_qty) {
frappe.confirm(
__(
"The sell quantity is less than the total asset quantity. The remaining quantity will be split into a new asset. This action cannot be undone. <br><br><b>Do you want to continue?</b>"
),
() => {
make_sales_invoice(sell_qty);
dialog.hide();
}
);
return;
}
make_sales_invoice(sell_qty);
dialog.hide();
});
dialog.show();
},
split_asset: function (frm) { split_asset: function (frm) {
const title = __("Split Asset"); const title = __("Split Asset");

View File

@@ -484,6 +484,9 @@ class Asset(AccountsController):
frappe.throw(_("Available-for-use Date should be after purchase date")) frappe.throw(_("Available-for-use Date should be after purchase date"))
def validate_linked_purchase_documents(self): def validate_linked_purchase_documents(self):
if self.flags.is_split_asset:
return
for fieldname, doctype in [ for fieldname, doctype in [
("purchase_receipt", "Purchase Receipt"), ("purchase_receipt", "Purchase Receipt"),
("purchase_invoice", "Purchase Invoice"), ("purchase_invoice", "Purchase Invoice"),
@@ -1085,7 +1088,7 @@ def get_asset_naming_series():
@frappe.whitelist() @frappe.whitelist()
def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=None): def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
asset_doc = frappe.get_doc("Asset", asset) asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice") si = frappe.new_doc("Sales Invoice")
si.company = company si.company = company
@@ -1100,7 +1103,7 @@ def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=N
"income_account": disposal_account, "income_account": disposal_account,
"serial_no": serial_no, "serial_no": serial_no,
"cost_center": depreciation_cost_center, "cost_center": depreciation_cost_center,
"qty": 1, "qty": sell_qty,
}, },
) )
@@ -1380,6 +1383,7 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity) scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
asset_doc = new_asset if is_new_asset else existing_asset asset_doc = new_asset if is_new_asset else existing_asset
asset_doc.flags.is_split_asset = True
set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset) set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset) log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)

View File

@@ -330,7 +330,9 @@ class TestAsset(AssetSetup):
post_depreciation_entries(date=add_months(purchase_date, 2)) post_depreciation_entries(date=add_months(purchase_date, 2))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si = make_sales_invoice(
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
)
si.customer = "_Test Customer" si.customer = "_Test Customer"
si.due_date = date si.due_date = date
si.get("items")[0].rate = 25000 si.get("items")[0].rate = 25000
@@ -458,7 +460,9 @@ class TestAsset(AssetSetup):
post_depreciation_entries(date="2021-01-01") post_depreciation_entries(date="2021-01-01")
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si = make_sales_invoice(
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
)
si.customer = "_Test Customer" si.customer = "_Test Customer"
si.due_date = nowdate() si.due_date = nowdate()
si.get("items")[0].rate = 25000 si.get("items")[0].rate = 25000
@@ -698,6 +702,128 @@ class TestAsset(AssetSetup):
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
def test_partial_asset_sale(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
depreciation_start_date = add_months(get_last_day(date), -2)
# create an asset
asset = create_asset(
item_code="Macbook Pro",
is_existing_asset=1,
calculate_depreciation=1,
available_for_use_date=purchase_date,
purchase_date=purchase_date,
depreciation_start_date=depreciation_start_date,
net_purchase_amount=1000000.0,
purchase_amount=1000000.0,
asset_quantity=10,
total_number_of_depreciations=12,
frequency_of_depreciation=1,
submit=1,
)
asset_depr_schedule_before_sale = get_asset_depr_schedule_doc(asset.name, "Active")
post_depreciation_entries(date)
asset.reload()
# check asset values before sale
self.assertEqual(asset.asset_quantity, 10)
self.assertEqual(asset.net_purchase_amount, 1000000)
self.assertEqual(asset.status, "Partially Depreciated")
self.assertEqual(
asset_depr_schedule_before_sale.depreciation_schedule[0].get("depreciation_amount"), 83333.33
)
# make a partial sales against the asset
si = make_sales_invoice(
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=5
)
si.customer = "_Test Customer"
si.due_date = date
si.get("items")[0].rate = 25000
si.insert()
si.submit()
asset.reload()
asset_depr_schedule_after_sale = get_asset_depr_schedule_doc(asset.name, "Active")
# check asset values after sales
self.assertEqual(asset.asset_quantity, 5)
self.assertEqual(asset.net_purchase_amount, 500000)
self.assertEqual(asset.status, "Sold")
self.assertEqual(
asset_depr_schedule_after_sale.depreciation_schedule[0].get("depreciation_amount"), 41666.66
)
def test_asset_splitting_for_non_existing_asset(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
depreciation_start_date = add_months(get_last_day(date), -2)
asset_qty = 10
asset_rate = 100000.0
asset_item = "Macbook Pro"
asset_location = "Test Location"
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 1)
# Inward asset via Purchase Receipt
pr = make_purchase_receipt(
item_code="Macbook Pro",
posting_date=purchase_date,
qty=asset_qty,
rate=asset_rate,
location=asset_location,
supplier="_Test Supplier",
)
asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
asset_doc = frappe.get_doc("Asset", asset)
asset_doc.calculate_depreciation = 1
asset_doc.available_for_use_date = purchase_date
asset_doc.location = asset_location
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 0,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 12,
"frequency_of_depreciation": 1,
"depreciation_start_date": depreciation_start_date,
},
)
asset_doc.submit()
# check asset values before splitting
asset_depr_schedule_before_splitting = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEqual(asset_doc.asset_quantity, 10)
self.assertEqual(asset_doc.net_purchase_amount, 1000000)
self.assertEqual(
asset_depr_schedule_before_splitting.depreciation_schedule[0].get("depreciation_amount"), 83333.33
)
# initate asset split
new_asset = split_asset(asset_doc.name, 5)
asset_doc.reload()
asset_depr_schedule_after_sale = get_asset_depr_schedule_doc(asset_doc.name, "Active")
new_asset_depr_schedule = get_asset_depr_schedule_doc(new_asset.name, "Active")
# check asset values after splitting
self.assertEqual(asset_doc.asset_quantity, 5)
self.assertEqual(asset_doc.net_purchase_amount, 500000)
self.assertEqual(
asset_depr_schedule_after_sale.depreciation_schedule[0].get("depreciation_amount"), 41666.66
)
# check new asset values after splitting
self.assertEqual(new_asset.asset_quantity, 5)
self.assertEqual(new_asset.net_purchase_amount, 500000)
self.assertEqual(
new_asset_depr_schedule.depreciation_schedule[0].get("depreciation_amount"), 41666.66
)
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0)
class TestDepreciationMethods(AssetSetup): class TestDepreciationMethods(AssetSetup):
def test_schedule_for_straight_line_method(self): def test_schedule_for_straight_line_method(self):

View File

@@ -51,7 +51,9 @@ class TestAssetRepair(IntegrationTestCase):
submit=1, submit=1,
) )
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si = make_sales_invoice(
asset=asset.name, item_code="Macbook Pro", company="_Test Company", sell_qty=asset.asset_quantity
)
si.customer = "_Test Customer" si.customer = "_Test Customer"
si.due_date = date si.due_date = date
si.get("items")[0].rate = 25000 si.get("items")[0].rate = 25000

View File

@@ -0,0 +1,44 @@
{
"allow_roles": [
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
},
{
"role": "Quality Manager"
}
],
"creation": "2026-02-23 20:56:50.917521",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 0,
"is_complete": 0,
"modified": "2026-02-23 22:51:11.027665",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Learn Asset"
},
{
"step": "Create Asset Category"
},
{
"step": "Create Asset Item"
},
{
"step": "Create Asset Location"
},
{
"step": "Create Existing Asset"
},
{
"step": "View Balance Sheet"
}
],
"title": "Assets Setup!"
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Asset Category",
"creation": "2026-02-23 20:50:50.211884",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:50:50.211884",
"modified_by": "Administrator",
"name": "Create Asset Category",
"owner": "Administrator",
"reference_document": "Asset Category",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Asset Category",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Create Entry",
"action_label": "Create Asset Item",
"creation": "2026-02-23 20:52:40.135614",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:31:53.211343",
"modified_by": "Administrator",
"name": "Create Asset Item",
"owner": "Administrator",
"reference_document": "Item",
"route_options": "{\n \"is_fixed_asset\": 1,\n \"is_stock_item\": 0\n}",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Asset Item",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Asset Location",
"creation": "2026-02-23 20:53:07.450876",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:53:07.450876",
"modified_by": "Administrator",
"name": "Create Asset Location",
"owner": "Administrator",
"reference_document": "Location",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Asset Location",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Create Entry",
"action_label": "Create Existing Asset",
"creation": "2026-02-23 20:54:25.961869",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 3,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:31:48.789836",
"modified_by": "Administrator",
"name": "Create Existing Asset",
"owner": "Administrator",
"reference_document": "Asset",
"route_options": "{\n \"asset_type\": \"Existing Asset\"\n}",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Existing Asset",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "View Docs",
"action_label": "Learn Asset",
"creation": "2026-02-23 21:00:47.254648",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:25.734547",
"modified_by": "Administrator",
"name": "Learn Asset",
"owner": "Administrator",
"path": "https://docs.frappe.io/erpnext/assets/setup/asset-category",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Learn Asset",
"validate_action": 1
}

View File

@@ -0,0 +1,23 @@
{
"action": "View Report",
"action_label": "View Balance Sheet",
"creation": "2026-02-23 19:22:57.651194",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:39.178107",
"modified_by": "Administrator",
"name": "View Balance Sheet",
"owner": "Administrator",
"reference_report": "Balance Sheet",
"report_description": "View Balance Sheet",
"report_reference_doctype": "GL Entry",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "View Balance Sheet",
"validate_action": 1
}

View File

@@ -428,7 +428,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
this.frm.add_custom_button( this.frm.add_custom_button(
__("Payment Request"), __("Payment Request"),
function () { function () {
me.make_payment_request(); me.make_payment_request_with_schedule();
}, },
__("Create") __("Create")
); );
@@ -461,27 +461,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
} }
} }
get_items_from_open_material_requests() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier",
args: {
supplier: this.frm.doc.supplier,
},
source_doctype: "Material Request",
source_name: this.frm.doc.supplier,
target: this.frm,
setters: {
company: this.frm.doc.company,
},
get_query_filters: {
docstatus: ["!=", 2],
supplier: this.frm.doc.supplier,
},
get_query_method:
"erpnext.stock.doctype.material_request.material_request.get_material_requests_based_on_supplier",
});
}
validate() { validate() {
set_schedule_date(this.frm); set_schedule_date(this.frm);
} }

View File

@@ -9,23 +9,20 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"supplier_section", "supplier_section",
"company",
"title", "title",
"naming_series", "naming_series",
"supplier",
"supplier_name",
"order_confirmation_no", "order_confirmation_no",
"order_confirmation_date", "order_confirmation_date",
"get_items_from_open_material_requests",
"mps",
"column_break_7", "column_break_7",
"transaction_date", "transaction_date",
"schedule_date", "schedule_date",
"column_break1", "column_break1",
"supplier", "company",
"is_subcontracted", "is_subcontracted",
"supplier_name",
"has_unit_price_items", "has_unit_price_items",
"supplier_warehouse", "supplier_warehouse",
"amended_from",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
@@ -79,16 +76,19 @@
"taxes_and_charges_deducted", "taxes_and_charges_deducted",
"total_taxes_and_charges", "total_taxes_and_charges",
"totals_section", "totals_section",
"base_grand_total",
"base_rounding_adjustment",
"base_in_words",
"base_rounded_total",
"column_break4",
"grand_total", "grand_total",
"in_words",
"column_break4",
"disable_rounded_total",
"rounding_adjustment", "rounding_adjustment",
"rounded_total", "rounded_total",
"disable_rounded_total", "base_totals_section",
"in_words", "base_grand_total",
"base_in_words",
"column_break_jkoz",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_tnkm",
"advance_paid", "advance_paid",
"discount_section", "discount_section",
"apply_discount_on", "apply_discount_on",
@@ -154,11 +154,13 @@
"auto_repeat", "auto_repeat",
"update_auto_repeat_reference", "update_auto_repeat_reference",
"additional_info_section", "additional_info_section",
"is_internal_supplier", "party_account_currency",
"represents_company", "represents_company",
"ref_sq", "ref_sq",
"amended_from",
"column_break_74", "column_break_74",
"party_account_currency", "mps",
"is_internal_supplier",
"inter_company_order_reference", "inter_company_order_reference",
"is_old_subcontracting_flow", "is_old_subcontracting_flow",
"connections_tab" "connections_tab"
@@ -206,13 +208,6 @@
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
{
"depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))",
"description": "Fetch items based on Default Supplier.",
"fieldname": "get_items_from_open_material_requests",
"fieldtype": "Button",
"label": "Get Items from Open Material Requests"
},
{ {
"bold": 1, "bold": 1,
"fetch_from": "supplier.supplier_name", "fetch_from": "supplier.supplier_name",
@@ -773,7 +768,7 @@
{ {
"fieldname": "base_grand_total", "fieldname": "base_grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total (Company Currency)", "label": "Grand Total",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "grand_total", "oldfieldname": "grand_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
@@ -785,7 +780,7 @@
"depends_on": "eval:!doc.disable_rounded_total", "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
@@ -794,7 +789,7 @@
{ {
"fieldname": "base_in_words", "fieldname": "base_in_words",
"fieldtype": "Data", "fieldtype": "Data",
"label": "In Words (Company Currency)", "label": "In Words",
"length": 240, "length": 240,
"oldfieldname": "in_words", "oldfieldname": "in_words",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -804,7 +799,7 @@
{ {
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounded Total (Company Currency)", "label": "Rounded Total",
"oldfieldname": "rounded_total", "oldfieldname": "rounded_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
@@ -862,7 +857,7 @@
{ {
"fieldname": "advance_paid", "fieldname": "advance_paid",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Advance Paid", "label": "Advance Paid (Company Currency)",
"no_copy": 1, "no_copy": 1,
"options": "party_account_currency", "options": "party_account_currency",
"print_hide": 1, "print_hide": 1,
@@ -1301,8 +1296,21 @@
"hidden": 1, "hidden": 1,
"label": "Item Wise Tax Details", "label": "Item Wise Tax Details",
"no_copy": 1, "no_copy": 1,
"options": "Item Wise Tax Detail", "options": "Item Wise Tax Detail"
"print_hide": 1 },
{
"fieldname": "column_break_jkoz",
"fieldtype": "Column Break"
},
{
"fieldname": "base_totals_section",
"fieldtype": "Section Break",
"label": "Totals (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "section_break_tnkm",
"fieldtype": "Section Break"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -1310,7 +1318,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-02-03 14:44:55.192192", "modified": "2026-02-23 14:22:33.323946",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@@ -191,6 +191,9 @@ class PurchaseOrder(BuyingController):
self.set_has_unit_price_items() self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items self.flags.allow_zero_qty = self.has_unit_price_items
if self.is_subcontracted:
self.status_updater[0]["source_field"] = "fg_item_qty"
def validate(self): def validate(self):
super().validate() super().validate()

View File

@@ -73,6 +73,12 @@ frappe.ui.form.on("Supplier", {
}; };
}, },
supplier_group(frm) {
if (frm.doc.supplier_group) {
frm.trigger("get_supplier_group_details");
}
},
refresh: function (frm) { refresh: function (frm) {
if (frappe.defaults.get_default("supp_master_name") != "Naming Series") { if (frappe.defaults.get_default("supp_master_name") != "Naming Series") {
frm.toggle_display("naming_series", false); frm.toggle_display("naming_series", false);
@@ -111,14 +117,6 @@ frappe.ui.form.on("Supplier", {
__("View") __("View")
); );
frm.add_custom_button(
__("Get Supplier Group Details"),
function () {
frm.trigger("get_supplier_group_details");
},
__("Actions")
);
if ( if (
cint(frappe.defaults.get_default("enable_common_party_accounting")) && cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
frappe.model.can_create("Party Link") frappe.model.can_create("Party Link")

View File

@@ -11,11 +11,12 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series", "naming_series",
"supplier_type",
"supplier_name", "supplier_name",
"country", "gender",
"column_break0", "column_break0",
"supplier_group", "supplier_group",
"supplier_type", "country",
"is_transporter", "is_transporter",
"image", "image",
"defaults_section", "defaults_section",
@@ -23,24 +24,12 @@
"default_bank_account", "default_bank_account",
"column_break_10", "column_break_10",
"default_price_list", "default_price_list",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
"column_break_16",
"companies",
"column_break2", "column_break2",
"supplier_details", "supplier_details",
"column_break_30", "column_break_30",
"website", "website",
"language", "language",
"customer_numbers", "customer_numbers",
"dashboard_tab",
"tax_tab",
"tax_id",
"tax_category",
"column_break_27",
"tax_withholding_category",
"tax_withholding_group",
"contact_and_address_tab", "contact_and_address_tab",
"address_contacts", "address_contacts",
"address_html", "address_html",
@@ -54,19 +43,32 @@
"supplier_primary_contact", "supplier_primary_contact",
"mobile_no", "mobile_no",
"email_id", "email_id",
"tax_tab",
"tax_id",
"tax_category",
"column_break_27",
"tax_withholding_category",
"tax_withholding_group",
"accounting_tab", "accounting_tab",
"payment_terms", "payment_terms",
"default_accounts_section", "default_accounts_section",
"accounts", "accounts",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
"column_break_16",
"companies",
"settings_tab", "settings_tab",
"allow_purchase_invoice_creation_without_purchase_order", "allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt", "allow_purchase_invoice_creation_without_purchase_receipt",
"column_break_54", "column_break_54",
"is_frozen",
"disabled", "disabled",
"rfq_and_purchase_order_settings_section",
"is_frozen",
"warn_rfqs", "warn_rfqs",
"warn_pos",
"prevent_rfqs", "prevent_rfqs",
"column_break_oxjw",
"warn_pos",
"prevent_pos", "prevent_pos",
"block_supplier_section", "block_supplier_section",
"on_hold", "on_hold",
@@ -75,7 +77,7 @@
"release_date", "release_date",
"portal_users_tab", "portal_users_tab",
"portal_users", "portal_users",
"column_break_1mqv" "dashboard_tab"
], ],
"fields": [ "fields": [
{ {
@@ -398,7 +400,7 @@
{ {
"fieldname": "dashboard_tab", "fieldname": "dashboard_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Dashboard", "label": "Connections",
"show_dashboard": 1 "show_dashboard": 1
}, },
{ {
@@ -430,7 +432,7 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "internal_supplier_section", "fieldname": "internal_supplier_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Internal Supplier" "label": "Internal Supplier Accounting"
}, },
{ {
"fieldname": "column_break_16", "fieldname": "column_break_16",
@@ -469,10 +471,6 @@
"label": "Supplier Portal Users", "label": "Supplier Portal Users",
"options": "Portal User" "options": "Portal User"
}, },
{
"fieldname": "column_break_1mqv",
"fieldtype": "Column Break"
},
{ {
"fieldname": "column_break_mglr", "fieldname": "column_break_mglr",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -488,6 +486,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Tax Withholding Group", "label": "Tax Withholding Group",
"options": "Tax Withholding Group" "options": "Tax Withholding Group"
},
{
"depends_on": "eval:doc.supplier_type == 'Individual'",
"fieldname": "gender",
"fieldtype": "Link",
"label": "Gender",
"options": "Gender"
},
{
"fieldname": "rfq_and_purchase_order_settings_section",
"fieldtype": "Section Break",
"label": "RFQ and Purchase Order Settings"
},
{
"fieldname": "column_break_oxjw",
"fieldtype": "Column Break"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -501,7 +515,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2026-02-06 12:58:01.398824", "modified": "2026-02-10 21:28:01.101808",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",

View File

@@ -49,6 +49,7 @@ class Supplier(TransactionBase):
default_price_list: DF.Link | None default_price_list: DF.Link | None
disabled: DF.Check disabled: DF.Check
email_id: DF.ReadOnly | None email_id: DF.ReadOnly | None
gender: DF.Link | None
hold_type: DF.Literal["", "All", "Invoices", "Payments"] hold_type: DF.Literal["", "All", "Invoices", "Payments"]
image: DF.AttachImage | None image: DF.AttachImage | None
is_frozen: DF.Check is_frozen: DF.Check
@@ -161,8 +162,6 @@ class Supplier(TransactionBase):
if doc.payment_terms: if doc.payment_terms:
self.payment_terms = doc.payment_terms self.payment_terms = doc.payment_terms
self.save()
def validate_internal_supplier(self): def validate_internal_supplier(self):
if not self.is_internal_supplier: if not self.is_internal_supplier:
self.represents_company = "" self.represents_company = ""

View File

@@ -0,0 +1,42 @@
{
"creation": "2026-02-22 16:46:17.299107",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"list_name": "List",
"modified": "2026-02-22 16:46:17.299107",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Form Tour",
"new_document_form": 0,
"owner": "Administrator",
"reference_doctype": "Supplier",
"report_name": "",
"save_on_complete": 1,
"steps": [
{
"description": "Enter the Full Name of the Supplier",
"fieldname": "supplier_name",
"fieldtype": "Data",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"label": "Supplier Name",
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Left",
"title": "Full Name",
"ui_tour": 0
}
],
"title": "Supplier Form Tour",
"track_steps": 0,
"ui_tour": 0,
"view_name": "Workspaces"
}

View File

@@ -0,0 +1,41 @@
{
"allow_roles": [
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
}
],
"creation": "2026-02-19 10:53:58.761773",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 0,
"is_complete": 0,
"modified": "2026-02-24 16:57:55.172763",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Create Supplier"
},
{
"step": "Create Item"
},
{
"step": "Create Purchase Order"
},
{
"step": "Create Purchase Invoice"
},
{
"step": "View Purchase Order Analysis"
},
{
"step": "Review Buying Settings"
}
],
"title": "Buying Setup! "
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Item",
"creation": "2026-02-19 12:38:40.865013",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 7,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-24 16:57:14.098288",
"modified_by": "Administrator",
"name": "Create Item",
"owner": "Administrator",
"reference_document": "Item",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Item",
"validate_action": 1
}

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Purchase Invoice",
"creation": "2026-02-19 12:38:14.868162",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 5,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:26:00.223899",
"modified_by": "Administrator",
"name": "Create Purchase Invoice",
"owner": "Administrator",
"reference_document": "Purchase Invoice",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Purchase Invoice",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Create Entry",
"action_label": "Create Purchase Order",
"creation": "2026-02-19 12:13:44.068135",
"docstatus": 0,
"doctype": "Onboarding Step",
"form_tour": "",
"idx": 2,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-24 16:57:37.904322",
"modified_by": "Administrator",
"name": "Create Purchase Order",
"owner": "Administrator",
"reference_document": "Purchase Order",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Purchase Order",
"validate_action": 1
}

View File

@@ -0,0 +1,22 @@
{
"action": "Create Entry",
"action_label": "Create supplier",
"creation": "2026-02-19 10:53:56.936107",
"description": "",
"docstatus": 0,
"doctype": "Onboarding Step",
"form_tour": "Supplier Form Tour",
"idx": 2,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.903633",
"modified_by": "Administrator",
"name": "Create Supplier",
"owner": "Administrator",
"reference_document": "Supplier",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Create Supplier",
"validate_action": 1
}

View File

@@ -0,0 +1,21 @@
{
"action": "Update Settings",
"action_label": "Review Buying Settings",
"creation": "2026-02-23 20:27:23.664752",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2026-02-24 16:57:14.031766",
"modified_by": "Administrator",
"name": "Review Buying Settings",
"owner": "Administrator",
"path": "desk/buying-settings",
"reference_document": "Buying Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Buying Settings",
"validate_action": 0
}

View File

@@ -0,0 +1,21 @@
{
"action": "Go to Page",
"action_label": "Set up company",
"creation": "2026-02-20 11:12:50.373049",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-22 16:44:20.499954",
"modified_by": "Administrator",
"name": "Setup Company",
"owner": "Administrator",
"path": "company",
"reference_document": "Company",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Company",
"validate_action": 1
}

View File

@@ -0,0 +1,23 @@
{
"action": "View Report",
"action_label": "View Purchase Order Analysis",
"creation": "2026-02-23 20:26:29.245112",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:35.794807",
"modified_by": "Administrator",
"name": "View Purchase Order Analysis",
"owner": "Administrator",
"reference_report": "Purchase Order Analysis",
"report_description": "View Purchase Order Analysis",
"report_reference_doctype": "Purchase Order",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "View Purchase Order Analysis",
"validate_action": 1
}

View File

@@ -4122,7 +4122,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_item.idx = len(parent.items) + 1 child_item.idx = len(parent.items) + 1
child_item.insert() child_item.insert()
else: else:
child_item.save() child_item.save(ignore_permissions=True)
parent.reload() parent.reload()
parent.flags.ignore_validate_update_after_submit = True parent.flags.ignore_validate_update_after_submit = True
@@ -4296,7 +4296,7 @@ def get_missing_company_details(doctype, docname):
from frappe.contacts.doctype.address.address import get_address_display_list from frappe.contacts.doctype.address.address import get_address_display_list
company = frappe.db.get_value(doctype, docname, "company") company = frappe.db.get_value(doctype, docname, "company")
if doctype == "Purchase Order": if doctype in ["Purchase Order", "Purchase Invoice"]:
company_address = frappe.db.get_value(doctype, docname, "billing_address") company_address = frappe.db.get_value(doctype, docname, "billing_address")
else: else:
company_address = frappe.db.get_value(doctype, docname, "company_address") company_address = frappe.db.get_value(doctype, docname, "company_address")
@@ -4392,6 +4392,8 @@ def update_doc_company_address(current_doctype, docname, company_address, detail
address_field_map = { address_field_map = {
"Purchase Order": ("billing_address", "billing_address_display"), "Purchase Order": ("billing_address", "billing_address_display"),
"Purchase Invoice": ("billing_address", "billing_address_display"),
"Sales Order": ("company_address", "company_address_display"),
"Sales Invoice": ("company_address", "company_address_display"), "Sales Invoice": ("company_address", "company_address_display"),
"Delivery Note": ("company_address", "company_address_display"), "Delivery Note": ("company_address", "company_address_display"),
"POS Invoice": ("company_address", "company_address_display"), "POS Invoice": ("company_address", "company_address_display"),

View File

@@ -498,10 +498,34 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows) sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self): def set_incoming_rate(self):
def reset_incoming_rate():
old_item = next(
(
item
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
if item.name == d.name
),
None,
)
if old_item:
old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty"))
if (
old_item.item_code != d.item_code
or old_item.warehouse != d.warehouse
or old_qty != qty
or old_item.serial_no != d.serial_no
or get_serial_nos(old_item.serial_and_batch_bundle)
!= get_serial_nos(d.serial_and_batch_bundle)
or old_item.batch_no != d.batch_no
or get_batch_nos(old_item.serial_and_batch_bundle)
!= get_batch_nos(d.serial_and_batch_bundle)
):
d.incoming_rate = 0
if self.doctype not in ("Delivery Note", "Sales Invoice"): if self.doctype not in ("Delivery Note", "Sales Invoice"):
return return
from erpnext.stock.serial_batch_bundle import get_batch_nos from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
allow_at_arms_length_price = frappe.get_cached_value( allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price" "Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
@@ -510,6 +534,8 @@ class SellingController(StockController):
"Selling Settings", "set_zero_rate_for_expired_batch" "Selling Settings", "set_zero_rate_for_expired_batch"
) )
is_standalone = self.is_return and not self.return_against
old_doc = self.get_doc_before_save() old_doc = self.get_doc_before_save()
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
for d in items: for d in items:
@@ -541,27 +567,7 @@ class SellingController(StockController):
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty")) qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
if old_doc: if old_doc:
old_item = next( reset_incoming_rate()
(
item
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
if item.name == d.name
),
None,
)
if old_item:
old_qty = flt(
old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
)
if (
old_item.item_code != d.item_code
or old_item.warehouse != d.warehouse
or old_qty != qty
or old_item.batch_no != d.batch_no
or get_batch_nos(old_item.serial_and_batch_bundle)
!= get_batch_nos(d.serial_and_batch_bundle)
):
d.incoming_rate = 0
if ( if (
not d.incoming_rate not d.incoming_rate
@@ -583,11 +589,12 @@ class SellingController(StockController):
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"voucher_detail_no": d.name, "voucher_detail_no": d.name,
"allow_zero_valuation": d.get("allow_zero_valuation"), "allow_zero_valuation": d.get("allow_zero_valuation_rate"),
"batch_no": d.batch_no, "batch_no": d.batch_no,
"serial_no": d.serial_no, "serial_no": d.serial_no,
}, },
raise_error_if_no_rate=False, raise_error_if_no_rate=is_standalone,
fallbacks=not is_standalone,
) )
if ( if (

View File

@@ -119,7 +119,7 @@ status_map = {
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"], ["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
[ [
"Ordered", "Ordered",
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture', 'Subcontracting']",
], ],
[ [
"Transferred", "Transferred",
@@ -511,13 +511,6 @@ class StatusUpdater(Document):
if d.doctype != args["source_dt"]: if d.doctype != args["source_dt"]:
continue continue
if (
d.get("material_request")
and frappe.db.get_value("Material Request", d.material_request, "material_request_type")
== "Subcontracting"
):
args.update({"source_field": "fg_item_qty"})
self._update_modified(args, update_modified) self._update_modified(args, update_modified)
# updates qty in the child table # updates qty in the child table

View File

@@ -1395,6 +1395,68 @@ def make_rm_stock_entry(
if target_doc and target_doc.get("items"): if target_doc and target_doc.get("items"):
target_doc.items = [] target_doc.items = []
def post_process(source_doc, target_doc):
target_doc.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
target_doc.purchase_order = source_doc.name
else:
target_doc.subcontracting_order = source_doc.name
target_doc.set_stock_entry_type()
over_transfer_allowance = frappe.get_single_value(
"Buying Settings", "over_transfer_allowance"
)
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
rm_item.get("main_item_code") == fg_item_code
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
qty = rm_item.get("qty") or max(
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
)
if qty <= 0 and rm_item.get("total_supplied_qty"):
per_transferred = (
flt(
rm_item.get("total_supplied_qty") / rm_item.get("required_qty"),
frappe.db.get_default("float_precision"),
)
* 100
)
if per_transferred >= 100 + over_transfer_allowance:
continue
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": qty,
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": source_doc.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
"allow_alternative_item"
),
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
"serial_no": rm_item.get("serial_no")
if rm_item.get("use_serial_batch_fields")
else None,
"batch_no": rm_item.get("batch_no")
if rm_item.get("use_serial_batch_fields")
else None,
}
}
target_doc.add_to_stock_entry_detail(items_dict)
stock_entry = get_mapped_doc( stock_entry = get_mapped_doc(
order_doctype, order_doctype,
subcontract_order.name, subcontract_order.name,
@@ -1415,67 +1477,9 @@ def make_rm_stock_entry(
}, },
target_doc, target_doc,
ignore_child_tables=True, ignore_child_tables=True,
postprocess=post_process,
) )
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.set_stock_entry_type()
over_transfer_allowance = frappe.get_single_value("Buying Settings", "over_transfer_allowance")
for fg_item_code in fg_item_code_list:
for rm_item in rm_items:
if (
rm_item.get("main_item_code") == fg_item_code
or rm_item.get("item_code") == fg_item_code
):
rm_item_code = rm_item.get("rm_item_code")
qty = rm_item.get("qty") or max(
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
)
if qty <= 0 and rm_item.get("total_supplied_qty"):
per_transferred = (
flt(
rm_item.get("total_supplied_qty") / rm_item.get("required_qty"),
frappe.db.get_default("float_precision"),
)
* 100
)
if per_transferred >= 100 + over_transfer_allowance:
continue
items_dict = {
rm_item_code: {
rm_detail_field: rm_item.get("name"),
"item_name": rm_item.get("item_name")
or item_wh.get(rm_item_code, {}).get("item_name", ""),
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": qty,
"from_warehouse": rm_item.get("warehouse")
or rm_item.get("reserve_warehouse"),
"to_warehouse": subcontract_order.supplier_warehouse,
"stock_uom": rm_item.get("stock_uom"),
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
"main_item_code": fg_item_code,
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
"allow_alternative_item"
),
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
"serial_no": rm_item.get("serial_no")
if rm_item.get("use_serial_batch_fields")
else None,
"batch_no": rm_item.get("batch_no")
if rm_item.get("use_serial_batch_fields")
else None,
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
if target_doc: if target_doc:
return stock_entry return stock_entry
else: else:
@@ -1507,6 +1511,8 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
def make_return_stock_entry_for_subcontract( def make_return_stock_entry_for_subcontract(
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
): ):
rm_detail_field = "po_detail" if order_doctype == "Purchase Order" else "sco_rm_detail"
def post_process(source_doc, target_doc): def post_process(source_doc, target_doc):
target_doc.purpose = "Material Transfer" target_doc.purpose = "Material Transfer"
@@ -1517,6 +1523,21 @@ def make_return_stock_entry_for_subcontract(
target_doc.company = source_doc.company target_doc.company = source_doc.company
target_doc.is_return = 1 target_doc.is_return = 1
for _key, value in available_materials.items():
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(target_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(target_doc, value, value.qty, rm_details, rm_detail_field)
target_doc.set_stock_entry_type()
ste_doc = get_mapped_doc( ste_doc = get_mapped_doc(
order_doctype, order_doctype,
@@ -1531,27 +1552,6 @@ def make_return_stock_entry_for_subcontract(
postprocess=post_process, postprocess=post_process,
) )
if order_doctype == "Purchase Order":
rm_detail_field = "po_detail"
else:
rm_detail_field = "sco_rm_detail"
for _key, value in available_materials.items():
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
ste_doc.set_stock_entry_type()
return ste_doc return ste_doc

View File

@@ -672,6 +672,11 @@ class calculate_taxes_and_totals:
else: else:
self.grand_total_diff = 0 self.grand_total_diff = 0
# Apply rounding adjustment to grand_total_for_distributing_discount
# to prevent precision errors during discount distribution
if hasattr(self, "grand_total_for_distributing_discount") and not self.discount_amount_applied:
self.grand_total_for_distributing_discount += self.grand_total_diff
def calculate_totals(self): def calculate_totals(self):
grand_total_diff = self.grand_total_diff grand_total_diff = self.grand_total_diff

View File

@@ -59,3 +59,41 @@ class TestTaxesAndTotals(AccountsTestMixin, IntegrationTestCase):
self.assertEqual(so.total, 1500) self.assertEqual(so.total, 1500)
self.assertAlmostEqual(so.net_total, 1272.73, places=2) self.assertAlmostEqual(so.net_total, 1272.73, places=2)
self.assertEqual(so.grand_total, 1400) self.assertEqual(so.grand_total, 1400)
def test_100_percent_discount_with_inclusive_tax(self):
"""Test that 100% discount with inclusive taxes results in zero net_total"""
so = make_sales_order(do_not_save=1)
so.apply_discount_on = "Grand Total"
so.items[0].qty = 2
so.items[0].rate = 1300
so.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Account VAT",
"included_in_print_rate": True,
"rate": 9,
},
)
so.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Account Service Tax",
"included_in_print_rate": True,
"rate": 9,
},
)
so.save()
# Apply 100% discount
so.discount_amount = 2600
calculate_taxes_and_totals(so)
# net_total should be exactly 0, not 0.01
self.assertEqual(so.net_total, 0)
self.assertEqual(so.grand_total, 0)

View File

@@ -270,7 +270,7 @@ standard_portal_menu_items = [
"role": "Customer", "role": "Customer",
}, },
{"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, {"title": "Issues", "route": "/issues", "reference_doctype": "Issue", "role": "Customer"},
{"title": "Addresses", "route": "/addresses", "reference_doctype": "Address"}, {"title": "Addresses", "route": "/addresses", "reference_doctype": "Address", "role": "Customer"},
{ {
"title": "Timesheets", "title": "Timesheets",
"route": "/timesheets", "route": "/timesheets",

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,8 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"production_item_tab", "production_item_tab",
"item",
"company", "company",
"uom", "item",
"quantity", "quantity",
"cb0", "cb0",
"is_active", "is_active",
@@ -17,8 +16,6 @@
"allow_alternative_item", "allow_alternative_item",
"set_rate_of_sub_assembly_item_based_on_bom", "set_rate_of_sub_assembly_item_based_on_bom",
"is_phantom_bom", "is_phantom_bom",
"project",
"image",
"currency_detail", "currency_detail",
"rm_cost_as_per", "rm_cost_as_per",
"buying_price_list", "buying_price_list",
@@ -33,11 +30,9 @@
"column_break_23", "column_break_23",
"transfer_material_against", "transfer_material_against",
"routing", "routing",
"fg_based_operating_cost",
"column_break_joxb",
"default_source_warehouse",
"default_target_warehouse",
"fg_based_section_section", "fg_based_section_section",
"fg_based_operating_cost",
"column_break_omye",
"operating_cost_per_bom_quantity", "operating_cost_per_bom_quantity",
"operations_section", "operations_section",
"operations", "operations",
@@ -61,15 +56,25 @@
"column_break_26", "column_break_26",
"total_cost", "total_cost",
"base_total_cost", "base_total_cost",
"more_info_tab", "quality_inspection_tab",
"item_name",
"column_break_27",
"description",
"has_variants",
"quality_inspection_section_break", "quality_inspection_section_break",
"inspection_required", "inspection_required",
"column_break_dxp7", "column_break_dxp7",
"quality_inspection_template", "quality_inspection_template",
"more_info_tab",
"production_item_info_section",
"item_name",
"uom",
"image",
"column_break_27",
"description",
"has_variants",
"default_warehouse_section",
"default_source_warehouse",
"column_break_inep",
"default_target_warehouse",
"section_break_ouuf",
"project",
"section_break0", "section_break0",
"exploded_items", "exploded_items",
"website_section", "website_section",
@@ -451,7 +456,8 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "route", "fieldname": "route",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Route" "label": "Route",
"read_only": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -651,15 +657,11 @@
{ {
"default": "0", "default": "0",
"depends_on": "with_operations", "depends_on": "with_operations",
"description": "Users can consume raw materials and add semi-finished goods or final finished goods against the operation using job cards.", "description": "Users can make manufacture entry against Job Cards",
"fieldname": "track_semi_finished_goods", "fieldname": "track_semi_finished_goods",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Track Semi Finished Goods" "label": "Track Semi Finished Goods"
}, },
{
"fieldname": "column_break_joxb",
"fieldtype": "Column Break"
},
{ {
"fieldname": "default_source_warehouse", "fieldname": "default_source_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
@@ -677,6 +679,33 @@
"fieldname": "is_phantom_bom", "fieldname": "is_phantom_bom",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Phantom BOM" "label": "Is Phantom BOM"
},
{
"fieldname": "default_warehouse_section",
"fieldtype": "Section Break",
"label": "Default Warehouse"
},
{
"fieldname": "column_break_inep",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_omye",
"fieldtype": "Column Break"
},
{
"fieldname": "production_item_info_section",
"fieldtype": "Section Break",
"label": "Production Item Info"
},
{
"fieldname": "section_break_ouuf",
"fieldtype": "Section Break"
},
{
"fieldname": "quality_inspection_tab",
"fieldtype": "Tab Break",
"label": "Quality Inspection"
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@@ -684,7 +713,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-11-19 16:17:15.925156", "modified": "2026-02-06 17:23:15.255301",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -627,16 +627,6 @@ frappe.ui.form.on("Job Card", {
} }
}, },
validate: function (frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer");
}
},
reset_timer: function (frm) {
frm.set_value("started_time", "");
},
make_dashboard: function (frm) { make_dashboard: function (frm) {
if (frm.doc.__islocal) return; if (frm.doc.__islocal) return;
var section = ""; var section = "";
@@ -791,10 +781,6 @@ frappe.ui.form.on("Job Card Time Log", {
frm.events.set_total_completed_qty(frm); frm.events.set_total_completed_qty(frm);
}, },
to_time: function (frm) {
frm.set_value("started_time", "");
},
time_in_mins(frm, cdt, cdn) { time_in_mins(frm, cdt, cdn) {
let d = locals[cdt][cdn]; let d = locals[cdt][cdn];
if (d.time_in_mins) { if (d.time_in_mins) {

View File

@@ -6,15 +6,15 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"company",
"naming_series", "naming_series",
"work_order", "work_order",
"employee", "employee",
"is_subcontracted",
"column_break_4", "column_break_4",
"posting_date", "posting_date",
"company",
"project", "project",
"bom_no", "bom_no",
"is_subcontracted",
"semi_finished_good__finished_good_section", "semi_finished_good__finished_good_section",
"finished_good", "finished_good",
"production_item", "production_item",
@@ -70,22 +70,24 @@
"more_information", "more_information",
"item_name", "item_name",
"requested_qty", "requested_qty",
"status",
"operation_row_id",
"is_paused", "is_paused",
"track_semi_finished_goods", "track_semi_finished_goods",
"column_break_20", "column_break_20",
"remarks",
"section_break_dfoc",
"status",
"operation_row_id",
"amended_from",
"column_break_xhzg",
"operation_row_number", "operation_row_number",
"operation_id", "operation_id",
"sequence_id", "sequence_id",
"remarks", "section_break_jcmx",
"serial_and_batch_bundle",
"batch_no",
"serial_no", "serial_no",
"serial_and_batch_bundle",
"column_break_swqr",
"barcode", "barcode",
"started_time", "batch_no",
"current_time",
"amended_from",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -210,7 +212,7 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "more_information", "fieldname": "more_information",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "More Information" "label": "More Info"
}, },
{ {
"fieldname": "operation_id", "fieldname": "operation_id",
@@ -259,16 +261,6 @@
"options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted", "options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted",
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"fieldname": "started_time",
"fieldtype": "Datetime",
"hidden": 1,
"label": "Started Time",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@@ -315,16 +307,7 @@
"fetch_if_empty": 1, "fetch_if_empty": 1,
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Read Only", "fieldtype": "Read Only",
"label": "Item Name" "label": "Item Name",
},
{
"allow_on_submit": 1,
"fieldname": "current_time",
"fieldtype": "Int",
"hidden": 1,
"label": "Current Time",
"no_copy": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -466,7 +449,8 @@
"label": "Serial and Batch Bundle", "label": "Serial and Batch Bundle",
"no_copy": 1, "no_copy": 1,
"options": "Serial and Batch Bundle", "options": "Serial and Batch Bundle",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"depends_on": "process_loss_qty", "depends_on": "process_loss_qty",
@@ -621,12 +605,30 @@
"fieldname": "track_semi_finished_goods", "fieldname": "track_semi_finished_goods",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Track Semi Finished Goods" "label": "Track Semi Finished Goods"
},
{
"fieldname": "section_break_jcmx",
"fieldtype": "Section Break",
"label": "Serial / Batch"
},
{
"fieldname": "column_break_swqr",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_dfoc",
"fieldtype": "Section Break",
"label": "Status and Reference"
},
{
"fieldname": "column_break_xhzg",
"fieldtype": "Column Break"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-12-22 14:20:07.817118", "modified": "2026-02-06 18:27:03.178783",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@@ -82,7 +82,6 @@ class JobCard(Document):
batch_no: DF.Link | None batch_no: DF.Link | None
bom_no: DF.Link | None bom_no: DF.Link | None
company: DF.Link company: DF.Link
current_time: DF.Int
employee: DF.TableMultiSelect[JobCardTimeLog] employee: DF.TableMultiSelect[JobCardTimeLog]
expected_end_date: DF.Datetime | None expected_end_date: DF.Datetime | None
expected_start_date: DF.Datetime | None expected_start_date: DF.Datetime | None
@@ -118,7 +117,6 @@ class JobCard(Document):
serial_no: DF.SmallText | None serial_no: DF.SmallText | None
skip_material_transfer: DF.Check skip_material_transfer: DF.Check
source_warehouse: DF.Link | None source_warehouse: DF.Link | None
started_time: DF.Datetime | None
status: DF.Literal[ status: DF.Literal[
"Open", "Open",
"Work In Progress", "Work In Progress",

View File

@@ -56,7 +56,6 @@ class TestRouting(IntegrationTestCase):
self.assertEqual(job_card_doc.total_completed_qty, 10) self.assertEqual(job_card_doc.total_completed_qty, 10)
wo_doc.cancel() wo_doc.cancel()
wo_doc.delete()
def test_update_bom_operation_time(self): def test_update_bom_operation_time(self):
"""Update cost shouldn't update routing times.""" """Update cost shouldn't update routing times."""

View File

@@ -598,6 +598,33 @@ class TestWorkOrder(IntegrationTestCase):
work_order1.cancel() work_order1.cancel()
work_order.cancel() work_order.cancel()
def test_planned_qty_updates_after_closing_work_order(self):
item_code = "_Test FG Item"
fg_warehouse = "_Test Warehouse 1 - _TC"
planned_before = (
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
or 0
)
wo = make_wo_order_test_record(item=item_code, fg_warehouse=fg_warehouse, qty=10)
planned_after_submit = (
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
or 0
)
self.assertEqual(planned_after_submit, planned_before + 10)
close_work_order(wo.name, "Closed")
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "status"), "Closed")
planned_after_close = (
frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": fg_warehouse}, "planned_qty")
or 0
)
self.assertEqual(planned_after_close, planned_before)
def test_work_order_with_non_transfer_item(self): def test_work_order_with_non_transfer_item(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")

View File

@@ -8,29 +8,23 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"item", "item",
"naming_series",
"status",
"production_item",
"item_name",
"image",
"bom_no",
"mps",
"subcontracting_inward_order",
"subcontracting_inward_order_item",
"sales_order",
"column_break1",
"company", "company",
"naming_series",
"production_item",
"bom_no",
"column_break1",
"qty", "qty",
"project", "sales_order",
"track_semi_finished_goods", "track_semi_finished_goods",
"reserve_stock", "reserve_stock",
"column_break_agjv", "section_break_vrpa",
"max_producible_qty", "max_producible_qty",
"material_transferred_for_manufacturing", "material_transferred_for_manufacturing",
"additional_transferred_qty", "additional_transferred_qty",
"column_break_ezmq",
"produced_qty", "produced_qty",
"disassembled_qty",
"process_loss_qty", "process_loss_qty",
"disassembled_qty",
"warehouses", "warehouses",
"source_warehouse", "source_warehouse",
"wip_warehouse", "wip_warehouse",
@@ -65,22 +59,34 @@
"column_break_24", "column_break_24",
"corrective_operation_cost", "corrective_operation_cost",
"total_operating_cost", "total_operating_cost",
"more_info",
"production_item_info_section",
"image",
"item_name",
"stock_uom",
"column_break2",
"description",
"serial_no_and_batch_for_finished_good_section", "serial_no_and_batch_for_finished_good_section",
"has_serial_no", "has_serial_no",
"has_batch_no", "has_batch_no",
"column_break_18", "column_break_18",
"batch_size", "batch_size",
"more_info", "reference_section",
"description", "project",
"stock_uom", "subcontracting_inward_order",
"column_break2",
"material_request",
"material_request_item",
"sales_order_item",
"production_plan", "production_plan",
"production_plan_item", "mps",
"material_request",
"column_break_xbhk",
"material_request_item",
"subcontracting_inward_order_item",
"sales_order_item",
"production_plan_sub_assembly_item", "production_plan_sub_assembly_item",
"production_plan_item",
"product_bundle_item", "product_bundle_item",
"section_break_ynih",
"status",
"column_break_cvuw",
"amended_from", "amended_from",
"connections_tab" "connections_tab"
], ],
@@ -149,6 +155,7 @@
{ {
"fieldname": "bom_no", "fieldname": "bom_no",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "BOM No", "label": "BOM No",
"oldfieldname": "bom_no", "oldfieldname": "bom_no",
"oldfieldtype": "Link", "oldfieldtype": "Link",
@@ -198,6 +205,7 @@
"default": "1.0", "default": "1.0",
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1,
"label": "Qty To Manufacture", "label": "Qty To Manufacture",
"oldfieldname": "qty", "oldfieldname": "qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
@@ -431,7 +439,8 @@
"fieldname": "material_request", "fieldname": "material_request",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Material Request", "label": "Material Request",
"options": "Material Request" "options": "Material Request",
"read_only": 1
}, },
{ {
"fieldname": "material_request_item", "fieldname": "material_request_item",
@@ -516,7 +525,7 @@
}, },
{ {
"collapsible": 1, "collapsible": 1,
"depends_on": "eval:!doc.__islocal", "depends_on": "eval:!doc.__islocal && doc.track_semi_finished_goods === 0 && (doc.has_serial_no === 1 || doc.has_batch_no === 1)",
"fieldname": "serial_no_and_batch_for_finished_good_section", "fieldname": "serial_no_and_batch_for_finished_good_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Finished Good Serial / Batch" "label": "Finished Good Serial / Batch"
@@ -624,10 +633,8 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_agjv", "default": "0",
"fieldtype": "Column Break" "depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0",
},
{
"fieldname": "additional_transferred_qty", "fieldname": "additional_transferred_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Additional Transferred Qty", "label": "Additional Transferred Qty",
@@ -659,6 +666,36 @@
"no_copy": 1, "no_copy": 1,
"non_negative": 1, "non_negative": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "section_break_vrpa",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ezmq",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ynih",
"fieldtype": "Section Break"
},
{
"fieldname": "reference_section",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "column_break_cvuw",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_xbhk",
"fieldtype": "Column Break"
},
{
"fieldname": "production_item_info_section",
"fieldtype": "Section Break",
"label": "Production Item Info"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -667,7 +704,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-10-12 14:24:57.699749", "modified": "2026-02-06 17:53:11.295600",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@@ -557,7 +557,7 @@ class WorkOrder(Document):
if status != self.status: if status != self.status:
self.db_set("status", status) self.db_set("status", status)
self.update_required_items() self.update_required_items()
return status or self.status return status or self.status
@@ -780,7 +780,6 @@ class WorkOrder(Document):
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
self.on_close_or_cancel() self.on_close_or_cancel()
self.delete_job_card()
def on_close_or_cancel(self): def on_close_or_cancel(self):
if self.production_plan and frappe.db.exists( if self.production_plan and frappe.db.exists(
@@ -794,7 +793,6 @@ class WorkOrder(Document):
self.update_planned_qty() self.update_planned_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.update_reserved_qty_for_production() self.update_reserved_qty_for_production()
self.delete_auto_created_batch_and_serial_no()
if self.reserve_stock: if self.reserve_stock:
self.update_stock_reservation() self.update_stock_reservation()
@@ -869,6 +867,9 @@ class WorkOrder(Document):
).run() ).run()
def create_serial_no_batch_no(self): def create_serial_no_batch_no(self):
if self.track_semi_finished_goods:
return
if not (self.has_serial_no or self.has_batch_no): if not (self.has_serial_no or self.has_batch_no):
return return
@@ -923,13 +924,6 @@ class WorkOrder(Document):
) )
) )
def delete_auto_created_batch_and_serial_no(self):
for row in frappe.get_all("Serial No", filters={"work_order": self.name}):
frappe.delete_doc("Serial No", row.name)
for row in frappe.get_all("Batch", filters={"reference_name": self.name}):
frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args): def make_serial_nos(self, args):
item_details = frappe.get_cached_value( item_details = frappe.get_cached_value(
"Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1 "Item", self.production_item, ["serial_no_series", "item_name", "description"], as_dict=1
@@ -1381,10 +1375,6 @@ class WorkOrder(Document):
if self.actual_start_date and self.actual_end_date: if self.actual_start_date and self.actual_end_date:
self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60) self.lead_time = flt(time_diff_in_hours(self.actual_end_date, self.actual_start_date) * 60)
def delete_job_card(self):
for d in frappe.get_all("Job Card", ["name"], {"work_order": self.name}):
frappe.delete_doc("Job Card", d.name)
def validate_production_item(self): def validate_production_item(self):
if frappe.get_cached_value("Item", self.production_item, "has_variants"): if frappe.get_cached_value("Item", self.production_item, "has_variants"):
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError) frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
@@ -1540,6 +1530,7 @@ class WorkOrder(Document):
"operation": item.operation or operation, "operation": item.operation or operation,
"item_code": item.item_code, "item_code": item.item_code,
"item_name": item.item_name, "item_name": item.item_name,
"stock_uom": item.stock_uom,
"description": item.description, "description": item.description,
"allow_alternative_item": item.allow_alternative_item, "allow_alternative_item": item.allow_alternative_item,
"required_qty": item.qty, "required_qty": item.qty,
@@ -1577,7 +1568,7 @@ class WorkOrder(Document):
.select( .select(
ste_child.item_code, ste_child.item_code,
ste_child.original_item, ste_child.original_item,
fn.Sum(ste_child.qty).as_("qty"), fn.Sum(ste_child.transfer_qty).as_("qty"),
) )
.where( .where(
(ste.docstatus == 1) (ste.docstatus == 1)
@@ -1650,7 +1641,7 @@ class WorkOrder(Document):
.select( .select(
ste_child.item_code, ste_child.item_code,
ste_child.original_item, ste_child.original_item,
fn.Sum(ste_child.qty).as_("qty"), fn.Sum(ste_child.transfer_qty).as_("qty"),
) )
.where( .where(
(ste.docstatus == 1) (ste.docstatus == 1)
@@ -2160,7 +2151,7 @@ def get_consumed_qty(work_order, item_code):
frappe.qb.from_(stock_entry) frappe.qb.from_(stock_entry)
.inner_join(stock_entry_detail) .inner_join(stock_entry_detail)
.on(stock_entry_detail.parent == stock_entry.name) .on(stock_entry_detail.parent == stock_entry.name)
.select(fn.Sum(stock_entry_detail.qty).as_("qty")) .select(fn.Sum(stock_entry_detail.transfer_qty).as_("qty"))
.where( .where(
(stock_entry.work_order == work_order) (stock_entry.work_order == work_order)
& (stock_entry.purpose.isin(["Manufacture", "Material Consumption for Manufacture"])) & (stock_entry.purpose.isin(["Manufacture", "Material Consumption for Manufacture"]))
@@ -2351,7 +2342,11 @@ def set_work_order_ops(name):
@frappe.whitelist() @frappe.whitelist()
def make_stock_entry( def make_stock_entry(
work_order_id, purpose, qty=None, target_warehouse=None, is_additional_transfer_entry=False work_order_id: str,
purpose: str,
qty: float | None = None,
target_warehouse: str | None = None,
is_additional_transfer_entry: bool = False,
): ):
work_order = frappe.get_doc("Work Order", work_order_id) work_order = frappe.get_doc("Work Order", work_order_id)
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
@@ -2373,9 +2368,6 @@ def make_stock_entry(
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
) )
if work_order.bom_no:
stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")
if purpose == "Material Transfer for Manufacture": if purpose == "Material Transfer for Manufacture":
stock_entry.to_warehouse = wip_warehouse stock_entry.to_warehouse = wip_warehouse
stock_entry.project = work_order.project stock_entry.project = work_order.project
@@ -2387,6 +2379,10 @@ def make_stock_entry(
) )
stock_entry.to_warehouse = work_order.fg_warehouse stock_entry.to_warehouse = work_order.fg_warehouse
stock_entry.project = work_order.project stock_entry.project = work_order.project
if work_order.bom_no:
stock_entry.inspection_required = frappe.db.get_value(
"BOM", work_order.bom_no, "inspection_required"
)
if purpose == "Disassemble": if purpose == "Disassemble":
stock_entry.from_warehouse = work_order.fg_warehouse stock_entry.from_warehouse = work_order.fg_warehouse
@@ -2506,8 +2502,8 @@ def close_work_order(work_order, status):
) )
) )
work_order.on_close_or_cancel()
work_order.update_status(status) work_order.update_status(status)
work_order.on_close_or_cancel()
frappe.msgprint(_("Work Order has been {0}").format(status)) frappe.msgprint(_("Work Order has been {0}").format(status))
work_order.notify_update() work_order.notify_update()
return work_order.status return work_order.status
@@ -2677,6 +2673,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
target_doc, target_doc,
) )
doc.purpose = "Material Transfer for Manufacture"
doc.for_qty = for_qty doc.for_qty = for_qty
doc.set_item_locations() doc.set_item_locations()

View File

@@ -233,7 +233,7 @@ class Workstation(Document):
@frappe.whitelist() @frappe.whitelist()
def get_job_cards(workstation, job_card=None): def get_job_cards(workstation: str):
if frappe.has_permission("Job Card", "read"): if frappe.has_permission("Job Card", "read"):
jc_data = frappe.get_all( jc_data = frappe.get_all(
"Job Card", "Job Card",
@@ -264,6 +264,7 @@ def get_job_cards(workstation, job_card=None):
"status": ["not in", ["Completed", "Stopped"]], "status": ["not in", ["Completed", "Stopped"]],
}, },
order_by="expected_start_date, expected_end_date", order_by="expected_start_date, expected_end_date",
limit=10,
) )
job_cards = [row.name for row in jc_data] job_cards = [row.name for row in jc_data]
@@ -517,7 +518,7 @@ def get_color_map():
@frappe.whitelist() @frappe.whitelist()
def update_job_card(job_card, method, **kwargs): def update_job_card(job_card: str, method: str, **kwargs):
if isinstance(kwargs, dict): if isinstance(kwargs, dict):
kwargs = frappe._dict(kwargs) kwargs = frappe._dict(kwargs)
@@ -527,7 +528,6 @@ def update_job_card(job_card, method, **kwargs):
if kwargs.qty and isinstance(kwargs.qty, str): if kwargs.qty and isinstance(kwargs.qty, str):
kwargs.qty = flt(kwargs.qty) kwargs.qty = flt(kwargs.qty)
print(method)
doc = frappe.get_doc("Job Card", job_card) doc = frappe.get_doc("Job Card", job_card)
doc.run_method(method, **kwargs) doc.run_method(method, **kwargs)

View File

@@ -0,0 +1,44 @@
{
"allow_roles": [
{
"role": "Manufacturing Manager"
},
{
"role": "Manufacturing User"
}
],
"creation": "2026-02-20 13:31:02.066000",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 0,
"is_complete": 0,
"modified": "2026-02-23 22:51:27.390568",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Create Raw Materials"
},
{
"step": "Create Finished Goods"
},
{
"step": "Create Operations"
},
{
"step": "Create Bill of Materials"
},
{
"step": "Create Work Order"
},
{
"step": "View Work Order Summary Report"
},
{
"step": "Review Manufacturing Settings"
}
],
"title": "Manufacturing Setup!"
}

View File

@@ -2,6 +2,7 @@
"attach_print": 0, "attach_print": 0,
"channel": "Email", "channel": "Email",
"condition": "doc.status == \"Received\" or doc.status == \"Partially Received\"", "condition": "doc.status == \"Received\" or doc.status == \"Partially Received\"",
"condition_type": "Python",
"creation": "2019-04-29 11:53:23.981418", "creation": "2019-04-29 11:53:23.981418",
"days_in_advance": 0, "days_in_advance": 0,
"docstatus": 0, "docstatus": 0,
@@ -11,16 +12,18 @@
"event": "Value Change", "event": "Value Change",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "<p><b>{{ _(\"Material Request Type\") }}</b>: {{ doc.material_request_type }}<br>\n<b>{{ _(\"Company\") }}</b>: {{ doc.company }}</p>\n\n<h3>{{ _(\"Order Summary\") }}</h3>\n\n<table border=2 >\n <tr align=\"center\">\n <th>{{ _(\"Item Name\") }}</th>\n <th>{{ _(\"Received Quantity\") }}</th>\n </tr>\n {% for item in doc.items %}\n {% if frappe.utils.flt(item.received_qty, 2) > 0.0 %}\n <tr align=\"center\">\n <td>{{ item.item_code }}</td>\n <td>{{ frappe.utils.flt(item.received_qty, 2) }}</td>\n </tr>\n {% endif %}\n {% endfor %}\n</table>\n",
"message_type": "HTML", "message_type": "HTML",
"method": "", "method": "",
"modified": "2023-11-17 08:53:29.525296", "minutes_offset": 0,
"modified": "2026-02-23 17:41:43.982194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Material Request Receipt Notification", "name": "Material Request Receipt Notification",
"owner": "Administrator", "owner": "Administrator",
"recipients": [ "recipients": [
{ {
"receiver_by_document_field": "requested_by" "receiver_by_document_field": "owner"
} }
], ],
"send_system_notification": 0, "send_system_notification": 0,

View File

@@ -0,0 +1,20 @@
{
"action": "Create Entry",
"action_label": "Create Bill of Materials",
"creation": "2026-02-20 13:31:41.740588",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:38:57.652419",
"modified_by": "Administrator",
"name": "Create Bill of Materials",
"owner": "Administrator",
"reference_document": "BOM",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Bill of Materials",
"validate_action": 1
}

Some files were not shown because too many files have changed in this diff Show More