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

chore: release v16
This commit is contained in:
ruthra kumar
2026-01-28 09:44:30 +05:30
committed by GitHub
63 changed files with 3756 additions and 2886 deletions

View File

@@ -0,0 +1,50 @@
{
"cards": [
{
"card": "Total Outgoing Bills"
},
{
"card": "Total Incoming Bills"
},
{
"card": "Total Incoming Payment"
},
{
"card": "Total Outgoing Payment"
}
],
"charts": [
{
"chart": "Incoming Bills (Purchase Invoice)",
"width": "Half"
},
{
"chart": "Outgoing Bills (Sales Invoice)",
"width": "Half"
},
{
"chart": "Accounts Receivable Ageing",
"width": "Half"
},
{
"chart": "Accounts Payable Ageing",
"width": "Half"
},
{
"chart": "Bank Balance",
"width": "Full"
}
],
"creation": "2026-01-26 21:25:12.793893",
"dashboard_name": "Payments",
"docstatus": 0,
"doctype": "Dashboard",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"modified": "2026-01-26 21:25:12.793893",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payments",
"owner": "Administrator"
}

View File

@@ -3,9 +3,6 @@
frappe.provide("erpnext.integrations"); frappe.provide("erpnext.integrations");
frappe.ui.form.on("Bank", { frappe.ui.form.on("Bank", {
onload: function (frm) {
add_fields_to_mapping_table(frm);
},
refresh: function (frm) { refresh: function (frm) {
add_fields_to_mapping_table(frm); add_fields_to_mapping_table(frm);
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
}); });
}); });
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property( const grid = frm.fields_dict.bank_transaction_mapping?.grid;
"bank_transaction_field",
"options", if (grid) {
options grid.update_docfield_property("bank_transaction_field", "options", options);
); }
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
"There was an issue connecting to Plaid's authentication server. Check browser console for more information" "There was an issue connecting to Plaid's authentication server. Check browser console for more information"
) )
); );
console.log(error); console.error(error);
} }
plaid_success(token, response) { plaid_success(token, response) {

View File

@@ -179,7 +179,7 @@ class JournalEntry(AccountsController):
validate_docs_for_deferred_accounting([self.name], []) validate_docs_for_deferred_accounting([self.name], [])
def submit(self): def submit(self):
if len(self.accounts) > 100: if len(self.accounts) > 100 and not self.meta.queue_in_background:
queue_submission(self, "_submit") queue_submission(self, "_submit")
else: else:
return self._submit() return self._submit()

View File

@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
); );
frm.refresh_fields(); frm.refresh_fields();
const party_currency =
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
var reference_grid = frm.fields_dict["references"].grid;
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
reference_grid.update_docfield_property(fieldname, "options", party_currency);
});
reference_grid.refresh();
}, },
show_general_ledger: function (frm) { show_general_ledger: function (frm) {
@@ -1104,7 +1114,7 @@ frappe.ui.form.on("Payment Entry", {
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", { await frm.call("allocate_amount_to_references", {
paid_amount: paid_amount, paid_amount: flt(paid_amount),
paid_amount_change: paid_amount_change, paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
}); });

View File

@@ -132,6 +132,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}, },
{ {
"fieldname": "due_date", "fieldname": "due_date",

View File

@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
amount_in_account_currency: DF.Currency amount_in_account_currency: DF.Currency
company: DF.Link | None company: DF.Link | None
cost_center: DF.Link | None cost_center: DF.Link | None
project: DF.Link | None
delinked: DF.Check delinked: DF.Check
due_date: DF.Date | None due_date: DF.Date | None
finance_book: DF.Link | None finance_book: DF.Link | None

View File

@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions: for x in self.dimensions:
dimension = x.fieldname dimension = x.fieldname
if self.get(dimension): if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):

View File

@@ -116,11 +116,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
if (cint(doc.update_stock) != 1) { if (cint(doc.update_stock) != 1) {
if (!is_delivered_by_supplier) { if (!is_delivered_by_supplier) {
this.frm.add_custom_button( const should_create_delivery_note = doc.items.some(
__("Delivery Note"), (item) =>
this.frm.cscript["Make Delivery Note"], item.qty - item.delivered_qty > 0 &&
__("Create") !item.scio_detail &&
!item.dn_detail &&
!item.delivered_by_supplier
); );
if (should_create_delivery_note) {
this.frm.add_custom_button(
__("Delivery Note"),
this.frm.cscript["Make Delivery Note"],
__("Create")
);
}
} }
} }

View File

@@ -2424,7 +2424,8 @@ def make_delivery_note(source_name, target_doc=None):
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1 "condition": lambda doc: doc.delivered_by_supplier != 1
and not doc.scio_detail and not doc.scio_detail
and not doc.dn_detail, and not doc.dn_detail
and doc.qty - doc.delivered_qty > 0,
}, },
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": { "Sales Team": {

View File

@@ -1947,6 +1947,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
account=gle.account, account=gle.account,
party_type=gle.party_type, party_type=gle.party_type,
party=gle.party, party=gle.party,
project=gle.project,
cost_center=gle.cost_center, cost_center=gle.cost_center,
finance_book=gle.finance_book, finance_book=gle.finance_book,
due_date=gle.due_date, due_date=gle.due_date,

View File

