mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-29 19:48:27 +00:00
Merge pull request #52925 from frappe/version-16-hotfix
chore: release v16
This commit is contained in:
@@ -20,6 +20,10 @@
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"confirm_before_resetting_posting_date",
|
||||
"analytics_section",
|
||||
"enable_accounting_dimensions",
|
||||
"column_break_vtnr",
|
||||
"enable_discounts_and_margin",
|
||||
"journals_section",
|
||||
"merge_similar_account_heads",
|
||||
"deferred_accounting_settings_section",
|
||||
@@ -51,12 +55,16 @@
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"payments_tab",
|
||||
"section_break_jpd0",
|
||||
"auto_reconcile_payments",
|
||||
"auto_reconciliation_job_trigger",
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"payment_options_section",
|
||||
"enable_loyalty_point_program",
|
||||
"column_break_ctam",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -281,7 +289,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
@@ -637,6 +645,49 @@
|
||||
"fieldname": "budget_section",
|
||||
"fieldtype": "Section Break",
|
||||
"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,
|
||||
@@ -646,7 +697,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-11 18:30:45.968531",
|
||||
"modified": "2026-02-04 17:15:38.609327",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -12,6 +12,28 @@ from frappe.utils import cint
|
||||
|
||||
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):
|
||||
# begin: auto-generated types
|
||||
@@ -43,9 +65,12 @@ class AccountsSettings(Document):
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_accounting_dimensions: DF.Check
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_discounts_and_margin: DF.Check
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_loyalty_point_program: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
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:
|
||||
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:
|
||||
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.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,
|
||||
)
|
||||
|
||||
@@ -139,6 +139,8 @@ class BankTransaction(Document):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
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"),
|
||||
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(
|
||||
_("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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Sum
|
||||
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.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -732,7 +732,7 @@ class FinancialQueryBuilder:
|
||||
user_conditions = build_match_conditions(doctype)
|
||||
|
||||
if user_conditions:
|
||||
query = query.where(LiteralValue(user_conditions))
|
||||
query = query.where(Bracket(LiteralValue(user_conditions)))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe import _, cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
@@ -33,24 +33,6 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
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):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
if self.is_short_year:
|
||||
@@ -66,28 +48,20 @@ class FiscalYear(Document):
|
||||
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):
|
||||
existing_fiscal_years = frappe.db.sql(
|
||||
"""select name from `tabFiscal Year`
|
||||
where (
|
||||
(%(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)
|
||||
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
|
||||
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
|
||||
) and name!=%(name)s""",
|
||||
{
|
||||
"year_start_date": self.year_start_date,
|
||||
"year_end_date": self.year_end_date,
|
||||
"name": self.name or "No Name",
|
||||
},
|
||||
as_dict=True,
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
name = self.name or self.year
|
||||
|
||||
existing_fiscal_years = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where(
|
||||
(fy.year_start_date <= self.year_end_date)
|
||||
& (fy.year_end_date >= self.year_start_date)
|
||||
& (fy.name != name)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if existing_fiscal_years:
|
||||
@@ -110,37 +84,30 @@ class FiscalYear(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"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.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():
|
||||
for d in frappe.db.sql(
|
||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
||||
):
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
# 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:
|
||||
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_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)
|
||||
end_year = cstr(new_fy.year_end_date.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.insert(ignore_permissions=True)
|
||||
|
||||
@@ -15,20 +15,22 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:44.659251",
|
||||
"modified": "2026-02-20 23:02:26.193606",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
company: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"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
|
||||
},
|
||||
{
|
||||
@@ -198,7 +198,7 @@
|
||||
"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",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Due Date",
|
||||
@@ -294,7 +294,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2026-02-19 17:01:22.642454",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
|
||||
"Fees",
|
||||
"Full and Final Statement",
|
||||
"Payment Entry",
|
||||
"Bank Transaction",
|
||||
]
|
||||
user_remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -512,12 +512,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("contact_email", "");
|
||||
frm.set_value("contact_person", "");
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
|
||||
if (!frm.doc.posting_date) {
|
||||
frappe.msgprint(__("Please select Posting Date before selecting Party"));
|
||||
frm.set_value("party", "");
|
||||
return;
|
||||
}
|
||||
|
||||
erpnext.utils.get_employee_contact_details(frm);
|
||||
|
||||
frm.set_party_account_based_on_party = true;
|
||||
|
||||
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) {
|
||||
if (!r.exc && r.message) {
|
||||
// set taxes table
|
||||
if (r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.add_child("taxes", tax);
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -701,7 +701,6 @@
|
||||
"fetch_from": "company.book_advance_payments_in_separate_party_account",
|
||||
"fieldname": "book_advance_payments_in_separate_party_account",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
@@ -793,7 +792,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-18 13:56:40.206038",
|
||||
"modified": "2026-02-03 16:08:49.800381",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"column_break_4",
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
"payment_reference_section",
|
||||
"payment_reference",
|
||||
"transaction_details",
|
||||
"grand_total",
|
||||
"currency",
|
||||
@@ -157,6 +159,7 @@
|
||||
"label": "Amount",
|
||||
"non_negative": 1,
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval:doc.payment_reference.length>0",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -457,6 +460,17 @@
|
||||
"fieldname": "phone_number",
|
||||
"fieldtype": "Data",
|
||||
"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,
|
||||
@@ -464,7 +478,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:52:48.555415",
|
||||
"modified": "2026-01-13 12:53:00.963274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
||||
@@ -45,6 +45,7 @@ class PaymentRequest(Document):
|
||||
if TYPE_CHECKING:
|
||||
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 (
|
||||
SubscriptionPlanDetail,
|
||||
)
|
||||
@@ -78,6 +79,7 @@ class PaymentRequest(Document):
|
||||
payment_gateway: DF.ReadOnly | None
|
||||
payment_gateway_account: DF.Link | None
|
||||
payment_order: DF.Link | None
|
||||
payment_reference: DF.Table[PaymentReference]
|
||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||
payment_url: DF.Data | None
|
||||
phone_number: DF.Data | None
|
||||
@@ -109,15 +111,36 @@ class PaymentRequest(Document):
|
||||
if self.get("__islocal"):
|
||||
self.status = "Draft"
|
||||
self.validate_reference_document()
|
||||
self.validate_against_payment_reference()
|
||||
self.validate_payment_request_amount()
|
||||
# self.validate_currency()
|
||||
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):
|
||||
if not self.reference_doctype or not self.reference_name:
|
||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||
|
||||
def validate_payment_request_amount(self):
|
||||
if self.payment_reference:
|
||||
return
|
||||
if self.grand_total == 0:
|
||||
frappe.throw(
|
||||
_("{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
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def make_payment_request(**args):
|
||||
"""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)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
|
||||
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:
|
||||
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
|
||||
ref_doc.db_update()
|
||||
grand_total = grand_total - loyalty_amount
|
||||
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
|
||||
|
||||
@@ -584,19 +660,20 @@ def make_payment_request(**args):
|
||||
else:
|
||||
# If PR's are processed, cancel all of them.
|
||||
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)
|
||||
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
if selected_payment_schedules:
|
||||
apply_payment_references(pr, payment_reference)
|
||||
pr.save()
|
||||
|
||||
else:
|
||||
bank_account = (
|
||||
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(
|
||||
{
|
||||
"cost_center": ref_doc.get("cost_center"),
|
||||
@@ -680,6 +760,51 @@ def make_payment_request(**args):
|
||||
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):
|
||||
"""get amount based on doctype"""
|
||||
grand_total = 0
|
||||
@@ -1024,3 +1149,44 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
|
||||
},
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
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.test_payment_entry import create_payment_terms_template
|
||||
@@ -851,3 +853,130 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr.load_from_db()
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -18,8 +18,19 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
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))
|
||||
|
||||
@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):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"tax_id",
|
||||
"company",
|
||||
"column_break_6",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -85,20 +85,24 @@
|
||||
"taxes_and_charges_added",
|
||||
"taxes_and_charges_deducted",
|
||||
"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_in_words",
|
||||
"column_break_hcca",
|
||||
"base_rounding_adjustment",
|
||||
"base_rounded_total",
|
||||
"base_in_words",
|
||||
"column_break8",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"section_break_ttrv",
|
||||
"total_advance",
|
||||
"column_break_peap",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
@@ -883,15 +887,10 @@
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_49",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Grand Total (Company Currency)",
|
||||
"label": "Grand Total",
|
||||
"oldfieldname": "grand_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
@@ -902,7 +901,7 @@
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
"label": "Rounding Adjustment",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
@@ -912,7 +911,7 @@
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounded Total (Company Currency)",
|
||||
"label": "Rounded Total",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
@@ -921,7 +920,7 @@
|
||||
{
|
||||
"fieldname": "base_in_words",
|
||||
"fieldtype": "Data",
|
||||
"label": "In Words (Company Currency)",
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -1626,8 +1625,7 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
"options": "Item Wise Tax Detail"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -1662,6 +1660,28 @@
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -1669,7 +1689,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-05 20:45:16.964500",
|
||||
"modified": "2026-02-23 14:23:57.269770",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -1745,10 +1745,6 @@ class PurchaseInvoice(BuyingController):
|
||||
project_doc.db_update()
|
||||
|
||||
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 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)
|
||||
|
||||
@@ -138,7 +138,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
@@ -77,34 +77,38 @@
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_47",
|
||||
"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_in_words",
|
||||
"column_break_xjag",
|
||||
"base_rounding_adjustment",
|
||||
"base_rounded_total",
|
||||
"base_in_words",
|
||||
"column_break5",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"section_break_vacb",
|
||||
"total_advance",
|
||||
"column_break_rdks",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"section_break_49",
|
||||
"additional_discount_section",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"coupon_code",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"additional_discount_account",
|
||||
"column_break_51",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"additional_discount_account",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"item_wise_tax_details",
|
||||
@@ -194,13 +198,13 @@
|
||||
"column_break8",
|
||||
"unrealized_profit_loss_account",
|
||||
"against_income_account",
|
||||
"sales_team_section_break",
|
||||
"commission_section",
|
||||
"sales_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break10",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break2",
|
||||
"sales_team_section",
|
||||
"sales_team",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
@@ -217,8 +221,7 @@
|
||||
"update_auto_repeat_reference",
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"remarks",
|
||||
"customer_group",
|
||||
"column_break_imbx",
|
||||
"utm_source",
|
||||
@@ -227,8 +230,9 @@
|
||||
"utm_content",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"inter_company_invoice_reference",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -794,7 +798,8 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheets",
|
||||
"options": "Sales Invoice Timesheet",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1073,14 +1078,6 @@
|
||||
"no_copy": 1,
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_49",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Additional Discount"
|
||||
},
|
||||
{
|
||||
"default": "Grand Total",
|
||||
"fieldname": "apply_discount_on",
|
||||
@@ -1125,22 +1122,12 @@
|
||||
"options": "currency",
|
||||
"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",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Grand Total (Company Currency)",
|
||||
"label": "Grand Total (Company Currency",
|
||||
"oldfieldname": "grand_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
@@ -1154,9 +1141,8 @@
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
"label": "Rounding Adjustment",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1166,10 +1152,9 @@
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Rounded Total (Company Currency)",
|
||||
"label": "Rounded Total",
|
||||
"oldfieldname": "rounded_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1179,7 +1164,7 @@
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words (Company Currency)",
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -1272,7 +1257,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "advances",
|
||||
"fieldname": "advances_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1706,10 +1690,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "No",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Is Opening Entry",
|
||||
@@ -1738,18 +1722,6 @@
|
||||
"oldfieldtype": "Text",
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
@@ -1793,16 +1765,6 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"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,
|
||||
"fieldname": "sales_team",
|
||||
@@ -2293,6 +2255,64 @@
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -2306,7 +2326,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-06 20:43:44.732805",
|
||||
"modified": "2026-02-23 14:29:00.301842",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -33,6 +33,7 @@ from erpnext.accounts.utils import (
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import split_asset
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
@@ -480,6 +481,8 @@ class SalesInvoice(SellingController):
|
||||
self.update_stock_reservation_entries()
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.split_asset_based_on_sale_qty()
|
||||
|
||||
self.process_asset_depreciation()
|
||||
|
||||
# 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))
|
||||
|
||||
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):
|
||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||
self.depreciate_asset_on_sale()
|
||||
|
||||
@@ -843,6 +843,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Incoming Rate (Costing)",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1009,7 +1010,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-15 21:08:57.341638",
|
||||
"modified": "2026-02-23 14:37:14.853941",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.auto_created",
|
||||
"condition": "doc.auto_created == 1",
|
||||
"condition_type": "Python",
|
||||
"creation": "2018-04-25 14:19:05.440361",
|
||||
"days_in_advance": 0,
|
||||
"docstatus": 0,
|
||||
@@ -11,8 +12,10 @@
|
||||
"event": "New",
|
||||
"idx": 0,
|
||||
"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",
|
||||
"modified": "2023-11-17 08:54:51.532104",
|
||||
"minutes_offset": 0,
|
||||
"modified": "2026-02-23 17:37:03.755394",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Notification for new fiscal year",
|
||||
@@ -27,5 +30,5 @@
|
||||
],
|
||||
"send_system_notification": 0,
|
||||
"send_to_all_assignees": 0,
|
||||
"subject": "Notification for new fiscal year {{ doc.name }}"
|
||||
"subject": "New Fiscal Year {{ doc.name }} - Review Required"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
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 (
|
||||
add_days,
|
||||
add_months,
|
||||
add_years,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_last_day,
|
||||
get_timestamp,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
@@ -298,19 +296,9 @@ def complete_contact_details(party_details):
|
||||
contact_details = frappe._dict()
|
||||
|
||||
if party_details.party_type == "Employee":
|
||||
contact_details = frappe.db.get_value(
|
||||
"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,
|
||||
)
|
||||
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
|
||||
|
||||
contact_details = get_employee_contact(party_details.party)
|
||||
contact_details.update({"contact_person": None, "contact_phone": None})
|
||||
elif party_details.contact_person:
|
||||
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
@@ -102,7 +102,7 @@ def execute(filters=None):
|
||||
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(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
@@ -231,18 +231,19 @@ def get_report_summary(
|
||||
], (net_asset - net_liability + net_equity)
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
|
||||
labels = [col.get("label") for col in chart_columns]
|
||||
|
||||
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:
|
||||
asset_data.append(asset[-2].get(p.get("fieldname")))
|
||||
asset_data.append(asset[-2].get(key))
|
||||
if liability:
|
||||
liability_data.append(liability[-2].get(p.get("fieldname")))
|
||||
liability_data.append(liability[-2].get(key))
|
||||
if equity:
|
||||
equity_data.append(equity[-2].get(p.get("fieldname")))
|
||||
equity_data.append(equity[-2].get(key))
|
||||
|
||||
datasets = []
|
||||
if asset_data:
|
||||
|
||||
@@ -145,7 +145,7 @@ def execute(filters=None):
|
||||
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)
|
||||
|
||||
@@ -417,12 +417,12 @@ def get_report_summary(summary_data, currency):
|
||||
return report_summary
|
||||
|
||||
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
def get_chart_data(period_list, data, currency):
|
||||
labels = [period.get("label") for period in period_list]
|
||||
datasets = [
|
||||
{
|
||||
"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
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
|
||||
@@ -48,22 +48,25 @@ def execute(filters=None):
|
||||
return columns, data, message, chart
|
||||
|
||||
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
|
||||
companies_column, companies = get_companies(filters)
|
||||
columns = get_columns(companies_column, filters)
|
||||
company_list, companies = get_companies(filters)
|
||||
company_columns = get_company_columns(company_list, filters)
|
||||
columns = get_columns(company_columns)
|
||||
|
||||
if filters.get("report") == "Balance Sheet":
|
||||
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":
|
||||
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:
|
||||
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
|
||||
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)
|
||||
company_currency = get_company_currency(filters)
|
||||
|
||||
@@ -174,7 +177,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
if 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(
|
||||
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
|
||||
|
||||
|
||||
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 = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
@@ -298,23 +324,7 @@ def get_columns(companies, filters):
|
||||
},
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
columns.append(
|
||||
{
|
||||
"fieldname": company,
|
||||
"label": f"{company} ({currency})",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
"apply_currency_formatter": apply_currency_formatter,
|
||||
"company_name": company,
|
||||
}
|
||||
)
|
||||
columns.extend(company_columns)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import getdate, nowdate
|
||||
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 (
|
||||
get_accounting_dimensions,
|
||||
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
match_conditions = build_match_conditions(party_type)
|
||||
|
||||
if match_conditions:
|
||||
query = query.where(LiteralValue(match_conditions))
|
||||
if match_conditions := build_match_conditions(party_type):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
party_details = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import frappe
|
||||
from frappe import _
|
||||
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 pypika.terms import ExistsCriterion
|
||||
from pypika.terms import Bracket, ExistsCriterion, LiteralValue
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
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)
|
||||
query = query.where(ExistsCriterion(account_filter_query))
|
||||
|
||||
if group_by_account:
|
||||
query = query.groupby("account")
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
if group_by_account:
|
||||
query += " GROUP BY `account`"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||
|
||||
@@ -324,10 +324,8 @@ def get_conditions(filters):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
match_conditions = build_match_conditions("GL Entry")
|
||||
|
||||
if match_conditions:
|
||||
conditions.append(match_conditions)
|
||||
if match_conditions := build_match_conditions("GL Entry"):
|
||||
conditions.append(f"({match_conditions})")
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
|
||||
@@ -444,6 +444,7 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv.items[0].allow_zero_valuation_rate = 1
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
import erpnext
|
||||
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
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
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():
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
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.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):
|
||||
invoice = f"`tab{doctype}`"
|
||||
invoice_item = f"`tab{doctype} Item`"
|
||||
invoice = frappe.qb.DocType(doctype)
|
||||
invoice_item = frappe.qb.DocType(f"{doctype} Item")
|
||||
|
||||
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":
|
||||
query += f" order by {invoice_item}.parent desc"
|
||||
query = query.orderby(invoice_item.parent, order=Order.desc)
|
||||
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":
|
||||
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"):
|
||||
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
|
||||
|
||||
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
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):
|
||||
|
||||
@@ -68,7 +68,7 @@ def execute(filters=None):
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"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(
|
||||
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
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
|
||||
labels = [col.get("label") for col in chart_columns]
|
||||
|
||||
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:
|
||||
income_data.append(income[-2].get(p.get("fieldname")))
|
||||
income_data.append(income[-2].get(key))
|
||||
if expense:
|
||||
expense_data.append(expense[-2].get(p.get("fieldname")))
|
||||
expense_data.append(expense[-2].get(key))
|
||||
if net_profit_loss:
|
||||
net_profit.append(net_profit_loss.get(p.get("fieldname")))
|
||||
net_profit.append(net_profit_loss.get(key))
|
||||
|
||||
datasets = []
|
||||
if income_data:
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
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.report.utils import (
|
||||
@@ -422,15 +422,13 @@ def get_invoices(filters, additional_query_columns):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
if match_conditions := build_match_conditions("Purchase Invoice"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
query = query.orderby("posting_date", order=Order.desc)
|
||||
query = query.orderby("name", order=Order.desc)
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -7,7 +7,7 @@ from frappe import _, msgprint
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
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.report.utils import (
|
||||
@@ -458,15 +458,13 @@ def get_invoices(filters, additional_query_columns):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
if match_conditions := build_match_conditions("Sales Invoice"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
query = query.orderby("posting_date", order=Order.desc)
|
||||
query = query.orderby("name", order=Order.desc)
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -500,7 +500,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
|
||||
dimensions_dict = frappe._dict()
|
||||
if entry and 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
|
||||
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.add_custom_button(
|
||||
__("Sell Asset"),
|
||||
function () {
|
||||
frm.trigger("make_sales_invoice");
|
||||
frm.trigger("sell_asset");
|
||||
},
|
||||
__("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) {
|
||||
frappe.call({
|
||||
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) {
|
||||
const title = __("Split Asset");
|
||||
|
||||
|
||||
@@ -484,6 +484,9 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||
|
||||
def validate_linked_purchase_documents(self):
|
||||
if self.flags.is_split_asset:
|
||||
return
|
||||
|
||||
for fieldname, doctype in [
|
||||
("purchase_receipt", "Purchase Receipt"),
|
||||
("purchase_invoice", "Purchase Invoice"),
|
||||
@@ -1085,7 +1088,7 @@ def get_asset_naming_series():
|
||||
|
||||
|
||||
@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)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
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,
|
||||
"serial_no": serial_no,
|
||||
"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)
|
||||
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.flags.is_split_asset = True
|
||||
|
||||
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)
|
||||
|
||||
@@ -330,7 +330,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
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.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
@@ -458,7 +460,9 @@ class TestAsset(AssetSetup):
|
||||
|
||||
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.due_date = nowdate()
|
||||
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.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):
|
||||
def test_schedule_for_straight_line_method(self):
|
||||
|
||||
@@ -51,7 +51,9 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
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.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
20
erpnext/assets/onboarding_step/learn_asset/learn_asset.json
Normal file
20
erpnext/assets/onboarding_step/learn_asset/learn_asset.json
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -428,7 +428,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("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() {
|
||||
set_schedule_date(this.frm);
|
||||
}
|
||||
|
||||
@@ -9,23 +9,20 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier_section",
|
||||
"company",
|
||||
"title",
|
||||
"naming_series",
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"order_confirmation_no",
|
||||
"order_confirmation_date",
|
||||
"get_items_from_open_material_requests",
|
||||
"mps",
|
||||
"column_break_7",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"column_break1",
|
||||
"supplier",
|
||||
"company",
|
||||
"is_subcontracted",
|
||||
"supplier_name",
|
||||
"has_unit_price_items",
|
||||
"supplier_warehouse",
|
||||
"amended_from",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -79,16 +76,19 @@
|
||||
"taxes_and_charges_deducted",
|
||||
"total_taxes_and_charges",
|
||||
"totals_section",
|
||||
"base_grand_total",
|
||||
"base_rounding_adjustment",
|
||||
"base_in_words",
|
||||
"base_rounded_total",
|
||||
"column_break4",
|
||||
"grand_total",
|
||||
"in_words",
|
||||
"column_break4",
|
||||
"disable_rounded_total",
|
||||
"rounding_adjustment",
|
||||
"rounded_total",
|
||||
"disable_rounded_total",
|
||||
"in_words",
|
||||
"base_totals_section",
|
||||
"base_grand_total",
|
||||
"base_in_words",
|
||||
"column_break_jkoz",
|
||||
"base_rounding_adjustment",
|
||||
"base_rounded_total",
|
||||
"section_break_tnkm",
|
||||
"advance_paid",
|
||||
"discount_section",
|
||||
"apply_discount_on",
|
||||
@@ -154,11 +154,13 @@
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"additional_info_section",
|
||||
"is_internal_supplier",
|
||||
"party_account_currency",
|
||||
"represents_company",
|
||||
"ref_sq",
|
||||
"amended_from",
|
||||
"column_break_74",
|
||||
"party_account_currency",
|
||||
"mps",
|
||||
"is_internal_supplier",
|
||||
"inter_company_order_reference",
|
||||
"is_old_subcontracting_flow",
|
||||
"connections_tab"
|
||||
@@ -206,13 +208,6 @@
|
||||
"reqd": 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,
|
||||
"fetch_from": "supplier.supplier_name",
|
||||
@@ -773,7 +768,7 @@
|
||||
{
|
||||
"fieldname": "base_grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Grand Total (Company Currency)",
|
||||
"label": "Grand Total",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "grand_total",
|
||||
"oldfieldtype": "Currency",
|
||||
@@ -785,7 +780,7 @@
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
"label": "Rounding Adjustment",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
@@ -794,7 +789,7 @@
|
||||
{
|
||||
"fieldname": "base_in_words",
|
||||
"fieldtype": "Data",
|
||||
"label": "In Words (Company Currency)",
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -804,7 +799,7 @@
|
||||
{
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounded Total (Company Currency)",
|
||||
"label": "Rounded Total",
|
||||
"oldfieldname": "rounded_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
@@ -862,7 +857,7 @@
|
||||
{
|
||||
"fieldname": "advance_paid",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Advance Paid",
|
||||
"label": "Advance Paid (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"options": "party_account_currency",
|
||||
"print_hide": 1,
|
||||
@@ -1301,8 +1296,21 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
"options": "Item Wise Tax Detail"
|
||||
},
|
||||
{
|
||||
"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,
|
||||
@@ -1310,7 +1318,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-03 14:44:55.192192",
|
||||
"modified": "2026-02-23 14:22:33.323946",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -191,6 +191,9 @@ class PurchaseOrder(BuyingController):
|
||||
self.set_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):
|
||||
super().validate()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
if (frappe.defaults.get_default("supp_master_name") != "Naming Series") {
|
||||
frm.toggle_display("naming_series", false);
|
||||
@@ -111,14 +117,6 @@ frappe.ui.form.on("Supplier", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Get Supplier Group Details"),
|
||||
function () {
|
||||
frm.trigger("get_supplier_group_details");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
if (
|
||||
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||
frappe.model.can_create("Party Link")
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"supplier_type",
|
||||
"supplier_name",
|
||||
"country",
|
||||
"gender",
|
||||
"column_break0",
|
||||
"supplier_group",
|
||||
"supplier_type",
|
||||
"country",
|
||||
"is_transporter",
|
||||
"image",
|
||||
"defaults_section",
|
||||
@@ -23,24 +24,12 @@
|
||||
"default_bank_account",
|
||||
"column_break_10",
|
||||
"default_price_list",
|
||||
"internal_supplier_section",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"column_break_16",
|
||||
"companies",
|
||||
"column_break2",
|
||||
"supplier_details",
|
||||
"column_break_30",
|
||||
"website",
|
||||
"language",
|
||||
"customer_numbers",
|
||||
"dashboard_tab",
|
||||
"tax_tab",
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"column_break_27",
|
||||
"tax_withholding_category",
|
||||
"tax_withholding_group",
|
||||
"contact_and_address_tab",
|
||||
"address_contacts",
|
||||
"address_html",
|
||||
@@ -54,19 +43,32 @@
|
||||
"supplier_primary_contact",
|
||||
"mobile_no",
|
||||
"email_id",
|
||||
"tax_tab",
|
||||
"tax_id",
|
||||
"tax_category",
|
||||
"column_break_27",
|
||||
"tax_withholding_category",
|
||||
"tax_withholding_group",
|
||||
"accounting_tab",
|
||||
"payment_terms",
|
||||
"default_accounts_section",
|
||||
"accounts",
|
||||
"internal_supplier_section",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"column_break_16",
|
||||
"companies",
|
||||
"settings_tab",
|
||||
"allow_purchase_invoice_creation_without_purchase_order",
|
||||
"allow_purchase_invoice_creation_without_purchase_receipt",
|
||||
"column_break_54",
|
||||
"is_frozen",
|
||||
"disabled",
|
||||
"rfq_and_purchase_order_settings_section",
|
||||
"is_frozen",
|
||||
"warn_rfqs",
|
||||
"warn_pos",
|
||||
"prevent_rfqs",
|
||||
"column_break_oxjw",
|
||||
"warn_pos",
|
||||
"prevent_pos",
|
||||
"block_supplier_section",
|
||||
"on_hold",
|
||||
@@ -75,7 +77,7 @@
|
||||
"release_date",
|
||||
"portal_users_tab",
|
||||
"portal_users",
|
||||
"column_break_1mqv"
|
||||
"dashboard_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -398,7 +400,7 @@
|
||||
{
|
||||
"fieldname": "dashboard_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
@@ -430,7 +432,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "internal_supplier_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Internal Supplier"
|
||||
"label": "Internal Supplier Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
@@ -469,10 +471,6 @@
|
||||
"label": "Supplier Portal Users",
|
||||
"options": "Portal User"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1mqv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_mglr",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -488,6 +486,22 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "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,
|
||||
@@ -501,7 +515,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-06 12:58:01.398824",
|
||||
"modified": "2026-02-10 21:28:01.101808",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
||||
@@ -49,6 +49,7 @@ class Supplier(TransactionBase):
|
||||
default_price_list: DF.Link | None
|
||||
disabled: DF.Check
|
||||
email_id: DF.ReadOnly | None
|
||||
gender: DF.Link | None
|
||||
hold_type: DF.Literal["", "All", "Invoices", "Payments"]
|
||||
image: DF.AttachImage | None
|
||||
is_frozen: DF.Check
|
||||
@@ -161,8 +162,6 @@ class Supplier(TransactionBase):
|
||||
if doc.payment_terms:
|
||||
self.payment_terms = doc.payment_terms
|
||||
|
||||
self.save()
|
||||
|
||||
def validate_internal_supplier(self):
|
||||
if not self.is_internal_supplier:
|
||||
self.represents_company = ""
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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! "
|
||||
}
|
||||
20
erpnext/buying/onboarding_step/create_item/create_item.json
Normal file
20
erpnext/buying/onboarding_step/create_item/create_item.json
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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.insert()
|
||||
else:
|
||||
child_item.save()
|
||||
child_item.save(ignore_permissions=True)
|
||||
|
||||
parent.reload()
|
||||
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
|
||||
|
||||
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")
|
||||
else:
|
||||
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 = {
|
||||
"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"),
|
||||
"Delivery Note": ("company_address", "company_address_display"),
|
||||
"POS Invoice": ("company_address", "company_address_display"),
|
||||
|
||||
@@ -498,10 +498,34 @@ class SellingController(StockController):
|
||||
sales_order.update_reserved_qty(so_item_rows)
|
||||
|
||||
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"):
|
||||
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(
|
||||
"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"
|
||||
)
|
||||
|
||||
is_standalone = self.is_return and not self.return_against
|
||||
|
||||
old_doc = self.get_doc_before_save()
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
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"))
|
||||
|
||||
if old_doc:
|
||||
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.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
|
||||
reset_incoming_rate()
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
@@ -583,11 +589,12 @@ class SellingController(StockController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.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,
|
||||
"serial_no": d.serial_no,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
raise_error_if_no_rate=is_standalone,
|
||||
fallbacks=not is_standalone,
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -119,7 +119,7 @@ status_map = {
|
||||
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
||||
[
|
||||
"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",
|
||||
@@ -511,13 +511,6 @@ class StatusUpdater(Document):
|
||||
if d.doctype != args["source_dt"]:
|
||||
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)
|
||||
|
||||
# updates qty in the child table
|
||||
|
||||
@@ -1395,6 +1395,68 @@ def make_rm_stock_entry(
|
||||
if target_doc and target_doc.get("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(
|
||||
order_doctype,
|
||||
subcontract_order.name,
|
||||
@@ -1415,67 +1477,9 @@ def make_rm_stock_entry(
|
||||
},
|
||||
target_doc,
|
||||
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:
|
||||
return stock_entry
|
||||
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(
|
||||
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):
|
||||
target_doc.purpose = "Material Transfer"
|
||||
|
||||
@@ -1517,6 +1523,21 @@ def make_return_stock_entry_for_subcontract(
|
||||
|
||||
target_doc.company = source_doc.company
|
||||
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(
|
||||
order_doctype,
|
||||
@@ -1531,27 +1552,6 @@ def make_return_stock_entry_for_subcontract(
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -672,6 +672,11 @@ class calculate_taxes_and_totals:
|
||||
else:
|
||||
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):
|
||||
grand_total_diff = self.grand_total_diff
|
||||
|
||||
|
||||
@@ -59,3 +59,41 @@ class TestTaxesAndTotals(AccountsTestMixin, IntegrationTestCase):
|
||||
self.assertEqual(so.total, 1500)
|
||||
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
|
||||
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)
|
||||
|
||||
@@ -270,7 +270,7 @@ standard_portal_menu_items = [
|
||||
"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",
|
||||
"route": "/timesheets",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"production_item_tab",
|
||||
"item",
|
||||
"company",
|
||||
"uom",
|
||||
"item",
|
||||
"quantity",
|
||||
"cb0",
|
||||
"is_active",
|
||||
@@ -17,8 +16,6 @@
|
||||
"allow_alternative_item",
|
||||
"set_rate_of_sub_assembly_item_based_on_bom",
|
||||
"is_phantom_bom",
|
||||
"project",
|
||||
"image",
|
||||
"currency_detail",
|
||||
"rm_cost_as_per",
|
||||
"buying_price_list",
|
||||
@@ -33,11 +30,9 @@
|
||||
"column_break_23",
|
||||
"transfer_material_against",
|
||||
"routing",
|
||||
"fg_based_operating_cost",
|
||||
"column_break_joxb",
|
||||
"default_source_warehouse",
|
||||
"default_target_warehouse",
|
||||
"fg_based_section_section",
|
||||
"fg_based_operating_cost",
|
||||
"column_break_omye",
|
||||
"operating_cost_per_bom_quantity",
|
||||
"operations_section",
|
||||
"operations",
|
||||
@@ -61,15 +56,25 @@
|
||||
"column_break_26",
|
||||
"total_cost",
|
||||
"base_total_cost",
|
||||
"more_info_tab",
|
||||
"item_name",
|
||||
"column_break_27",
|
||||
"description",
|
||||
"has_variants",
|
||||
"quality_inspection_tab",
|
||||
"quality_inspection_section_break",
|
||||
"inspection_required",
|
||||
"column_break_dxp7",
|
||||
"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",
|
||||
"exploded_items",
|
||||
"website_section",
|
||||
@@ -451,7 +456,8 @@
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "route",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Route"
|
||||
"label": "Route",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -651,15 +657,11 @@
|
||||
{
|
||||
"default": "0",
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Track Semi Finished Goods"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_joxb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
@@ -677,6 +679,33 @@
|
||||
"fieldname": "is_phantom_bom",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
@@ -684,7 +713,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-19 16:17:15.925156",
|
||||
"modified": "2026-02-06 17:23:15.255301",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -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) {
|
||||
if (frm.doc.__islocal) return;
|
||||
var section = "";
|
||||
@@ -791,10 +781,6 @@ frappe.ui.form.on("Job Card Time Log", {
|
||||
frm.events.set_total_completed_qty(frm);
|
||||
},
|
||||
|
||||
to_time: function (frm) {
|
||||
frm.set_value("started_time", "");
|
||||
},
|
||||
|
||||
time_in_mins(frm, cdt, cdn) {
|
||||
let d = locals[cdt][cdn];
|
||||
if (d.time_in_mins) {
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"naming_series",
|
||||
"work_order",
|
||||
"employee",
|
||||
"is_subcontracted",
|
||||
"column_break_4",
|
||||
"posting_date",
|
||||
"company",
|
||||
"project",
|
||||
"bom_no",
|
||||
"is_subcontracted",
|
||||
"semi_finished_good__finished_good_section",
|
||||
"finished_good",
|
||||
"production_item",
|
||||
@@ -70,22 +70,24 @@
|
||||
"more_information",
|
||||
"item_name",
|
||||
"requested_qty",
|
||||
"status",
|
||||
"operation_row_id",
|
||||
"is_paused",
|
||||
"track_semi_finished_goods",
|
||||
"column_break_20",
|
||||
"remarks",
|
||||
"section_break_dfoc",
|
||||
"status",
|
||||
"operation_row_id",
|
||||
"amended_from",
|
||||
"column_break_xhzg",
|
||||
"operation_row_number",
|
||||
"operation_id",
|
||||
"sequence_id",
|
||||
"remarks",
|
||||
"serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"section_break_jcmx",
|
||||
"serial_no",
|
||||
"serial_and_batch_bundle",
|
||||
"column_break_swqr",
|
||||
"barcode",
|
||||
"started_time",
|
||||
"current_time",
|
||||
"amended_from",
|
||||
"batch_no",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -210,7 +212,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_information",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Information"
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_id",
|
||||
@@ -259,16 +261,6 @@
|
||||
"options": "Open\nWork In Progress\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted",
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
@@ -315,16 +307,7 @@
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Item Name"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "current_time",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Current Time",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -466,7 +449,8 @@
|
||||
"label": "Serial and Batch Bundle",
|
||||
"no_copy": 1,
|
||||
"options": "Serial and Batch Bundle",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "process_loss_qty",
|
||||
@@ -621,12 +605,30 @@
|
||||
"fieldname": "track_semi_finished_goods",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-22 14:20:07.817118",
|
||||
"modified": "2026-02-06 18:27:03.178783",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -82,7 +82,6 @@ class JobCard(Document):
|
||||
batch_no: DF.Link | None
|
||||
bom_no: DF.Link | None
|
||||
company: DF.Link
|
||||
current_time: DF.Int
|
||||
employee: DF.TableMultiSelect[JobCardTimeLog]
|
||||
expected_end_date: DF.Datetime | None
|
||||
expected_start_date: DF.Datetime | None
|
||||
@@ -118,7 +117,6 @@ class JobCard(Document):
|
||||
serial_no: DF.SmallText | None
|
||||
skip_material_transfer: DF.Check
|
||||
source_warehouse: DF.Link | None
|
||||
started_time: DF.Datetime | None
|
||||
status: DF.Literal[
|
||||
"Open",
|
||||
"Work In Progress",
|
||||
|
||||
@@ -56,7 +56,6 @@ class TestRouting(IntegrationTestCase):
|
||||
self.assertEqual(job_card_doc.total_completed_qty, 10)
|
||||
|
||||
wo_doc.cancel()
|
||||
wo_doc.delete()
|
||||
|
||||
def test_update_bom_operation_time(self):
|
||||
"""Update cost shouldn't update routing times."""
|
||||
|
||||
@@ -598,6 +598,33 @@ class TestWorkOrder(IntegrationTestCase):
|
||||
work_order1.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):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
|
||||
|
||||
@@ -8,29 +8,23 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item",
|
||||
"naming_series",
|
||||
"status",
|
||||
"production_item",
|
||||
"item_name",
|
||||
"image",
|
||||
"bom_no",
|
||||
"mps",
|
||||
"subcontracting_inward_order",
|
||||
"subcontracting_inward_order_item",
|
||||
"sales_order",
|
||||
"column_break1",
|
||||
"company",
|
||||
"naming_series",
|
||||
"production_item",
|
||||
"bom_no",
|
||||
"column_break1",
|
||||
"qty",
|
||||
"project",
|
||||
"sales_order",
|
||||
"track_semi_finished_goods",
|
||||
"reserve_stock",
|
||||
"column_break_agjv",
|
||||
"section_break_vrpa",
|
||||
"max_producible_qty",
|
||||
"material_transferred_for_manufacturing",
|
||||
"additional_transferred_qty",
|
||||
"column_break_ezmq",
|
||||
"produced_qty",
|
||||
"disassembled_qty",
|
||||
"process_loss_qty",
|
||||
"disassembled_qty",
|
||||
"warehouses",
|
||||
"source_warehouse",
|
||||
"wip_warehouse",
|
||||
@@ -65,22 +59,34 @@
|
||||
"column_break_24",
|
||||
"corrective_operation_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",
|
||||
"has_serial_no",
|
||||
"has_batch_no",
|
||||
"column_break_18",
|
||||
"batch_size",
|
||||
"more_info",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"column_break2",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"sales_order_item",
|
||||
"reference_section",
|
||||
"project",
|
||||
"subcontracting_inward_order",
|
||||
"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_item",
|
||||
"product_bundle_item",
|
||||
"section_break_ynih",
|
||||
"status",
|
||||
"column_break_cvuw",
|
||||
"amended_from",
|
||||
"connections_tab"
|
||||
],
|
||||
@@ -149,6 +155,7 @@
|
||||
{
|
||||
"fieldname": "bom_no",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "BOM No",
|
||||
"oldfieldname": "bom_no",
|
||||
"oldfieldtype": "Link",
|
||||
@@ -198,6 +205,7 @@
|
||||
"default": "1.0",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty To Manufacture",
|
||||
"oldfieldname": "qty",
|
||||
"oldfieldtype": "Currency",
|
||||
@@ -431,7 +439,8 @@
|
||||
"fieldname": "material_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Material Request",
|
||||
"options": "Material Request"
|
||||
"options": "Material Request",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "material_request_item",
|
||||
@@ -516,7 +525,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Finished Good Serial / Batch"
|
||||
@@ -624,10 +633,8 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_agjv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.docstatus==1 && doc.skip_transfer==0",
|
||||
"fieldname": "additional_transferred_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Additional Transferred Qty",
|
||||
@@ -659,6 +666,36 @@
|
||||
"no_copy": 1,
|
||||
"non_negative": 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,
|
||||
@@ -667,7 +704,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-12 14:24:57.699749",
|
||||
"modified": "2026-02-06 17:53:11.295600",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -557,7 +557,7 @@ class WorkOrder(Document):
|
||||
if status != self.status:
|
||||
self.db_set("status", status)
|
||||
|
||||
self.update_required_items()
|
||||
self.update_required_items()
|
||||
|
||||
return status or self.status
|
||||
|
||||
@@ -780,7 +780,6 @@ class WorkOrder(Document):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
self.on_close_or_cancel()
|
||||
self.delete_job_card()
|
||||
|
||||
def on_close_or_cancel(self):
|
||||
if self.production_plan and frappe.db.exists(
|
||||
@@ -794,7 +793,6 @@ class WorkOrder(Document):
|
||||
self.update_planned_qty()
|
||||
self.update_ordered_qty()
|
||||
self.update_reserved_qty_for_production()
|
||||
self.delete_auto_created_batch_and_serial_no()
|
||||
|
||||
if self.reserve_stock:
|
||||
self.update_stock_reservation()
|
||||
@@ -869,6 +867,9 @@ class WorkOrder(Document):
|
||||
).run()
|
||||
|
||||
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):
|
||||
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):
|
||||
item_details = frappe.get_cached_value(
|
||||
"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:
|
||||
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):
|
||||
if frappe.get_cached_value("Item", self.production_item, "has_variants"):
|
||||
frappe.throw(_("Work Order cannot be raised against a Item Template"), ItemHasVariantError)
|
||||
@@ -1540,6 +1530,7 @@ class WorkOrder(Document):
|
||||
"operation": item.operation or operation,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item.item_name,
|
||||
"stock_uom": item.stock_uom,
|
||||
"description": item.description,
|
||||
"allow_alternative_item": item.allow_alternative_item,
|
||||
"required_qty": item.qty,
|
||||
@@ -1577,7 +1568,7 @@ class WorkOrder(Document):
|
||||
.select(
|
||||
ste_child.item_code,
|
||||
ste_child.original_item,
|
||||
fn.Sum(ste_child.qty).as_("qty"),
|
||||
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(ste.docstatus == 1)
|
||||
@@ -1650,7 +1641,7 @@ class WorkOrder(Document):
|
||||
.select(
|
||||
ste_child.item_code,
|
||||
ste_child.original_item,
|
||||
fn.Sum(ste_child.qty).as_("qty"),
|
||||
fn.Sum(ste_child.transfer_qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(ste.docstatus == 1)
|
||||
@@ -2160,7 +2151,7 @@ def get_consumed_qty(work_order, item_code):
|
||||
frappe.qb.from_(stock_entry)
|
||||
.inner_join(stock_entry_detail)
|
||||
.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(
|
||||
(stock_entry.work_order == work_order)
|
||||
& (stock_entry.purpose.isin(["Manufacture", "Material Consumption for Manufacture"]))
|
||||
@@ -2351,7 +2342,11 @@ def set_work_order_ops(name):
|
||||
|
||||
@frappe.whitelist()
|
||||
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)
|
||||
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))
|
||||
)
|
||||
|
||||
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":
|
||||
stock_entry.to_warehouse = wip_warehouse
|
||||
stock_entry.project = work_order.project
|
||||
@@ -2387,6 +2379,10 @@ def make_stock_entry(
|
||||
)
|
||||
stock_entry.to_warehouse = work_order.fg_warehouse
|
||||
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":
|
||||
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.on_close_or_cancel()
|
||||
frappe.msgprint(_("Work Order has been {0}").format(status))
|
||||
work_order.notify_update()
|
||||
return work_order.status
|
||||
@@ -2677,6 +2673,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.purpose = "Material Transfer for Manufacture"
|
||||
doc.for_qty = for_qty
|
||||
|
||||
doc.set_item_locations()
|
||||
|
||||
@@ -233,7 +233,7 @@ class Workstation(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_job_cards(workstation, job_card=None):
|
||||
def get_job_cards(workstation: str):
|
||||
if frappe.has_permission("Job Card", "read"):
|
||||
jc_data = frappe.get_all(
|
||||
"Job Card",
|
||||
@@ -264,6 +264,7 @@ def get_job_cards(workstation, job_card=None):
|
||||
"status": ["not in", ["Completed", "Stopped"]],
|
||||
},
|
||||
order_by="expected_start_date, expected_end_date",
|
||||
limit=10,
|
||||
)
|
||||
|
||||
job_cards = [row.name for row in jc_data]
|
||||
@@ -517,7 +518,7 @@ def get_color_map():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_job_card(job_card, method, **kwargs):
|
||||
def update_job_card(job_card: str, method: str, **kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
@@ -527,7 +528,6 @@ def update_job_card(job_card, method, **kwargs):
|
||||
if kwargs.qty and isinstance(kwargs.qty, str):
|
||||
kwargs.qty = flt(kwargs.qty)
|
||||
|
||||
print(method)
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.run_method(method, **kwargs)
|
||||
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.status == \"Received\" or doc.status == \"Partially Received\"",
|
||||
"condition_type": "Python",
|
||||
"creation": "2019-04-29 11:53:23.981418",
|
||||
"days_in_advance": 0,
|
||||
"docstatus": 0,
|
||||
@@ -11,16 +12,18 @@
|
||||
"event": "Value Change",
|
||||
"idx": 0,
|
||||
"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",
|
||||
"method": "",
|
||||
"modified": "2023-11-17 08:53:29.525296",
|
||||
"minutes_offset": 0,
|
||||
"modified": "2026-02-23 17:41:43.982194",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Material Request Receipt Notification",
|
||||
"owner": "Administrator",
|
||||
"recipients": [
|
||||
{
|
||||
"receiver_by_document_field": "requested_by"
|
||||
"receiver_by_document_field": "owner"
|
||||
}
|
||||
],
|
||||
"send_system_notification": 0,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user