@@ -14,10 +14,10 @@
"for_user": "", "for_user": "",
"hide_custom": 0, "hide_custom": 0,
"icon": "accounting", "icon": "accounting",
"idx": 3, "idx": 4,
"indicator_color": "", "indicator_color": "",
"is_hidden": 0, "is_hidden": 0,
"label": "Accounting", "label": "Invoicing",
"links": [ "links": [
{ {
"hidden": 0, "hidden": 0,
@@ -587,10 +587,10 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2025-12-24 13:20:34.857205", "modified": "2026-01-23 11:05:47.246213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Invoicing",
"number_cards": [ "number_cards": [
{ {
"label": "Outgoing Bills", "label": "Outgoing Bills",
@@ -617,6 +617,6 @@
"roles": [], "roles": [],
"sequence_id": 2.0, "sequence_id": 2.0,
"shortcuts": [], "shortcuts": [],
"title": "Accounting", "title": "Invoicing",
"type": "Workspace" "type": "Workspace"
} }

View File

@@ -116,14 +116,6 @@ frappe.ui.form.on("Asset", {
__("Manage") __("Manage")
); );
frm.add_custom_button(
__("Repair Asset"),
function () {
frm.trigger("create_asset_repair");
},
__("Manage")
);
frm.add_custom_button( frm.add_custom_button(
__("Split Asset"), __("Split Asset"),
function () { function () {
@@ -155,6 +147,14 @@ frappe.ui.form.on("Asset", {
}, },
__("Manage") __("Manage")
); );
frm.add_custom_button(
__("Repair Asset"),
function () {
frm.trigger("create_asset_repair");
},
__("Manage")
);
} }
if (!frm.doc.calculate_depreciation) { if (!frm.doc.calculate_depreciation) {

View File

@@ -244,6 +244,8 @@ class Asset(AccountsController):
def before_submit(self): def before_submit(self):
if self.is_composite_asset and not has_active_capitalization(self.name): if self.is_composite_asset and not has_active_capitalization(self.name):
if self.split_from and has_active_capitalization(self.split_from):
return
frappe.throw(_("Please capitalize this asset before submitting.")) frappe.throw(_("Please capitalize this asset before submitting."))
def on_submit(self): def on_submit(self):

View File

@@ -1691,6 +1691,71 @@ class TestDepreciationBasics(AssetSetup):
pr.submit() pr.submit()
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
def test_split_asset_created_via_capitalization(self):
"""Test that assets created via Asset Capitalization can be split without capitalization error"""
from erpnext.assets.doctype.asset_capitalization.test_asset_capitalization import (
create_asset_capitalization,
create_asset_capitalization_data,
)
# Ensure test data exists
create_asset_capitalization_data()
company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company)
name = frappe.db.get_value(
"Asset Category Account",
filters={"parent": "Computers", "company_name": company},
fieldname=["name"],
)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
stock_rate = 1000
stock_qty = 2
total_amount = 2000
# Create composite asset
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset for Split",
is_composite_asset=1,
warehouse="Stores - TCP1",
company=company,
asset_quantity=2, # Set quantity > 1 to allow splitting
)
# Create and submit Asset Capitalization
asset_capitalization = create_asset_capitalization(
target_asset=wip_composite_asset.name,
stock_qty=stock_qty,
stock_rate=stock_rate,
company=company,
submit=1,
)
# Verify asset was capitalized
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.net_purchase_amount, total_amount)
self.assertEqual(target_asset.status, "Work In Progress")
# Submit the capitalized asset
target_asset.submit()
self.assertEqual(target_asset.status, "Submitted")
# Split the asset - this should work without capitalization error
split_qty = 1
splitted_asset = split_asset(target_asset.name, split_qty)
# Verify split asset was created and submitted successfully
self.assertIsNotNone(splitted_asset)
self.assertEqual(splitted_asset.asset_quantity, split_qty)
self.assertEqual(splitted_asset.split_from, target_asset.name)
self.assertEqual(splitted_asset.docstatus, 1) # Should be submitted
self.assertEqual(splitted_asset.status, "Submitted")
# Verify original asset was updated
target_asset.reload()
self.assertEqual(target_asset.asset_quantity, 1) # Remaining quantity
def get_gl_entries(doctype, docname): def get_gl_entries(doctype, docname):
gl_entry = frappe.qb.DocType("GL Entry") gl_entry = frappe.qb.DocType("GL Entry")

View File

@@ -574,13 +574,19 @@ class AssetCapitalization(StockController):
if self.docstatus == 2: if self.docstatus == 2:
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
purchase_amount = asset_doc.purchase_amount - total_target_asset_value purchase_amount = asset_doc.purchase_amount - total_target_asset_value
asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value) total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
else: else:
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
purchase_amount = asset_doc.purchase_amount + total_target_asset_value purchase_amount = asset_doc.purchase_amount + total_target_asset_value
total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
asset_doc.db_set("net_purchase_amount", net_purchase_amount) asset_doc.db_set(
asset_doc.db_set("purchase_amount", purchase_amount) {
"net_purchase_amount": net_purchase_amount,
"purchase_amount": purchase_amount,
"total_asset_cost": total_asset_cost,
}
)
frappe.msgprint( frappe.msgprint(
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format( _("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(

View File

@@ -212,7 +212,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
party = filters.get("customer") or filters.get("supplier") party = filters.get("customer") or filters.get("supplier")
item_rules_list = frappe.get_all( item_rules_list = frappe.get_all(
"Party Specific Item", "Party Specific Item",
filters={"party": party}, filters={
"party": ["!=", party],
"party_type": "Customer" if filters.get("customer") else "Supplier",
},
fields=["restrict_based_on", "based_on_value"], fields=["restrict_based_on", "based_on_value"],
) )
@@ -226,7 +229,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
filters_dict[rule.restrict_based_on].append(rule.based_on_value) filters_dict[rule.restrict_based_on].append(rule.based_on_value)
for filter in filters_dict: for filter in filters_dict:
filters[scrub(filter)] = ["in", filters_dict[filter]] filters[scrub(filter)] = ["not in", filters_dict[filter]]
if filters.get("customer"): if filters.get("customer"):
del filters["customer"] del filters["customer"]

View File

@@ -552,7 +552,7 @@ class StockController(AccountsController):
if is_rejected: if is_rejected:
serial_nos = row.get("rejected_serial_no") serial_nos = row.get("rejected_serial_no")
type_of_transaction = "Inward" if not self.is_return else "Outward" type_of_transaction = "Inward" if not self.is_return else "Outward"
qty = row.get("rejected_qty") qty = row.get("rejected_qty") * row.get("conversion_factor", 1.0)
warehouse = row.get("rejected_warehouse") warehouse = row.get("rejected_warehouse")
if ( if (

View File

@@ -166,29 +166,46 @@ class SubcontractingController(StockController):
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name) _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
) )
if self.doctype != "Subcontracting Receipt" and item.qty > flt( if self.doctype != "Subcontracting Receipt":
get_pending_subcontracted_quantity( order_item_doctype = (
self.doctype,
self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order,
).get(
item.purchase_order_item
if self.doctype == "Subcontracting Order"
else item.sales_order_item
)
/ item.subcontracting_conversion_factor,
frappe.get_precision(
"Purchase Order Item" "Purchase Order Item"
if self.doctype == "Subcontracting Order" if self.doctype == "Subcontracting Order"
else "Sales Order Item", else "Sales Order Item"
"qty",
),
):
frappe.throw(
_(
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
).format(item.idx, item.item_name)
) )
order_name = (
self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order
)
order_item_field = frappe.scrub(order_item_doctype)
if not item.get(order_item_field):
frappe.throw(
_("Row {0}: Item {1} must be linked to a {2}.").format(
item.idx, item.item_name, order_item_doctype
)
)
pending_qty = flt(
flt(
get_pending_subcontracted_quantity(
order_item_doctype,
order_name,
).get(item.get(order_item_field))
)
/ item.subcontracting_conversion_factor,
frappe.get_precision(
order_item_doctype,
"qty",
),
)
if item.qty > pending_qty:
frappe.throw(
_(
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
).format(item.idx, item.item_name)
)
if self.doctype != "Subcontracting Inward Order": if self.doctype != "Subcontracting Inward Order":
item.amount = item.qty * item.rate item.amount = item.qty * item.rate
@@ -1333,9 +1350,7 @@ def get_item_details(items):
def get_pending_subcontracted_quantity(doctype, name): def get_pending_subcontracted_quantity(doctype, name):
table = frappe.qb.DocType( table = frappe.qb.DocType(doctype)
"Purchase Order Item" if doctype == "Subcontracting Order" else "Sales Order Item"
)
query = ( query = (
frappe.qb.from_(table) frappe.qb.from_(table)
.select(table.name, table.stock_qty, table.subcontracted_qty) .select(table.name, table.stock_qty, table.subcontracted_qty)

View File

@@ -720,6 +720,7 @@ class SubcontractingInwardController:
item.db_set("scio_detail", scio_rm.name) item.db_set("scio_detail", scio_rm.name)
if data: if data:
precision = self.precision("customer_provided_item_cost", "items")
result = frappe.get_all( result = frappe.get_all(
"Subcontracting Inward Order Received Item", "Subcontracting Inward Order Received Item",
filters={ filters={
@@ -734,10 +735,17 @@ class SubcontractingInwardController:
table = frappe.qb.DocType("Subcontracting Inward Order Received Item") table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
case_expr_qty, case_expr_rate = Case(), Case() case_expr_qty, case_expr_rate = Case(), Case()
for d in result: for d in result:
d.received_qty += ( current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
data[d.name].transfer_qty if self._action == "submit" else -data[d.name].transfer_qty current_rate = flt(data[d.name].rate)
# Calculate weighted average rate
old_total = d.rate * d.received_qty
current_total = current_rate * current_qty
d.received_qty = d.received_qty + current_qty
d.rate = (
flt((old_total + current_total) / d.received_qty, precision) if d.received_qty else 0.0
) )
d.rate += data[d.name].rate if self._action == "submit" else -data[d.name].rate
if not d.required_qty and not d.received_qty: if not d.required_qty and not d.received_qty:
deleted_docs.append(d.name) deleted_docs.append(d.name)

View File

@@ -1,20 +1,20 @@
{ {
"app": "erpnext", "app": "erpnext",
"creation": "2025-11-17 20:55:11.854086", "creation": "2026-01-27 17:02:43.440221",
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "accounting", "icon": "accounting",
"icon_type": "Link", "icon_type": "Folder",
"idx": 1, "idx": 1,
"label": "Accounting", "label": "Accounting",
"link_to": "Accounting", "link_to": "",
"link_type": "Workspace Sidebar", "link_type": "Workspace Sidebar",
"modified": "2026-01-01 20:07:01.203651", "modified": "2026-01-27 17:04:04.351402",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Accounting", "name": "Accounting",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"standard": 1 "standard": 1

View File

@@ -1,18 +0,0 @@
{
"app": "erpnext",
"creation": "2025-11-12 13:07:51.988728",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon_type": "Folder",
"idx": 1,
"label": "Accounts",
"link_type": "DocType",
"logo_url": "",
"modified": "2025-11-17 17:39:36.915358",
"modified_by": "Administrator",
"name": "Accounts",
"owner": "Administrator",
"roles": [],
"standard": 1
}

View File

@@ -0,0 +1,21 @@
{
"app": "erpnext",
"bg_color": "blue",
"creation": "2026-01-27 17:37:55.824821",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon_type": "Link",
"idx": 3,
"label": "Accounts Setup",
"link_to": "Accounts Setup",
"link_type": "Workspace Sidebar",
"modified": "2026-01-27 18:34:57.092350",
"modified_by": "Administrator",
"name": "Accounts Setup",
"owner": "Administrator",
"parent_icon": "Accounting",
"restrict_removal": 0,
"roles": [],
"standard": 1
}

View File

@@ -14,7 +14,7 @@
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Banking", "name": "Banking",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "Accounting",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"standard": 1 "standard": 1

View File

@@ -1,20 +1,20 @@
{ {
"app": "erpnext", "app": "erpnext",
"creation": "2025-11-10 16:54:04.780644", "creation": "2026-01-23 11:00:23.272751",
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "expenses", "icon": "expenses",
"icon_type": "Link", "icon_type": "Link",
"idx": 4, "idx": 6,
"label": "Budget", "label": "Budget",
"link_to": "Budget", "link_to": "Budget",
"link_type": "Workspace Sidebar", "link_type": "Workspace Sidebar",
"modified": "2026-01-01 20:07:01.449176", "modified": "2026-01-23 14:39:30.839274",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Budget", "name": "Budget",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "Accounting",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"standard": 1 "standard": 1

View File

@@ -1,20 +1,20 @@
{ {
"app": "erpnext", "app": "erpnext",
"creation": "2025-11-17 20:55:11.772622", "creation": "2026-01-23 11:00:23.250819",
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "file", "icon": "file",
"icon_type": "Link", "icon_type": "Link",
"idx": 0, "idx": 2,
"label": "Financial Reports", "label": "Financial Reports",
"link_to": "Financial Reports", "link_to": "Financial Reports",
"link_type": "Workspace Sidebar", "link_type": "Workspace Sidebar",
"modified": "2026-01-01 20:07:01.253367", "modified": "2026-01-23 14:38:46.479759",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Financial Reports", "name": "Financial Reports",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "Accounting",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"sidebar": "", "sidebar": "",

View File

@@ -0,0 +1,21 @@
{
"app": "erpnext",
"creation": "2026-01-23 10:51:05.799725",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon": "accounting",
"icon_type": "Link",
"idx": 0,
"label": "Invoicing",
"link_to": "Invoicing",
"link_type": "Workspace Sidebar",
"modified": "2026-01-23 15:17:23.564795",
"modified_by": "Administrator",
"name": "Invoicing",
"owner": "Administrator",
"parent_icon": "Accounting",
"restrict_removal": 0,
"roles": [],
"standard": 1
}

View File

@@ -0,0 +1,22 @@
{
"app": "erpnext",
"bg_color": "blue",
"creation": "2026-01-27 17:37:55.866525",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon": "receipt-text",
"icon_type": "Link",
"idx": 1,
"label": "Payments",
"link_to": "Payments",
"link_type": "Workspace Sidebar",
"modified": "2026-01-27 18:31:59.617181",
"modified_by": "Administrator",
"name": "Payments",
"owner": "Administrator",
"parent_icon": "Accounting",
"restrict_removal": 0,
"roles": [],
"standard": 1
}

View File

@@ -1,19 +1,19 @@
{ {
"app": "erpnext", "app": "erpnext",
"creation": "2026-01-12 12:31:53.444807", "creation": "2026-01-23 11:00:23.303554",
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon_type": "Link", "icon_type": "Link",
"idx": 8, "idx": 7,
"label": "Share Management", "label": "Share Management",
"link_to": "Share Management", "link_to": "Share Management",
"link_type": "Workspace Sidebar", "link_type": "Workspace Sidebar",
"modified": "2026-01-12 12:31:53.444807", "modified": "2026-01-23 14:39:34.128991",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Share Management", "name": "Share Management",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "Accounting",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"standard": 1 "standard": 1

View File

@@ -1,20 +1,20 @@
{ {
"app": "erpnext", "app": "erpnext",
"creation": "2025-11-10 16:14:25.976756", "creation": "2026-01-23 11:00:23.344237",
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "monitor-check", "icon": "monitor-check",
"icon_type": "Link", "icon_type": "Link",
"idx": 99, "idx": 8,
"label": "Subscription", "label": "Subscription",
"link_to": "Subscription", "link_to": "Subscription",
"link_type": "Workspace Sidebar", "link_type": "Workspace Sidebar",
"modified": "2026-01-01 20:07:01.548581", "modified": "2026-01-23 14:39:37.830722",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Subscription", "name": "Subscription",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "Accounting",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"standard": 1 "standard": 1

View File

@@ -1,20 +1,20 @@
{ {
"app": "erpnext", "app": "erpnext",
"creation": "2025-11-12 15:05:54.474218", "creation": "2026-01-23 11:00:23.262357",
"docstatus": 0, "docstatus": 0,
"doctype": "Desktop Icon", "doctype": "Desktop Icon",
"hidden": 0, "hidden": 0,
"icon": "book-text", "icon": "book-text",
"icon_type": "Link", "icon_type": "Link",
"idx": 3, "idx": 4,
"label": "Taxes", "label": "Taxes",
"link_to": "Taxes", "link_to": "Taxes",
"link_type": "Workspace Sidebar", "link_type": "Workspace Sidebar",
"modified": "2026-01-01 20:07:01.356333", "modified": "2026-01-23 14:39:25.636166",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Taxes", "name": "Taxes",
"owner": "Administrator", "owner": "Administrator",
"parent_icon": "Accounts", "parent_icon": "Accounting",
"restrict_removal": 0, "restrict_removal": 0,
"roles": [], "roles": [],
"sidebar": "", "sidebar": "",

File diff suppressed because it is too large Load Diff

View File

@@ -697,8 +697,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
do_not_explode: d.do_not_explode, do_not_explode: d.do_not_explode,
}, },
callback: function (r) { callback: function (r) {
d = locals[cdt][cdn];
$.extend(d, r.message); $.extend(d, r.message);
refresh_field("items"); refresh_field("items");
refresh_field("scrap_items"); refresh_field("scrap_items");

View File

@@ -1547,6 +1547,9 @@ def add_operating_cost_component_wise(
if job_card and job_card.operation_id != row.name: if job_card and job_card.operation_id != row.name:
continue continue
if not row.actual_operation_time:
continue
workstation_cost = frappe.get_all( workstation_cost = frappe.get_all(
"Workstation Cost", "Workstation Cost",
fields=["operating_component", "operating_cost"], fields=["operating_component", "operating_cost"],
@@ -1609,7 +1612,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
job_card=job_card, job_card=job_card,
) )
if not cost_added: if not cost_added and not job_card:
stock_entry.append( stock_entry.append(
"additional_costs", "additional_costs",
{ {

View File

@@ -3725,6 +3725,53 @@ class TestWorkOrder(IntegrationTestCase):
wo = make_wo_order_test_record(item="Top Level Parent") wo = make_wo_order_test_record(item="Top Level Parent")
self.assertEqual([item.item_code for item in wo.required_items], expected) self.assertEqual([item.item_code for item in wo.required_items], expected)
def test_reserved_qty_for_pp_with_extra_material_transfer(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
rm_item_code = make_item(
"_Test Reserved Qty PP Item",
{
"is_stock_item": 1,
},
).name
fg_item_code = make_item(
"_Test Reserved Qty PP FG Item",
{
"is_stock_item": 1,
},
).name
make_stock_entry_test_record(
item_code=rm_item_code, target="_Test Warehouse - _TC", qty=10, basic_rate=100
)
make_bom(
item=fg_item_code,
raw_materials=[rm_item_code],
)
wo_order = make_wo_order_test_record(
item=fg_item_code,
qty=1,
source_warehouse="_Test Warehouse - _TC",
skip_transfer=0,
target_warehouse="_Test Warehouse - _TC",
)
bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 1)
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
s.items[0].qty += 2 # extra material transfer
s.submit()
bin1_at_completion = get_bin(rm_item_code, "_Test Warehouse - _TC")
self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0)
def get_reserved_entries(voucher_no, warehouse=None): def get_reserved_entries(voucher_no, warehouse=None):
doctype = frappe.qb.DocType("Stock Reservation Entry") doctype = frappe.qb.DocType("Stock Reservation Entry")

View File

@@ -770,6 +770,7 @@ class WorkOrder(Document):
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
self.on_close_or_cancel() self.on_close_or_cancel()
self.delete_job_card()
def on_close_or_cancel(self): def on_close_or_cancel(self):
if self.production_plan and frappe.db.exists( if self.production_plan and frappe.db.exists(
@@ -779,7 +780,6 @@ class WorkOrder(Document):
else: else:
self.update_work_order_qty_in_so() self.update_work_order_qty_in_so()
self.delete_job_card()
self.update_completed_qty_in_material_request() self.update_completed_qty_in_material_request()
self.update_planned_qty() self.update_planned_qty()
self.update_ordered_qty() self.update_ordered_qty()
@@ -2654,6 +2654,9 @@ def get_reserved_qty_for_production(
qty_field = wo_item.required_qty qty_field = wo_item.required_qty
else: else:
qty_field = Case() qty_field = Case()
qty_field = qty_field.when(
((wo.skip_transfer == 0) & (wo_item.transferred_qty > wo_item.required_qty)), 0.0
)
qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty) qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty)

View File

@@ -603,7 +603,7 @@ def send_project_update_email_to_users(project):
"sent": 0, "sent": 0,
"date": today(), "date": today(),
"time": nowtime(), "time": nowtime(),
"naming_series": "UPDATE-.project.-.YY.MM.DD.-", "naming_series": "UPDATE-.project.-.YY.MM.DD.-.####",
} }
).insert() ).insert()

View File

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 928 B

View File

@@ -637,6 +637,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
tax_count ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total tax_count ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff : this.frm.doc.net_total
); );
// total taxes and charges is calculated before adjusting base grand total
this.frm.doc.total_taxes_and_charges = flt(
this.frm.doc.grand_total - this.frm.doc.net_total - grand_total_diff,
precision("total_taxes_and_charges")
);
if ( if (
["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes( ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(
this.frm.doc.doctype this.frm.doc.doctype
@@ -679,11 +685,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
]); ]);
} }
this.frm.doc.total_taxes_and_charges = flt(
this.frm.doc.grand_total - this.frm.doc.net_total - grand_total_diff,
precision("total_taxes_and_charges")
);
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]); this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
// Round grand total as per precision // Round grand total as per precision

View File

@@ -3131,10 +3131,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
set_warehouse() { set_warehouse() {
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse); this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
this.autofill_warehouse(this.frm.doc.packed_items, "warehouse", this.frm.doc.set_warehouse);
} }
set_target_warehouse() { set_target_warehouse() {
this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse); this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse);
this.autofill_warehouse(
this.frm.doc.packed_items,
"target_warehouse",
this.frm.doc.set_target_warehouse
);
} }
set_from_warehouse() { set_from_warehouse() {

View File

@@ -1,12 +1,17 @@
frappe.provide("erpnext.financial_statements"); frappe.provide("erpnext.financial_statements");
function get_filter_value(filter_name) {
// not warn when the filter is missing
return frappe.query_report.get_filter_value(filter_name, false);
}
erpnext.financial_statements = { erpnext.financial_statements = {
filters: get_filters(), filters: get_filters(),
baseData: null, baseData: null,
get_pdf_format: function (report, custom_format) { get_pdf_format: function (report, custom_format) {
// If report template is selected, use default pdf formatting // If report template is selected, use default pdf formatting
return report.get_filter_value("report_template") ? null : custom_format; return get_filter_value("report_template") ? null : custom_format;
}, },
formatter: function (value, row, column, data, default_formatter, filter) { formatter: function (value, row, column, data, default_formatter, filter) {
@@ -15,14 +20,14 @@ erpnext.financial_statements = {
if (erpnext.financial_statements._is_special_view(column, data)) if (erpnext.financial_statements._is_special_view(column, data))
return erpnext.financial_statements._format_special_view(...report_params); return erpnext.financial_statements._format_special_view(...report_params);
if (frappe.query_report.get_filter_value("report_template")) if (get_filter_value("report_template"))
return erpnext.financial_statements._format_custom_report(...report_params); return erpnext.financial_statements._format_custom_report(...report_params);
else return erpnext.financial_statements._format_standard_report(...report_params); else return erpnext.financial_statements._format_standard_report(...report_params);
}, },
_is_special_view: function (column, data) { _is_special_view: function (column, data) {
if (!data) return false; if (!data) return false;
const view = frappe.query_report.get_filter_value("selected_view"); const view = get_filter_value("selected_view");
return (view === "Growth" && column.colIndex >= 3) || (view === "Margin" && column.colIndex >= 2); return (view === "Growth" && column.colIndex >= 3) || (view === "Margin" && column.colIndex >= 2);
}, },
@@ -100,7 +105,7 @@ erpnext.financial_statements = {
from_date: formatting.from_date || formatting.period_start_date, from_date: formatting.from_date || formatting.period_start_date,
to_date: formatting.to_date || formatting.period_end_date, to_date: formatting.to_date || formatting.period_end_date,
account_type: formatting.account_type, account_type: formatting.account_type,
company: frappe.query_report.get_filter_value("company"), company: get_filter_value("company"),
}; };
column.link_onclick = column.link_onclick =
@@ -177,7 +182,7 @@ erpnext.financial_statements = {
}, },
_format_special_view: function (value, row, column, data, default_formatter) { _format_special_view: function (value, row, column, data, default_formatter) {
const selectedView = frappe.query_report.get_filter_value("selected_view"); const selectedView = get_filter_value("selected_view");
if (selectedView === "Growth") { if (selectedView === "Growth") {
const growthPercent = data[column.fieldname]; const growthPercent = data[column.fieldname];
@@ -252,7 +257,7 @@ erpnext.financial_statements = {
frappe.route_options = { frappe.route_options = {
account: data.account || data.accounts, account: data.account || data.accounts,
company: frappe.query_report.get_filter_value("company"), company: get_filter_value("company"),
from_date: data.from_date || data.year_start_date, from_date: data.from_date || data.year_start_date,
to_date: data.to_date || data.year_end_date, to_date: data.to_date || data.year_end_date,
project: project && project.length > 0 ? project[0].get_value() : "", project: project && project.length > 0 ? project[0].get_value() : "",
@@ -305,17 +310,49 @@ erpnext.financial_statements = {
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function () { report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function () {
var filters = report.get_values(); var filters = report.get_values();
frappe.set_route("query-report", "Balance Sheet", { company: filters.company }); frappe.set_route("query-report", "Balance Sheet", {
company: filters.company,
filter_based_on: filters.filter_based_on,
period_start_date: filters.period_start_date,
period_end_date: filters.period_end_date,
from_fiscal_year: filters.from_fiscal_year,
to_fiscal_year: filters.to_fiscal_year,
periodicity: filters.periodicity,
presentation_currency: filters.presentation_currency,
cost_center: filters.cost_center,
project: filters.project,
});
}); });
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function () { report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function () {
var filters = report.get_values(); var filters = report.get_values();
frappe.set_route("query-report", "Profit and Loss Statement", { company: filters.company }); frappe.set_route("query-report", "Profit and Loss Statement", {
company: filters.company,
filter_based_on: filters.filter_based_on,
period_start_date: filters.period_start_date,
period_end_date: filters.period_end_date,
from_fiscal_year: filters.from_fiscal_year,
to_fiscal_year: filters.to_fiscal_year,
periodicity: filters.periodicity,
presentation_currency: filters.presentation_currency,
cost_center: filters.cost_center,
project: filters.project,
});
}); });
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function () { report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function () {
var filters = report.get_values(); var filters = report.get_values();
frappe.set_route("query-report", "Cash Flow", { company: filters.company }); frappe.set_route("query-report", "Cash Flow", {
company: filters.company,
filter_based_on: filters.filter_based_on,
period_start_date: filters.period_start_date,
period_end_date: filters.period_end_date,
from_fiscal_year: filters.from_fiscal_year,
to_fiscal_year: filters.to_fiscal_year,
periodicity: filters.periodicity,
cost_center: filters.cost_center,
project: filters.project,
});
}); });
} }
}, },
@@ -345,7 +382,7 @@ function get_filters() {
default: ["Fiscal Year"], default: ["Fiscal Year"],
reqd: 1, reqd: 1,
on_change: function () { on_change: function () {
let filter_based_on = frappe.query_report.get_filter_value("filter_based_on"); let filter_based_on = get_filter_value("filter_based_on");
frappe.query_report.toggle_filter_display( frappe.query_report.toggle_filter_display(
"from_fiscal_year", "from_fiscal_year",
filter_based_on === "Date Range" filter_based_on === "Date Range"
@@ -422,7 +459,7 @@ function get_filters() {
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, { return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"), company: get_filter_value("company"),
}); });
}, },
options: "Cost Center", options: "Cost Center",
@@ -433,7 +470,7 @@ function get_filters() {
fieldtype: "MultiSelectList", fieldtype: "MultiSelectList",
get_data: function (txt) { get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, { return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"), company: get_filter_value("company"),
}); });
}, },
options: "Project", options: "Project",

View File

@@ -495,7 +495,30 @@ erpnext.sales_common = {
} }
} }
project() { project(doc, cdt, cdn) {
if (!cdt || !cdn) {
if (this.frm.doc.project) {
$.each(this.frm.doc["items"] || [], function (i, item) {
if (!item.project) {
frappe.model.set_value(item.doctype, item.name, "project", doc.project);
}
});
}
} else {
const item = frappe.get_doc(cdt, cdn);
if (item.project) {
$.each(this.frm.doc["items"] || [], function (i, other_item) {
if (!other_item.project) {
frappe.model.set_value(
other_item.doctype,
other_item.name,
"project",
item.project
);
}
});
}
}
let me = this; let me = this;
if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) { if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) {
if (this.frm.doc.project) { if (this.frm.doc.project) {

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -183,6 +183,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Customer Group", "label": "Customer Group",
"link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"oldfieldname": "customer_group", "oldfieldname": "customer_group",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Customer Group", "options": "Customer Group",
@@ -625,7 +626,7 @@
"link_fieldname": "party" "link_fieldname": "party"
} }
], ],
"modified": "2026-01-16 15:56:05.967663", "modified": "2026-01-21 17:23:42.151114",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@@ -117,6 +117,7 @@ class Customer(TransactionBase):
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def get_customer_name(self): def get_customer_name(self):
self.customer_name = self.customer_name.strip()
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
count = frappe.db.sql( count = frappe.db.sql(
"""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer

View File

@@ -35,8 +35,7 @@ class TestPartySpecificItem(IntegrationTestCase):
items = item_query( items = item_query(
doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False
) )
for item in items: self.assertTrue(self.item.name in flatten(items))
self.assertEqual(item[0], self.item.name)
def test_item_query_for_supplier(self): def test_item_query_for_supplier(self):
create_party_specific_item( create_party_specific_item(
@@ -49,5 +48,14 @@ class TestPartySpecificItem(IntegrationTestCase):
items = item_query( items = item_query(
doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False
) )
for item in items: self.assertTrue(self.item.item_group in flatten(items))
self.assertEqual(item[2], self.item.item_group)
def flatten(lst):
result = []
for item in lst:
if isinstance(item, tuple):
result.extend(flatten(item))
else:
result.append(item)
return result

View File

@@ -61,6 +61,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Default Customer Group", "label": "Default Customer Group",
"link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"options": "Customer Group" "options": "Customer Group"
}, },
{ {
@@ -297,7 +298,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-01-02 18:17:05.734945", "modified": "2026-01-21 17:28:37.027837",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

View File

@@ -6,14 +6,14 @@
} }
}, },
"Algeria": { "Algeria": {
"Algeria VAT 17%": { "Algeria TVA 19%": {
"account_name": "VAT 17%", "account_name": "TVA 19%",
"tax_rate": 17.00, "tax_rate": 19.00,
"default": 1 "default": 1
}, },
"Algeria VAT 7%": { "Algeria TVA 9%": {
"account_name": "VAT 7%", "account_name": "TVA 9%",
"tax_rate": 7.00 "tax_rate": 9.00
} }
}, },

View File

@@ -97,7 +97,6 @@ class DeprecatedBatchNoValuation:
for ledger in entries: for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value) self.stock_value_differece[ledger.batch_no] += flt(ledger.batch_value)
self.available_qty[ledger.batch_no] += flt(ledger.batch_qty) self.available_qty[ledger.batch_no] += flt(ledger.batch_qty)
self.total_qty[ledger.batch_no] += flt(ledger.batch_qty)
@deprecated( @deprecated(
"erpnext.stock.serial_batch_bundle.BatchNoValuation.get_sle_for_batches", "erpnext.stock.serial_batch_bundle.BatchNoValuation.get_sle_for_batches",
@@ -271,7 +270,6 @@ class DeprecatedBatchNoValuation:
batch_data = query.run(as_dict=True) batch_data = query.run(as_dict=True)
for d in batch_data: for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty) self.available_qty[d.batch_no] += flt(d.batch_qty)
self.total_qty[d.batch_no] += flt(d.batch_qty)
for d in batch_data: for d in batch_data:
if self.available_qty.get(d.batch_no): if self.available_qty.get(d.batch_no):
@@ -383,7 +381,6 @@ class DeprecatedBatchNoValuation:
batch_data = query.run(as_dict=True) batch_data = query.run(as_dict=True)
for d in batch_data: for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty) self.available_qty[d.batch_no] += flt(d.batch_qty)
self.total_qty[d.batch_no] += flt(d.batch_qty)
if not self.last_sle: if not self.last_sle:
return return

View File

@@ -232,8 +232,25 @@ class Item(Document):
cint(frappe.get_single_value("Stock Settings", "clean_description_html")) cint(frappe.get_single_value("Stock Settings", "clean_description_html"))
and self.description != self.item_name # perf: Avoid cleaning up a fallback and self.description != self.item_name # perf: Avoid cleaning up a fallback
): ):
old_desc = self.description
self.description = clean_html(self.description) self.description = clean_html(self.description)
if (
old_desc
and self.description
and "<img src" in old_desc
and "<img src" not in self.description
):
frappe.msgprint(
_(
'Image in the description has been removed. To disable this behavior, uncheck "{0}" in {1}.'
).format(
frappe.get_meta("Stock Settings").get_label("clean_description_html"),
get_link_to_form("Stock Settings"),
),
alert=True,
)
def validate_customer_provided_part(self): def validate_customer_provided_part(self):
if self.is_customer_provided_item: if self.is_customer_provided_item:
if self.is_purchase_item: if self.is_purchase_item:

View File

@@ -282,7 +282,6 @@
{ {
"fieldname": "set_warehouse", "fieldname": "set_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Set Target Warehouse", "label": "Set Target Warehouse",
"options": "Warehouse" "options": "Warehouse"
@@ -378,7 +377,7 @@
"idx": 70, "idx": 70,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2026-01-10 15:34:59.000603", "modified": "2026-01-21 12:48:40.792323",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Material Request", "name": "Material Request",

View File

@@ -4997,6 +4997,45 @@ class TestPurchaseReceipt(IntegrationTestCase):
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]]) self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
def test_negative_stock_error_for_purchase_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = make_item(
"Test Negative Stock for Purchase Return Item",
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TNSFPRI.#####"},
).name
pr = make_purchase_receipt(
item_code=item_code,
posting_date=add_days(today(), -3),
qty=10,
rate=100,
warehouse="_Test Warehouse - _TC",
)
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
make_purchase_receipt(
item_code=item_code,
posting_date=add_days(today(), -4),
qty=10,
rate=100,
warehouse="_Test Warehouse - _TC",
)
make_stock_entry(
item_code=item_code,
qty=10,
source="_Test Warehouse - _TC",
target="_Test Warehouse 1 - _TC",
batch_no=batch_no,
use_serial_batch_fields=1,
)
return_pr = make_return_doc("Purchase Receipt", pr.name)
self.assertRaises(frappe.ValidationError, return_pr.submit)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -576,14 +576,12 @@ class SerialandBatchBundle(Document):
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
precision = d.precision("qty") precision = d.precision("qty")
for field in ["available_qty", "total_qty"]: available_qty = flt(sn_obj.available_qty.get(d.batch_no), precision)
value = getattr(sn_obj, field) if self.docstatus == 1:
available_qty = flt(value.get(d.batch_no), precision) available_qty += flt(d.qty, precision)
if self.docstatus == 1:
available_qty += flt(d.qty, precision)
if not allow_negative_stock: if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty, field) self.validate_negative_batch(d.batch_no, available_qty)
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -596,8 +594,8 @@ class SerialandBatchBundle(Document):
} }
) )
def validate_negative_batch(self, batch_no, available_qty, field=None): def validate_negative_batch(self, batch_no, available_qty):
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty, field=field): if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)} msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
has negative stock has negative stock
of quantity {bold(available_qty)} in the of quantity {bold(available_qty)} in the
@@ -605,7 +603,7 @@ class SerialandBatchBundle(Document):
frappe.throw(_(msg), BatchNegativeStockError) frappe.throw(_(msg), BatchNegativeStockError)
def is_stock_reco_for_valuation_adjustment(self, available_qty, field=None): def is_stock_reco_for_valuation_adjustment(self, available_qty):
if ( if (
self.voucher_type == "Stock Reconciliation" self.voucher_type == "Stock Reconciliation"
and self.type_of_transaction == "Outward" and self.type_of_transaction == "Outward"
@@ -613,7 +611,6 @@ class SerialandBatchBundle(Document):
and ( and (
abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty")) abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
== abs(available_qty) == abs(available_qty)
or field == "total_qty"
) )
): ):
return True return True
@@ -1344,6 +1341,7 @@ class SerialandBatchBundle(Document):
def on_submit(self): def on_submit(self):
self.validate_docstatus() self.validate_docstatus()
self.validate_serial_nos_inventory() self.validate_serial_nos_inventory()
self.validate_batch_quantity()
def validate_docstatus(self): def validate_docstatus(self):
for row in self.entries: for row in self.entries:
@@ -1437,6 +1435,106 @@ class SerialandBatchBundle(Document):
def on_cancel(self): def on_cancel(self):
self.validate_voucher_no_docstatus() self.validate_voucher_no_docstatus()
self.validate_batch_quantity()
def validate_batch_quantity(self):
if not self.has_batch_no:
return
if self.type_of_transaction != "Outward" or (
self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Outward"
):
return
batch_wise_available_qty = self.get_batchwise_available_qty()
precision = frappe.get_precision("Serial and Batch Entry", "qty")
for d in self.entries:
available_qty = batch_wise_available_qty.get(d.batch_no, 0)
if flt(available_qty, precision) < 0:
frappe.throw(
_(
"""
The Batch {0} of an item {1} has negative stock in the warehouse {2}. Please add a stock quantity of {3} to proceed with this entry."""
).format(
bold(d.batch_no),
bold(self.item_code),
bold(self.warehouse),
bold(abs(flt(available_qty, precision))),
),
title=_("Negative Stock Error"),
)
def get_batchwise_available_qty(self):
available_qty = self.get_available_qty_from_sabb()
available_qty_from_ledger = self.get_available_qty_from_stock_ledger()
if not available_qty_from_ledger:
return available_qty
for batch_no, qty in available_qty_from_ledger.items():
if batch_no in available_qty:
available_qty[batch_no] += qty
else:
available_qty[batch_no] = qty
return available_qty
def get_available_qty_from_stock_ledger(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
sle = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(sle)
.select(
sle.batch_no,
Sum(sle.actual_qty).as_("available_qty"),
)
.where(
(sle.item_code == self.item_code)
& (sle.warehouse == self.warehouse)
& (sle.is_cancelled == 0)
& (sle.batch_no.isin(batches))
& (sle.docstatus == 1)
& (sle.serial_and_batch_bundle.isnull())
& (sle.batch_no.isnotnull())
)
.for_update()
.groupby(sle.batch_no)
)
res = query.run(as_list=True)
return frappe._dict(res) if res else frappe._dict()
def get_available_qty_from_sabb(self):
batches = [d.batch_no for d in self.entries if d.batch_no]
child = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(child)
.select(
child.batch_no,
Sum(child.qty).as_("available_qty"),
)
.where(
(child.item_code == self.item_code)
& (child.warehouse == self.warehouse)
& (child.is_cancelled == 0)
& (child.batch_no.isin(batches))
& (child.docstatus == 1)
& (child.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
.groupby(child.batch_no)
)
query = query.where(child.voucher_type != "Pick List")
res = query.run(as_list=True)
return frappe._dict(res) if res else frappe._dict()
def validate_voucher_no_docstatus(self): def validate_voucher_no_docstatus(self):
if self.voucher_type == "POS Invoice": if self.voucher_type == "POS Invoice":

View File

@@ -261,9 +261,9 @@ frappe.ui.form.on("Shipment", {
frappe.db.get_value( frappe.db.get_value(
"User", "User",
{ name: frappe.session.user }, { name: frappe.session.user },
["full_name", "last_name", "email", "phone", "mobile_no"], ["full_name", "email", "phone", "mobile_no"],
(r) => { (r) => {
if (!(r.last_name && r.email && (r.phone || r.mobile_no))) { if (!(r.full_name && r.email && (r.phone || r.mobile_no))) {
if (delivery_type == "Delivery") { if (delivery_type == "Delivery") {
frm.set_value("delivery_company", ""); frm.set_value("delivery_company", "");
frm.set_value("delivery_contact", ""); frm.set_value("delivery_contact", "");
@@ -272,9 +272,9 @@ frappe.ui.form.on("Shipment", {
frm.set_value("pickup_contact", ""); frm.set_value("pickup_contact", "");
} }
frappe.throw( frappe.throw(
__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + __("Full Name, Email or Phone/Mobile of the user are mandatory to continue.") +
"</br>" + "</br>" +
__("Please first set Last Name, Email and Phone for the user") + __("Please first set Full Name, Email and Phone for the user") +
` <a href="/app/user/${frappe.session.user}">${frappe.session.user}</a>` ` <a href="/app/user/${frappe.session.user}">${frappe.session.user}</a>`
); );
} }

View File

@@ -278,9 +278,30 @@ class StockEntry(StockController, SubcontractingInwardController):
self.validate_closed_subcontracting_order() self.validate_closed_subcontracting_order()
self.validate_subcontract_order() self.validate_subcontract_order()
self.validate_raw_materials_exists()
super().validate_subcontracting_inward() super().validate_subcontracting_inward()
def validate_raw_materials_exists(self):
if self.purpose not in ["Manufacture", "Repack", "Disassemble"]:
return
if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
return
raw_materials = []
for row in self.items:
if row.s_warehouse:
raw_materials.append(row.item_code)
if not raw_materials:
frappe.throw(
_(
"At least one raw material item must be present in the stock entry for the type {0}"
).format(bold(self.purpose)),
title=_("Raw Materials Missing"),
)
def set_serial_batch_for_disassembly(self): def set_serial_batch_for_disassembly(self):
if self.purpose != "Disassemble": if self.purpose != "Disassemble":
return return

View File

@@ -2362,6 +2362,28 @@ class TestStockEntry(IntegrationTestCase):
self.assertEqual(target_sabb.entries[0].batch_no, batch) self.assertEqual(target_sabb.entries[0].batch_no, batch)
self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2])
def test_raw_material_missing_validation(self):
original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption")
frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0)
stock_entry = make_stock_entry(
item_code="_Test Item",
qty=1,
target="_Test Warehouse - _TC",
do_not_save=True,
)
stock_entry.purpose = "Manufacture"
stock_entry.stock_entry_type = "Manufacture"
stock_entry.items[0].is_finished_item = 1
self.assertRaises(
frappe.ValidationError,
stock_entry.save,
)
frappe.db.set_single_value("Manufacturing Settings", "material_consumption", original_value)
@IntegrationTestCase.change_settings( @IntegrationTestCase.change_settings(
"Manufacturing Settings", "Manufacturing Settings",
{ {

View File

@@ -666,6 +666,16 @@ class StockReconciliation(StockController):
validate_is_stock_item, validate_is_stock_item,
) )
def validate_serial_batch_items():
has_batch_no, has_serial_no = frappe.get_value(
"Item", item_code, ["has_batch_no", "has_serial_no"]
)
if row.use_serial_batch_fields and self.purpose == "Stock Reconciliation":
if has_batch_no and not row.batch_no:
raise frappe.ValidationError(_("Please enter Batch No"))
if has_serial_no and not row.serial_no:
raise frappe.ValidationError(_("Please enter Serial No"))
# using try except to catch all validation msgs and display together # using try except to catch all validation msgs and display together
try: try:
@@ -674,12 +684,13 @@ class StockReconciliation(StockController):
# end of life and stock item # end of life and stock item
validate_end_of_life(item_code, item.end_of_life, item.disabled) validate_end_of_life(item_code, item.end_of_life, item.disabled)
validate_is_stock_item(item_code, item.is_stock_item) validate_is_stock_item(item_code, item.is_stock_item)
validate_serial_batch_items()
# docstatus should be < 2 # docstatus should be < 2
validate_cancelled_item(item_code, item.docstatus) validate_cancelled_item(item_code, item.docstatus)
except Exception as e: except Exception as e:
self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) self.validation_messages.append(_("Row #") + ("%d: " % (row.idx)) + cstr(e))
def validate_reserved_stock(self) -> None: def validate_reserved_stock(self) -> None:
"""Raises an exception if there is any reserved stock for the items in the Stock Reconciliation.""" """Raises an exception if there is any reserved stock for the items in the Stock Reconciliation."""

View File

@@ -1450,6 +1450,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin):
qty=10, qty=10,
rate=100, rate=100,
use_serial_batch_fields=1, use_serial_batch_fields=1,
purpose="Opening Stock",
) )
sr.reload() sr.reload()
@@ -1592,6 +1593,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin):
qty=10, qty=10,
rate=80, rate=80,
use_serial_batch_fields=1, use_serial_batch_fields=1,
purpose="Opening Stock",
) )
batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle) batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle)
@@ -1676,6 +1678,7 @@ class TestStockReconciliation(IntegrationTestCase, StockTestMixin):
qty=10, qty=10,
rate=100, rate=100,
use_serial_batch_fields=1, use_serial_batch_fields=1,
purpose="Opening Stock",
) )
sr.reload() sr.reload()

View File

@@ -439,8 +439,10 @@ def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> It
if not ctx.uom: if not ctx.uom:
if ctx.doctype in sales_doctypes: if ctx.doctype in sales_doctypes:
ctx.uom = item.sales_uom if item.sales_uom else item.stock_uom ctx.uom = item.sales_uom if item.sales_uom else item.stock_uom
elif (ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( elif (
ctx.doctype == "Material Request" and ctx.material_request_type == "Purchase" (ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"])
or (ctx.doctype == "Material Request" and ctx.material_request_type == "Purchase")
or (ctx.doctype == "Supplier Quotation")
): ):
ctx.uom = item.purchase_uom if item.purchase_uom else item.stock_uom ctx.uom = item.purchase_uom if item.purchase_uom else item.stock_uom
else: else:

View File

@@ -807,62 +807,11 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
for ledger in entries: for ledger in entries:
self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate) self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
self.available_qty[ledger.batch_no] += flt(ledger.qty) self.available_qty[ledger.batch_no] += flt(ledger.qty)
self.total_qty[ledger.batch_no] += flt(ledger.qty)
entries = self.get_batch_stock_after_date()
for row in entries:
self.total_qty[row.batch_no] += flt(row.total_qty)
self.calculate_avg_rate_from_deprecarated_ledgers() self.calculate_avg_rate_from_deprecarated_ledgers()
self.calculate_avg_rate_for_non_batchwise_valuation() self.calculate_avg_rate_for_non_batchwise_valuation()
self.set_stock_value_difference() self.set_stock_value_difference()
def get_batch_stock_after_date(self) -> list[dict]:
# Get total qty of each batch no from Serial and Batch Bundle without checking time condition
if not self.batchwise_valuation_batches:
return []
child = frappe.qb.DocType("Serial and Batch Entry")
timestamp_condition = ""
if self.sle.posting_datetime:
timestamp_condition = child.posting_datetime > self.sle.posting_datetime
if self.sle.creation:
timestamp_condition |= (child.posting_datetime == self.sle.posting_datetime) & (
child.creation > self.sle.creation
)
query = (
frappe.qb.from_(child)
.select(
child.batch_no,
Sum(child.qty).as_("total_qty"),
)
.where(
(child.item_code == self.sle.item_code)
& (child.warehouse == self.sle.warehouse)
& (child.batch_no.isin(self.batchwise_valuation_batches))
& (child.docstatus == 1)
& (child.type_of_transaction.isin(["Inward", "Outward"]))
)
.for_update()
.groupby(child.batch_no)
)
# Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
if self.sle.voucher_detail_no:
query = query.where(child.voucher_detail_no != self.sle.voucher_detail_no)
elif self.sle.voucher_no:
query = query.where(child.voucher_no != self.sle.voucher_no)
query = query.where(child.voucher_type != "Pick List")
if timestamp_condition:
query = query.where(timestamp_condition)
return query.run(as_dict=True)
def get_batch_stock_before_date(self) -> list[dict]: def get_batch_stock_before_date(self) -> list[dict]:
# Get batch wise stock value difference from Serial and Batch Bundle considering time condition # Get batch wise stock value difference from Serial and Batch Bundle considering time condition
if not self.batchwise_valuation_batches: if not self.batchwise_valuation_batches:

View File

@@ -51,6 +51,49 @@ class IntegrationTestSubcontractingInwardOrder(IntegrationTestCase):
for item in rm_in.get("items"): for item in rm_in.get("items"):
self.assertEqual(item.customer_provided_item_cost, 15) self.assertEqual(item.customer_provided_item_cost, 15)
def test_customer_provided_item_cost_with_multiple_receipts(self):
"""
Validate that rate is calculated correctly (Weighted Average) when multiple receipts
occur for the same SCIO Received Item.
"""
so, scio = create_so_scio()
rm_item = "Basic RM"
# Receipt 1: 5 Qty @ Unit Cost 10
rm_in_1 = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
rm_in_1.items = [item for item in rm_in_1.items if item.item_code == rm_item]
rm_in_1.items[0].qty = 5
rm_in_1.items[0].basic_rate = 10
rm_in_1.items[0].transfer_qty = 5
rm_in_1.submit()
scio.reload()
received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item)
self.assertEqual(received_item.rate, 10)
# Receipt 2: 5 Qty @ Unit Cost 20
rm_in_2 = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
rm_in_2.items = [item for item in rm_in_2.items if item.item_code == rm_item]
rm_in_2.items[0].qty = 5
rm_in_2.items[0].basic_rate = 20
rm_in_2.items[0].transfer_qty = 5
rm_in_2.save()
rm_in_2.submit()
# Check 2: Rate should be Weighted Average
# (5 * 10 + 5 * 20) / 10 = 150 / 10 = 15
scio.reload()
received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item)
self.assertEqual(received_item.rate, 15)
# Cancel Receipt 2: Rate should revert to original
# (15 * 10 - 20 * 5) / 5 = 50 / 5 = 10
rm_in_2.cancel()
scio.reload()
received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item)
self.assertEqual(received_item.received_qty, 5)
self.assertEqual(received_item.rate, 10)
def test_add_extra_customer_provided_item(self): def test_add_extra_customer_provided_item(self):
so, scio = create_so_scio() so, scio = create_so_scio()

View File

@@ -0,0 +1,287 @@
{
"app": "erpnext",
"creation": "2026-01-23 14:36:51.659571",
"docstatus": 0,
"doctype": "Workspace Sidebar",
"header_icon": "database",
"idx": 1,
"items": [
{
"child": 0,
"collapsible": 1,
"icon": "database",
"indent": 1,
"keep_closed": 0,
"label": "Setup",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Chart of Accounts",
"link_to": "Account",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Chart of Cost Centers",
"link_to": "Cost Center",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Account Category",
"link_to": "Account Category",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Accounting Dimension",
"link_to": "Accounting Dimension",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Currency",
"link_to": "Currency",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Currency Exchange",
"link_to": "Currency Exchange",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Finance Book",
"link_to": "Finance Book",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Mode of Payment",
"link_to": "Mode of Payment",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Payment Term",
"link_to": "Payment Term",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Journal Entry Template",
"link_to": "Journal Entry Template",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Terms and Conditions",
"link_to": "Terms and Conditions",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Company",
"link_to": "Company",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Fiscal Year",
"link_to": "Fiscal Year",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "lock-keyhole-open",
"indent": 1,
"keep_closed": 0,
"label": "Opening & Closing",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "COA Importer",
"link_to": "Chart of Accounts Importer",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Opening Invoice Tool",
"link_to": "Opening Invoice Creation Tool",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Accounting Period",
"link_to": "Accounting Period",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "FX Revaluation",
"link_to": "Exchange Rate Revaluation",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Period Closing Voucher",
"link_to": "Period Closing Voucher",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "settings",
"indent": 1,
"keep_closed": 0,
"label": "Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Accounts Settings",
"link_to": "Accounts Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Currency Exchange Settings",
"link_to": "Currency Exchange Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Repost Accounting Ledger Settings",
"link_to": "Repost Accounting Ledger Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
}
],
"modified": "2026-01-23 16:43:27.989825",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Setup",
"owner": "Administrator",
"standard": 1,
"title": "Accounts Setup"
}

View File

@@ -3,8 +3,9 @@
"creation": "2025-10-26 21:53:46.303793", "creation": "2025-10-26 21:53:46.303793",
"docstatus": 0, "docstatus": 0,
"doctype": "Workspace Sidebar", "doctype": "Workspace Sidebar",
"for_user": "",
"header_icon": "accounting", "header_icon": "accounting",
"idx": 0, "idx": 1,
"items": [ "items": [
{ {
"child": 0, "child": 0,
@@ -13,7 +14,7 @@
"indent": 0, "indent": 0,
"keep_closed": 0, "keep_closed": 0,
"label": "Home", "label": "Home",
"link_to": "Accounting", "link_to": "Invoicing",
"link_type": "Workspace", "link_type": "Workspace",
"show_arrow": 0, "show_arrow": 0,
"type": "Link" "type": "Link"
@@ -30,6 +31,18 @@
"show_arrow": 0, "show_arrow": 0,
"type": "Link" "type": "Link"
}, },
{
"child": 0,
"collapsible": 1,
"icon": "list-tree",
"indent": 0,
"keep_closed": 0,
"label": "Chart of Accounts",
"link_to": "Account",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{ {
"child": 0, "child": 0,
"collapsible": 1, "collapsible": 1,
@@ -151,7 +164,7 @@
"collapsible": 1, "collapsible": 1,
"icon": "money-coins-1", "icon": "money-coins-1",
"indent": 1, "indent": 1,
"keep_closed": 1, "keep_closed": 0,
"label": "Payments", "label": "Payments",
"link_type": "DocType", "link_type": "DocType",
"show_arrow": 0, "show_arrow": 0,
@@ -261,7 +274,7 @@
"collapsible": 1, "collapsible": 1,
"icon": "sheet", "icon": "sheet",
"indent": 1, "indent": 1,
"keep_closed": 1, "keep_closed": 0,
"label": "Reports", "label": "Reports",
"link_type": "DocType", "link_type": "DocType",
"show_arrow": 0, "show_arrow": 0,
@@ -299,283 +312,13 @@
"link_type": "Workspace", "link_type": "Workspace",
"show_arrow": 0, "show_arrow": 0,
"type": "Link" "type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "lock-keyhole-open",
"indent": 1,
"keep_closed": 1,
"label": "Opening & Closing",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "COA Importer",
"link_to": "Chart of Accounts Importer",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Opening Invoice Tool",
"link_to": "Opening Invoice Creation Tool",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Accounting Period",
"link_to": "Accounting Period",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "FX Revaluation",
"link_to": "Exchange Rate Revaluation",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Period Closing Voucher",
"link_to": "Period Closing Voucher",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "database",
"indent": 1,
"keep_closed": 1,
"label": "Setup",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Chart of Accounts",
"link_to": "Account",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Chart of Cost Centers",
"link_to": "Cost Center",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Account Category",
"link_to": "Account Category",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Accounting Dimension",
"link_to": "Accounting Dimension",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Currency",
"link_to": "Currency",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Currency Exchange",
"link_to": "Currency Exchange",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Finance Book",
"link_to": "Finance Book",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Mode of Payment",
"link_to": "Mode of Payment",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Payment Term",
"link_to": "Payment Term",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Journal Entry Template",
"link_to": "Journal Entry Template",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Terms and Conditions",
"link_to": "Terms and Conditions",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Company",
"link_to": "Company",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Fiscal Year",
"link_to": "Fiscal Year",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "settings",
"indent": 1,
"keep_closed": 1,
"label": "Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"icon": "",
"indent": 0,
"keep_closed": 0,
"label": "Accounts Settings",
"link_to": "Accounts Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Currency Exchange Settings",
"link_to": "Currency Exchange Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Repost Accounting Ledger Settings",
"link_to": "Repost Accounting Ledger Settings",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
} }
], ],
"modified": "2026-01-10 00:06:13.234927", "modified": "2026-01-26 21:23:15.665712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Invoicing",
"owner": "Administrator", "owner": "Administrator",
"standard": 1, "standard": 1,
"title": "Accounting" "title": "Invoicing"
} }

View File

@@ -0,0 +1,205 @@
{
"app": "erpnext",
"creation": "2026-01-23 11:36:55.020705",
"docstatus": 0,
"doctype": "Workspace Sidebar",
"header_icon": "receipt-text",
"idx": 0,
"items": [
{
"child": 0,
"collapsible": 1,
"icon": "chart",
"indent": 0,
"keep_closed": 0,
"label": "Dashboard",
"link_to": "Payments",
"link_type": "Dashboard",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "money-coins-1",
"indent": 1,
"keep_closed": 0,
"label": "Payments",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Payment Entry",
"link_to": "Payment Entry",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Journal Entry",
"link_to": "Journal Entry",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Payment Request",
"link_to": "Payment Request",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Payment Order",
"link_to": "Payment Order",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Payment Reconciliaition",
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Unreconcile Payment",
"link_to": "Unreconcile Payment",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Process Payment Reconciliation",
"link_to": "Process Payment Reconciliation",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Repost Accounting Ledger",
"link_to": "Repost Accounting Ledger",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Repost Payment Ledger",
"link_to": "Repost Payment Ledger",
"link_type": "DocType",
"show_arrow": 0,
"type": "Link"
},
{
"child": 0,
"collapsible": 1,
"icon": "sheet",
"indent": 1,
"keep_closed": 1,
"label": "Reports",
"link_type": "DocType",
"show_arrow": 0,
"type": "Section Break"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Accounts Receivable",
"link_to": "Accounts Receivable",
"link_type": "Report",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Accounts Payable",
"link_to": "Accounts Payable",
"link_type": "Report",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "General Ledger",
"link_to": "General Ledger",
"link_type": "Report",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Trial Balance",
"link_to": "Trial Balance",
"link_type": "Report",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Financial Reports",
"link_to": "Financial Reports",
"link_type": "Workspace",
"show_arrow": 0,
"type": "Link"
}
],
"modified": "2026-01-26 21:25:37.877020",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payments",
"owner": "Administrator",
"standard": 1,
"title": "Payments"
}