Compare commits

..

29 Commits

Author SHA1 Message Date
Khushi Rawat
d3cd887f5e Merge pull request #51666 from aerele/fix-asset-value-adjustment-cancel
fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled
2026-01-13 12:38:44 +05:30
Navin-S-R
d65cd605a1 fix: move validation to before_cancel 2026-01-13 12:16:52 +05:30
Ankush Menat
6ec41fa47e build: Bump dev version 2026-01-13 12:15:21 +05:30
Khushi Rawat
d879a91165 Merge pull request #51509 from khushi8112/fix-test-cases
fix: use system configured float precision for depreciation rate
2026-01-13 11:58:58 +05:30
Khushi Rawat
d21cfae095 Merge pull request #51363 from aerele/asset-partial-sales
fix(asset): handle partial asset sales by splitting remaining quantity
2026-01-13 11:54:20 +05:30
Mihir Kandoi
be5f2b6cf0 Merge pull request #51650 from mihir-kandoi/v16-prep 2026-01-13 11:06:02 +05:30
ruthra kumar
37b3a22825 Merge pull request #51412 from ljain112/fix-tds-customer
fix(tds): correct tax logic for customer
2026-01-13 11:05:31 +05:30
Mihir Kandoi
bb307dec0a chore: add v14
remove when EOL reached
2026-01-13 11:04:17 +05:30
Nabin Hait
3bc58fb46f fix: Redirect to Desktop after signup (#51696) 2026-01-12 19:20:27 +05:30
Navin-S-R
73b038084b fix: prevent manual cancellation of the linked Revaluation Journal Entry 2026-01-12 18:18:22 +05:30
Navin-S-R
eeb6d0e9bf fix: remove the redundant purchase receipt submit 2026-01-12 16:45:04 +05:30
Navin-S-R
ca97f34092 fix: use new_asset instead of asset_doc when checking values after splitting 2026-01-12 16:36:34 +05:30
Navin-S-R
500c44e3f5 fix: ignore permissions when cancelling revaluation journal entry 2026-01-11 21:30:09 +05:30
Navin-S-R
5f00239bba refactor(journal entry): replace raw SQL with query builder to unlink asset value adjustment 2026-01-11 19:25:22 +05:30
Navin-S-R
b1704ccef1 fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled 2026-01-11 19:20:01 +05:30
Mihir Kandoi
4987b2fe26 ci: ignore ci folder for tests 2026-01-10 17:01:04 +05:30
Mihir Kandoi
7e7e83440f ci: version 16 related changes 2026-01-10 16:43:39 +05:30
khushi8112
c0a85faa68 test: set up float precision 2026-01-06 23:49:21 +05:30
khushi8112
825e3717ca fix: do not update float precision on setup 2026-01-06 14:41:36 +05:30
khushi8112
007258d657 refactor: modify test cases to handle float precision rounded to 2 decimals 2026-01-06 14:39:31 +05:30
khushi8112
8d186d6b3f fix: use correct test class 2026-01-05 16:47:32 +05:30
khushi8112
1296829b9c fix(test): Use the system-configured float precision 2026-01-05 16:44:06 +05:30
ljain112
86b0f67dbc fix(tds): correct tax logic for customer 2025-12-31 14:26:22 +05:30
Navin-S-R
4adeaedfde test: validate asset split for auto created asset from purchase voucher 2025-12-30 16:29:46 +05:30
Navin-S-R
23b094f151 fix(asset): handle same asset being sold in multiple line items in sales invoice 2025-12-30 14:47:28 +05:30
Navin-S-R
e7e6567792 fix(asset): skip purchase document validation while splitting existing asset 2025-12-30 12:09:37 +05:30
Navin-S-R
9eeccb765d test: validate asset partial sales 2025-12-29 22:14:26 +05:30
Navin-S-R
a88fe2ecab fix: refactor older testcases 2025-12-29 15:53:31 +05:30
Navin-S-R
9a2710b9d7 fix(asset): handle partial asset sales by splitting remaining quantity 2025-12-29 15:40:46 +05:30
117 changed files with 3377 additions and 10529 deletions

View File

@@ -11,6 +11,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '.github/**'
workflow_dispatch:
permissions:
@@ -113,8 +114,8 @@ jobs:
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
wget https://frappe.io/files/erpnext-v14.sql.gz
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
wget https://erpnext.com/files/v13-erpnext.sql.gz
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
@@ -142,6 +143,7 @@ jobs:
bench --site test_site migrate
}
update_to_version 14 3.11
update_to_version 15 3.13
echo "Updating to latest version"

View File

@@ -2,7 +2,7 @@ name: Generate Semantic Release
on:
push:
branches:
- version-16
- version-13
permissions:
contents: read

View File

@@ -13,6 +13,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '.github/**'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"

View File

@@ -50,13 +50,13 @@ pull_request_rules:
- version-15-hotfix
assignees:
- "{{ author }}"
- name: backport to version-16-beta
- name: backport to version-16-hotfix
conditions:
- label="backport version-16-beta"
- label="backport version-16-hotfix"
actions:
backport:
branches:
- version-16-beta
- version-16-hotfix
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review

View File

@@ -1,5 +1,5 @@
{
"branches": ["version-16"],
"branches": ["version-13"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
@@ -21,4 +21,4 @@
],
"@semantic-release/github"
]
}
}

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.3.0"
__version__ = "17.0.0-dev"
def get_default_company(user=None):

View File

@@ -1,50 +0,0 @@
{
"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

@@ -281,7 +281,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.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"

View File

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

View File

@@ -42,4 +42,8 @@ frappe.ui.form.on("Bank Account", {
});
}
},
is_company_account: function (frm) {
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
},
});

View File

@@ -52,7 +52,6 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company Account",
"mandatory_depends_on": "is_company_account",
"options": "Account"
},
{
@@ -99,7 +98,6 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"mandatory_depends_on": "is_company_account",
"options": "Company"
},
{
@@ -254,7 +252,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2026-01-20 00:46:16.633364",
"modified": "2025-08-29 12:32:01.081687",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",

View File

@@ -51,29 +51,25 @@ class BankAccount(Document):
delete_contact_and_address("Bank Account", self.name)
def validate(self):
self.validate_is_company_account()
self.validate_company()
self.validate_account()
self.update_default_bank_account()
def validate_is_company_account(self):
if self.is_company_account:
if not self.company:
frappe.throw(_("Company is mandatory for company account"))
if not self.account:
frappe.throw(_("Company Account is mandatory"))
self.validate_account()
def validate_account(self):
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
if self.account:
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
)
def validate_company(self):
if self.is_company_account and not self.company:
frappe.throw(_("Company is mandatory for company account"))
def update_default_bank_account(self):
if self.is_default and not self.disabled:

View File

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

View File

@@ -400,16 +400,6 @@ frappe.ui.form.on("Payment Entry", {
);
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) {
@@ -1114,7 +1104,7 @@ frappe.ui.form.on("Payment Entry", {
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
paid_amount: flt(paid_amount),
paid_amount: paid_amount,
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});

View File

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

View File

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

View File

@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
if 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):

View File

@@ -115,21 +115,18 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (cint(doc.update_stock) != 1) {
if (!is_delivered_by_supplier) {
const should_create_delivery_note = doc.items.some(
(item) =>
item.qty - item.delivered_qty > 0 &&
!item.scio_detail &&
!item.dn_detail &&
!item.delivered_by_supplier
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
var from_delivery_note = false;
from_delivery_note = this.frm.doc.items.some(function (item) {
return item.delivery_note ? true : false;
});
if (!from_delivery_note && !is_delivered_by_supplier) {
this.frm.add_custom_button(
__("Delivery"),
this.frm.cscript["Make Delivery Note"],
__("Create")
);
if (should_create_delivery_note) {
this.frm.add_custom_button(
__("Delivery Note"),
this.frm.cscript["Make Delivery Note"],
__("Create")
);
}
}
}

View File

@@ -778,7 +778,8 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval:!doc.is_return",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -792,6 +793,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
"no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1
},
@@ -2090,7 +2092,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
"fieldname": "section_break_104",
"fieldtype": "Section Break"
},
@@ -2304,7 +2306,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-12-24 18:29:50.242618",
"modified": "2025-10-09 14:48:59.472826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -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,
@@ -352,22 +353,10 @@ class SalesInvoice(SellingController):
self.is_opening = "No"
self.set_against_income_account()
if self.is_return and not self.return_against and self.timesheets:
frappe.throw(_("Direct return is not allowed for Timesheet."))
if not self.is_return:
self.validate_time_sheets_are_submitted()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if self.is_return and self.return_against:
for row in self.timesheets:
if row.billing_hours:
row.billing_hours = -abs(row.billing_hours)
if row.billing_amount:
row.billing_amount = -abs(row.billing_amount)
if self.is_return:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -480,6 +469,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
@@ -496,7 +487,7 @@ class SalesInvoice(SellingController):
if cint(self.is_pos) != 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
self.update_time_sheet(self.name)
if frappe.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
update_company_current_month_sales(self.company)
@@ -576,7 +567,7 @@ class SalesInvoice(SellingController):
self.check_if_consolidated_invoice()
super().before_cancel()
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
self.update_time_sheet(None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
@@ -816,20 +807,8 @@ class SalesInvoice(SellingController):
for data in timesheet.time_logs:
if (
(self.project and args.timesheet_detail == data.name)
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
or (
not sales_invoice
and data.sales_invoice == self.name
and args.timesheet_detail == data.name
)
or (
self.is_return
and self.return_against
and data.sales_invoice
and data.sales_invoice == self.return_against
and not sales_invoice
and args.timesheet_detail == data.name
)
or (not self.project and not data.sales_invoice)
or (not sales_invoice and data.sales_invoice == self.name)
):
data.sales_invoice = sales_invoice
@@ -869,26 +848,11 @@ class SalesInvoice(SellingController):
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
# Note: This validation is skipped for return invoices
# to allow returns to reference already-billed timesheet details
for data in self.timesheets:
# Handle invoice duplication
if data.time_sheet and data.timesheet_detail:
if sales_invoice := frappe.db.get_value(
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
):
frappe.throw(
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
)
)
if data.time_sheet:
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
if status not in ["Submitted", "Payslip", "Partially Billed"]:
frappe.throw(
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
)
if status not in ["Submitted", "Payslip"]:
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
@@ -1322,12 +1286,7 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if (
not self.is_return
and not self.timesheets
and self.project
and self.is_auto_fetch_timesheet_enabled()
):
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
self.add_timesheet_data()
else:
self.calculate_billing_amount_for_timesheet()
@@ -1402,6 +1361,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()
@@ -2422,10 +2426,7 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center",
},
"postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1
and not doc.scio_detail
and not doc.dn_detail
and doc.qty - doc.delivered_qty > 0,
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {

View File

@@ -2951,60 +2951,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
def test_item_tax_template_change_with_grand_total_discount(self):
"""
Test that when item tax template changes due to discount on Grand Total,
the tax calculations are consistent.
"""
item = create_item("Test Item With Multiple Tax Templates")
item.set("taxes", [])
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500,
},
)
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000,
},
)
item.save()
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Excise Duty - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"rate": 0,
},
)
si.insert()
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
si.apply_discount_on = "Grand Total"
si.discount_amount = 300
si.save()
# Verify template changed to 10%
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
si.submit()
@IntegrationTestCase.change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
discount_account = create_account(

View File

@@ -52,6 +52,7 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -116,16 +117,15 @@
],
"istable": 1,
"links": [],
"modified": "2025-12-23 13:54:17.677187",
"modified": "2024-03-27 13:10:36.562795",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -877,15 +877,11 @@ class ReceivablePayableReport:
else:
entry_date = row.posting_date
row.range0 = 0.0
self.get_ageing_data(entry_date, row)
# ageing buckets should not have amounts if due date is not reached
if getdate(entry_date) > getdate(self.age_as_on):
row.range0 = row.outstanding
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
row.total_due = 0
return
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
@@ -1285,8 +1281,6 @@ class ReceivablePayableReport:
ranges = [*self.ranges, _("Above")]
prev_range_value = 0
self.add_column(label=_("<0"), fieldname="range0", fieldtype="Currency")
self.ageing_column_labels.append(_("<0"))
for idx, curr_range_value in enumerate(ranges):
label = f"{prev_range_value}-{curr_range_value}"
self.add_column(label=label, fieldname="range" + str(idx + 1))
@@ -1302,9 +1296,7 @@ class ReceivablePayableReport:
for row in self.data:
row = frappe._dict(row)
if not cint(row.bold):
values = [flt(row.get("range0", 0), precision)] + [
flt(row.get(f"range{i}", 0), precision) for i in self.range_numbers
]
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
rows.append({"values": values})
self.chart = {

View File

@@ -18,8 +18,6 @@ def execute(filters=None):
dimensions = filters.get("budget_against_filter")
else:
dimensions = get_budget_dimensions(filters)
if not dimensions:
return columns, [], None, None
budget_records = get_budget_records(filters, dimensions)
budget_map = build_budget_map(budget_records, filters)

View File

@@ -219,18 +219,13 @@ def get_net_profit(
has_value = False
gross_income_roots = [row for row in (gross_income or []) if not flt(row.get("indent"))]
non_gross_income_roots = [row for row in (non_gross_income or []) if not flt(row.get("indent"))]
gross_expense_roots = [row for row in (gross_expense or []) if not flt(row.get("indent"))]
non_gross_expense_roots = [row for row in (non_gross_expense or []) if not flt(row.get("indent"))]
for period in period_list:
key = period if consolidated else period.key
gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0
gross_income_for_period = sum(flt(row.get(key, 0)) for row in gross_income_roots)
non_gross_income_for_period = sum(flt(row.get(key, 0)) for row in non_gross_income_roots)
gross_expense_for_period = sum(flt(row.get(key, 0)) for row in gross_expense_roots)
non_gross_expense_for_period = sum(flt(row.get(key, 0)) for row in non_gross_expense_roots)
gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0
total_income = gross_income_for_period + non_gross_income_for_period
total_expense = gross_expense_for_period + non_gross_expense_for_period

View File

@@ -105,7 +105,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
{
"total_tax": total_tax,
"total_other_charges": total_other_charges,
"total": d.base_net_amount + total_tax + total_other_charges,
"total": d.base_net_amount + total_tax,
"currency": company_currency,
}
)

View File

@@ -547,7 +547,6 @@ def reconcile_against_document(
doc.make_advance_gl_entries(entry=row)
else:
_delete_pl_entries(voucher_type, voucher_no)
_delete_adv_pl_entries(voucher_type, voucher_no)
gl_map = doc.build_gl_map()
# Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference
@@ -1947,7 +1946,6 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
account=gle.account,
party_type=gle.party_type,
party=gle.party,
project=gle.project,
cost_center=gle.cost_center,
finance_book=gle.finance_book,
due_date=gle.due_date,

View File

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

View File

@@ -111,7 +111,15 @@ frappe.ui.form.on("Asset", {
frm.add_custom_button(
__("Sell Asset"),
function () {
frm.trigger("make_sales_invoice");
frm.trigger("sell_asset");
},
__("Manage")
);
frm.add_custom_button(
__("Repair Asset"),
function () {
frm.trigger("create_asset_repair");
},
__("Manage")
);
@@ -147,14 +155,6 @@ frappe.ui.form.on("Asset", {
},
__("Manage")
);
frm.add_custom_button(
__("Repair Asset"),
function () {
frm.trigger("create_asset_repair");
},
__("Manage")
);
}
if (!frm.doc.calculate_depreciation) {
@@ -521,22 +521,6 @@ frappe.ui.form.on("Asset", {
frm.trigger("toggle_reference_doc");
},
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: {
@@ -585,6 +569,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");

View File

@@ -244,8 +244,6 @@ class Asset(AccountsController):
def before_submit(self):
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."))
def on_submit(self):
@@ -484,6 +482,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"),
@@ -591,9 +592,7 @@ class Asset(AccountsController):
def set_depreciation_rate(self):
for d in self.get("finance_books"):
self.validate_asset_finance_books(d)
d.rate_of_depreciation = flt(
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
d.rate_of_depreciation = self.get_depreciation_rate(d, on_validate=True)
def validate_asset_finance_books(self, row):
row.expected_value_after_useful_life = flt(
@@ -983,7 +982,7 @@ class Asset(AccountsController):
if isinstance(args, str):
args = json.loads(args)
rate_field_precision = frappe.get_precision(args.doctype, "rate_of_depreciation") or 2
rate_field_precision = frappe.get_single_value("System Settings", "float_precision") or 2
if args.get("depreciation_method") == "Double Declining Balance":
return self.get_double_declining_balance_rate(args, rate_field_precision)
@@ -1085,7 +1084,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 +1099,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 +1379,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)

View File

@@ -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,8 +702,142 @@ 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):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._old_float_precision = frappe.db.get_single_value("System Settings", "float_precision")
frappe.db.set_single_value("System Settings", "float_precision", 2)
@classmethod
def tearDownClass(cls):
frappe.db.set_single_value("System Settings", "float_precision", cls._old_float_precision)
super().tearDownClass()
def test_schedule_for_straight_line_method(self):
asset = create_asset(
calculate_depreciation=1,
@@ -797,9 +935,9 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.status, "Draft")
expected_schedules = [
["2030-12-31", 66667.00, 66667.00],
["2031-12-31", 22222.11, 88889.11],
["2032-12-31", 1110.89, 90000.0],
["2030-12-31", 66670.0, 66670.0],
["2031-12-31", 22221.11, 88891.11],
["2032-12-31", 1108.89, 90000.0],
]
schedules = [
@@ -825,7 +963,7 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.status, "Draft")
expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
expected_schedules = [["2031-12-31", 33335.0, 83335.0], ["2032-12-31", 6665.0, 90000.0]]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
@@ -943,12 +1081,12 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2022-02-28", 337.72, 337.72],
["2022-03-31", 675.45, 1013.17],
["2022-04-30", 675.45, 1688.62],
["2022-05-31", 675.45, 2364.07],
["2022-06-30", 675.45, 3039.52],
["2022-07-15", 1960.48, 5000.0],
["2022-02-28", 337.71, 337.71],
["2022-03-31", 675.42, 1013.13],
["2022-04-30", 675.42, 1688.55],
["2022-05-31", 675.42, 2363.97],
["2022-06-30", 675.42, 3039.39],
["2022-07-15", 1960.61, 5000.0],
]
schedules = [
@@ -1691,71 +1829,6 @@ class TestDepreciationBasics(AssetSetup):
pr.submit()
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):
gl_entry = frappe.qb.DocType("GL Entry")

View File

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

View File

@@ -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

View File

@@ -1330,55 +1330,6 @@ class TestPurchaseOrder(IntegrationTestCase):
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 50)
def test_multiple_advances_against_purchase_order_are_allocated_across_partial_purchase_invoices(self):
# step - 1: create PO
po = create_purchase_order(qty=10, rate=10)
# step - 2: create first partial advance payment
pe1 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
pe1.reference_no = "1"
pe1.reference_date = nowdate()
pe1.paid_amount = 50
pe1.references[0].allocated_amount = 50
pe1.save(ignore_permissions=True).submit()
# check first advance paid against PO
po.reload()
self.assertEqual(po.advance_paid, 50)
# step - 3: create first PI for partial qty and allocate first advance
pi_1 = make_pi_from_po(po.name)
pi_1.update_stock = 1
pi_1.allocate_advances_automatically = 1
pi_1.items[0].qty = 5
pi_1.save(ignore_permissions=True).submit()
# step - 4: create second advance payment for remaining
pe2 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
pe2.reference_no = "2"
pe2.reference_date = nowdate()
pe2.paid_amount = 50
pe2.references[0].allocated_amount = 50
pe2.save(ignore_permissions=True).submit()
# check second advance paid against PO
po.reload()
self.assertEqual(po.advance_paid, 100)
# step - 5: create second PI for remaining qty and allocate second advance
pi_2 = make_pi_from_po(po.name)
pi_2.update_stock = 1
pi_2.allocate_advances_automatically = 1
pi_2.save(ignore_permissions=True).submit()
# check PO and PI status
po.reload()
pi_1.reload()
pi_2.reload()
self.assertEqual(pi_1.status, "Paid")
self.assertEqual(pi_2.status, "Paid")
self.assertEqual(po.status, "Completed")
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -383,7 +383,7 @@
},
{
"fieldname": "primary_address",
"fieldtype": "Text Editor",
"fieldtype": "Text",
"label": "Primary Address",
"read_only": 1
},
@@ -500,7 +500,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-01-16 15:56:31.139206",
"modified": "2025-06-29 05:30:50.398653",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -62,7 +62,7 @@ class Supplier(TransactionBase):
portal_users: DF.Table[PortalUser]
prevent_pos: DF.Check
prevent_rfqs: DF.Check
primary_address: DF.TextEditor | None
primary_address: DF.Text | None
release_date: DF.Date | None
represents_company: DF.Link | None
supplier_details: DF.Text | None

View File

@@ -16,14 +16,6 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : "";
});
this.frm.set_query("warehouse", "items", (doc, cdt, cdn) => {
return {
filters: {
company: doc.company,
is_group: 0,
},
};
});
super.setup();
}

View File

@@ -187,8 +187,9 @@ class AccountsController(TransactionBase):
msg = ""
if self.get("update_outstanding_for_self"):
msg = _(
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox."
msg = (
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
"uncheck '{2}' checkbox. <br><br>Or"
).format(
frappe.bold(document_type),
get_link_to_form(self.doctype, self.get("return_against")),
@@ -199,8 +200,8 @@ class AccountsController(TransactionBase):
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
):
self.update_outstanding_for_self = 1
msg = _(
"The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice."
msg = (
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
).format(
against_voucher_outstanding,
get_link_to_form(self.doctype, self.get("return_against")),
@@ -208,11 +209,11 @@ class AccountsController(TransactionBase):
)
if msg:
msg += "<br><br>" + _("You can use {0} to reconcile against {1} later.").format(
msg += " you can use {} tool to reconcile against {} later.".format(
get_link_to_form("Payment Reconciliation"),
get_link_to_form(self.doctype, self.get("return_against")),
)
frappe.msgprint(msg)
frappe.msgprint(_(msg))
def validate(self):
if not self.get("is_return") and not self.get("is_debit_note"):

View File

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

View File

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

View File

@@ -166,46 +166,29 @@ class SubcontractingController(StockController):
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
)
if self.doctype != "Subcontracting Receipt":
order_item_doctype = (
if self.doctype != "Subcontracting Receipt" and item.qty > flt(
get_pending_subcontracted_quantity(
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"
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":
item.amount = item.qty * item.rate
@@ -627,9 +610,7 @@ class SubcontractingController(StockController):
and self.doctype != "Subcontracting Inward Order"
):
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item") and self.get(
"customer_warehouse"
):
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item"):
row.warehouse = self.customer_warehouse
def __set_alternative_item(self, bom_item):
@@ -1350,7 +1331,9 @@ def get_item_details(items):
def get_pending_subcontracted_quantity(doctype, name):
table = frappe.qb.DocType(doctype)
table = frappe.qb.DocType(
"Purchase Order Item" if doctype == "Subcontracting Order" else "Sales Order Item"
)
query = (
frappe.qb.from_(table)
.select(table.name, table.stock_qty, table.subcontracted_qty)

View File

@@ -720,7 +720,6 @@ class SubcontractingInwardController:
item.db_set("scio_detail", scio_rm.name)
if data:
precision = self.precision("customer_provided_item_cost", "items")
result = frappe.get_all(
"Subcontracting Inward Order Received Item",
filters={
@@ -735,17 +734,10 @@ class SubcontractingInwardController:
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
case_expr_qty, case_expr_rate = Case(), Case()
for d in result:
current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
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.received_qty += (
data[d.name].transfer_qty if self._action == "submit" else -data[d.name].transfer_qty
)
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:
deleted_docs.append(d.name)

View File

@@ -39,23 +39,17 @@ class calculate_taxes_and_totals:
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
return items
def calculate(self, ignore_tax_template_validation=False):
def calculate(self):
if not len(self.doc.items):
return
self.discount_amount_applied = False
self.need_recomputation = False
self.ignore_tax_template_validation = ignore_tax_template_validation
self._calculate()
if self.doc.meta.get_field("discount_amount"):
self.set_discount_amount()
self.apply_discount_amount()
if not ignore_tax_template_validation and self.need_recomputation:
return self.calculate(ignore_tax_template_validation=True)
# Update grand total as per cash and non trade discount
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount
@@ -85,9 +79,6 @@ class calculate_taxes_and_totals:
self.calculate_total_net_weight()
def validate_item_tax_template(self):
if self.ignore_tax_template_validation:
return
if self.doc.get("is_return") and self.doc.get("return_against"):
return
@@ -131,10 +122,6 @@ class calculate_taxes_and_totals:
)
)
# For correct tax_amount calculation re-computation is required
if self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total":
self.need_recomputation = True
def update_item_tax_map(self):
for item in self.doc.items:
item.item_tax_rate = get_item_tax_map(

View File

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

View File

@@ -0,0 +1,18 @@
{
"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

@@ -1,21 +0,0 @@
{
"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",
"name": "Banking",
"owner": "Administrator",
"parent_icon": "Accounting",
"parent_icon": "Accounts",
"restrict_removal": 0,
"roles": [],
"standard": 1

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
{
"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

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

View File

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

View File

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

View File

@@ -569,7 +569,6 @@ accounting_dimension_doctypes = [
"Payment Request",
"Asset Movement Item",
"Asset Depreciation Schedule",
"Advance Taxes and Charges",
]
get_matching_queries = (

File diff suppressed because it is too large Load Diff

View File

@@ -92,10 +92,6 @@ frappe.ui.form.on("BOM", {
};
});
frm.events.set_company_filters(frm, "project");
frm.events.set_company_filters(frm, "default_source_warehouse");
frm.events.set_company_filters(frm, "default_target_warehouse");
frm.trigger("toggle_fields_for_semi_finished_goods");
},
@@ -108,16 +104,6 @@ frappe.ui.form.on("BOM", {
}
},
set_company_filters: function (frm, fieldname) {
frm.set_query(fieldname, () => {
return {
filters: {
company: frm.doc.company,
},
};
});
},
track_semi_finished_goods(frm) {
frm.trigger("toggle_fields_for_semi_finished_goods");
},
@@ -697,6 +683,8 @@ var get_bom_material_detail = function (doc, cdt, cdn, scrap_items) {
do_not_explode: d.do_not_explode,
},
callback: function (r) {
d = locals[cdt][cdn];
$.extend(d, r.message);
refresh_field("items");
refresh_field("scrap_items");

View File

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

View File

@@ -3725,53 +3725,6 @@ class TestWorkOrder(IntegrationTestCase):
wo = make_wo_order_test_record(item="Top Level Parent")
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):
doctype = frappe.qb.DocType("Stock Reservation Entry")

View File

@@ -829,7 +829,7 @@ erpnext.work_order = {
}
}
if (counter > 0) {
frm.add_custom_button(
var consumption_btn = frm.add_custom_button(
__("Material Consumption"),
function () {
const backflush_raw_materials_based_on =

View File

@@ -502,8 +502,8 @@ class WorkOrder(Document):
def validate_work_order_against_so(self):
# already ordered qty
ordered_qty_against_so = frappe.db.sql(
"""select sum(qty - process_loss_qty) from `tabWork Order`
where production_item = %s and sales_order = %s and docstatus = 1 and status != 'Closed' and name != %s""",
"""select sum(qty) from `tabWork Order`
where production_item = %s and sales_order = %s and docstatus < 2 and status != 'Closed' and name != %s""",
(self.production_item, self.sales_order, self.name),
)[0][0]
@@ -512,13 +512,13 @@ class WorkOrder(Document):
# get qty from Sales Order Item table
so_item_qty = frappe.db.sql(
"""select sum(stock_qty) from `tabSales Order Item`
where parent = %s and item_code = %s and docstatus = 1""",
where parent = %s and item_code = %s""",
(self.sales_order, self.production_item),
)[0][0]
# get qty from Packing Item table
dnpi_qty = frappe.db.sql(
"""select sum(qty) from `tabPacked Item`
where parent = %s and parenttype = 'Sales Order' and item_code = %s and docstatus = 1""",
where parent = %s and parenttype = 'Sales Order' and item_code = %s""",
(self.sales_order, self.production_item),
)[0][0]
# total qty in SO
@@ -530,10 +530,8 @@ class WorkOrder(Document):
if total_qty > so_qty + (allowance_percentage / 100 * so_qty):
frappe.throw(
_("Cannot produce more Item {0} than Sales Order quantity {1} {2}").format(
get_link_to_form("Item", self.production_item),
frappe.bold(so_qty),
frappe.bold(frappe.get_value("Item", self.production_item, "stock_uom")),
_("Cannot produce more Item {0} than Sales Order quantity {1}").format(
self.production_item, so_qty
),
OverProductionError,
)
@@ -770,7 +768,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(
@@ -780,6 +777,7 @@ class WorkOrder(Document):
else:
self.update_work_order_qty_in_so()
self.delete_job_card()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
@@ -2654,9 +2652,6 @@ def get_reserved_qty_for_production(
qty_field = wo_item.required_qty
else:
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.else_(wo_item.required_qty - wo_item.consumed_qty)

View File

@@ -458,4 +458,3 @@ erpnext.patches.v16_0.update_corrected_cancelled_status
erpnext.patches.v16_0.fix_barcode_typo
erpnext.patches.v16_0.set_post_change_gl_entries_on_pos_settings
execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing")
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges

View File

@@ -1,7 +0,0 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
create_accounting_dimensions_for_doctype,
)
def execute():
create_accounting_dimensions_for_doctype(doctype="Advance Taxes and Charges")

View File

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

View File

@@ -7,7 +7,6 @@ import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_to_date, now_datetime, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.projects.doctype.task.test_task import create_task
from erpnext.projects.doctype.timesheet.timesheet import OverlapError, make_sales_invoice
@@ -273,60 +272,6 @@ class TestTimesheet(ERPNextTestSuite):
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def test_partial_billing_and_return(self):
"""
Test Timesheet status transitions during partial billing, full billing,
sales return, and return cancellation.
Scenario:
1. Create a Timesheet with two billable time logs.
2. Create a Sales Invoice billing only one time log → Timesheet becomes Partially Billed.
3. Create another Sales Invoice billing the remaining time log → Timesheet becomes Billed.
4. Create a Sales Return against the second invoice → Timesheet reverts to Partially Billed.
5. Cancel the Sales Return → Timesheet returns to Billed status.
This test ensures Timesheet status is recalculated correctly
across billing and return lifecycle events.
"""
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, is_billable=1, do_not_submit=True)
timesheet_detail = timesheet.append("time_logs", {})
timesheet_detail.is_billable = 1
timesheet_detail.activity_type = "_Test Activity Type"
timesheet_detail.from_time = timesheet.time_logs[0].to_time + datetime.timedelta(minutes=1)
timesheet_detail.hours = 2
timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(
hours=timesheet_detail.hours
)
timesheet.save().submit()
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
sales_invoice.due_date = nowdate()
sales_invoice.timesheets.pop()
sales_invoice.submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Partially Billed")
sales_invoice2 = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
sales_invoice2.due_date = nowdate()
sales_invoice2.submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Billed")
sales_return = make_sales_return(sales_invoice2.name).submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Partially Billed")
sales_return.load_from_db()
sales_return.cancel()
timesheet.load_from_db()
self.assertEqual(timesheet.time_logs[1].sales_invoice, sales_invoice2.name)
self.assertEqual(timesheet.status, "Billed")
def make_timesheet(
employee,
@@ -338,7 +283,6 @@ def make_timesheet(
company=None,
currency=None,
exchange_rate=None,
do_not_submit=False,
):
update_activity_type(activity_type)
timesheet = frappe.new_doc("Timesheet")
@@ -367,8 +311,7 @@ def make_timesheet(
else:
timesheet.save(ignore_permissions=True)
if not do_not_submit:
timesheet.submit()
timesheet.submit()
return timesheet

View File

@@ -91,7 +91,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Billed\nBilled\nPayslip\nCompleted\nCancelled",
"options": "Draft\nSubmitted\nBilled\nPayslip\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1
},
@@ -310,7 +310,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-12-19 13:48:23.453636",
"modified": "2024-03-27 13:10:53.551907",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
@@ -386,9 +386,8 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"title_field": "title"
}
}

View File

@@ -51,9 +51,7 @@ class Timesheet(Document):
per_billed: DF.Percent
sales_invoice: DF.Link | None
start_date: DF.Date | None
status: DF.Literal[
"Draft", "Submitted", "Partially Billed", "Billed", "Payslip", "Completed", "Cancelled"
]
status: DF.Literal["Draft", "Submitted", "Billed", "Payslip", "Completed", "Cancelled"]
time_logs: DF.Table[TimesheetDetail]
title: DF.Data | None
total_billable_amount: DF.Currency
@@ -130,9 +128,6 @@ class Timesheet(Document):
if flt(self.per_billed, self.precision("per_billed")) >= 100.0:
self.status = "Billed"
if 0.0 < flt(self.per_billed, self.precision("per_billed")) < 100.0:
self.status = "Partially Billed"
if self.sales_invoice:
self.status = "Completed"
@@ -438,7 +433,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate})
for time_log in timesheet.time_logs:
if time_log.is_billable and not time_log.sales_invoice:
if time_log.is_billable:
target.append(
"timesheets",
{

View File

@@ -1,10 +1,6 @@
frappe.listview_settings["Timesheet"] = {
add_fields: ["status", "total_hours", "start_date", "end_date"],
get_indicator: function (doc) {
if (doc.status == "Partially Billed") {
return [__("Partially Billed"), "orange", "status,=," + "Partially Billed"];
}
if (doc.status == "Billed") {
return [__("Billed"), "green", "status,=," + "Billed"];
}

View File

Before

Width:  |  Height:  |  Size: 928 B

After

Width:  |  Height:  |  Size: 928 B

View File

@@ -637,12 +637,6 @@ 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
);
// 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 (
["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(
this.frm.doc.doctype
@@ -685,6 +679,11 @@ 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"]);
// Round grand total as per precision

View File

@@ -518,7 +518,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode && !frappe.flags.trigger_from_barcode_scanner) {
if (row.barcode) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
frappe.model.set_value(cdt, cdn, {
item_code: r.message.item_code,
@@ -945,10 +945,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
// Replace all occurences of comma with line feed
item.serial_no = item.serial_no.replace(/,/g, "\n");
item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield);
if (!doc.is_return) {
setTimeout(() => {
me.update_qty(cdt, cdn);
}, 300);
}, 3000);
}
}
}
@@ -1529,8 +1530,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} else if (
this.frm.doc.price_list_currency === this.frm.doc.currency &&
this.frm.doc.plc_conversion_rate &&
flt(this.frm.doc.plc_conversion_rate) != 1 &&
flt(this.frm.doc.plc_conversion_rate) != flt(this.frm.doc.conversion_rate)
cint(this.frm.doc.plc_conversion_rate) != 1 &&
cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)
) {
this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate);
}
@@ -3131,16 +3132,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
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() {
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() {

View File

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

View File

@@ -735,7 +735,6 @@ erpnext.utils.update_child_items = function (opts) {
fieldname: "item_name",
label: __("Item Name"),
read_only: 1,
in_list_view: 1,
},
{
fieldtype: "Link",
@@ -793,7 +792,7 @@ erpnext.utils.update_child_items = function (opts) {
];
if (frm.doc.doctype == "Sales Order" || frm.doc.doctype == "Purchase Order") {
fields.splice(3, 0, {
fields.splice(2, 0, {
fieldtype: "Date",
fieldname: frm.doc.doctype == "Sales Order" ? "delivery_date" : "schedule_date",
in_list_view: 1,
@@ -801,7 +800,7 @@ erpnext.utils.update_child_items = function (opts) {
default: frm.doc.doctype == "Sales Order" ? frm.doc.delivery_date : frm.doc.schedule_date,
reqd: 1,
});
fields.splice(4, 0, {
fields.splice(3, 0, {
fieldtype: "Float",
fieldname: "conversion_factor",
label: __("Conversion Factor"),

View File

@@ -138,6 +138,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => this.set_barcode_uom(row, uom),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
@@ -147,7 +148,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.clean_up(),
() => this.set_barcode_uom(row, uom),
() => this.revert_selector_flag(),
() => resolve(row),
]);

View File

@@ -6,19 +6,6 @@ $(document).on("toolbar_setup", function () {
}
});
$(document).on("desktop_screen", function (event, data) {
data.desktop.add_menu_item({
label: __("Clear Demo Data"),
icon: "trash",
condition: function () {
return frappe.boot.sysdefaults.demo_company;
},
onClick: function () {
return erpnext.demo.clear_demo();
},
});
});
function render_clear_demo_action() {
let demo_action = $(
`<a class="dropdown-item" onclick="return erpnext.demo.clear_demo()">

View File

@@ -157,7 +157,7 @@ erpnext.utils.get_address_display = function (frm, address_field, display_field,
args: { address_dict: frm.doc[address_field] },
callback: function (r) {
if (r.message) {
frm.set_value(display_field, r.message);
frm.set_value(display_field, frappe.utils.html2text(r.message));
}
},
});

View File

@@ -495,30 +495,7 @@ erpnext.sales_common = {
}
}
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
);
}
});
}
}
project() {
let me = this;
if (["Delivery Note", "Sales Invoice", "Sales Order"].includes(this.frm.doc.doctype)) {
if (this.frm.doc.project) {

View File

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

View File

@@ -183,7 +183,6 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Customer Group",
"link_filters": "[[\"Customer Group\", \"is_group\", \"=\", 0]]",
"oldfieldname": "customer_group",
"oldfieldtype": "Link",
"options": "Customer Group",
@@ -336,7 +335,7 @@
},
{
"fieldname": "primary_address",
"fieldtype": "Text Editor",
"fieldtype": "Text",
"label": "Primary Address",
"read_only": 1
},
@@ -626,7 +625,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-01-21 17:23:42.151114",
"modified": "2025-11-25 09:35:56.772949",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -83,7 +83,7 @@ class Customer(TransactionBase):
opportunity_name: DF.Link | None
payment_terms: DF.Link | None
portal_users: DF.Table[PortalUser]
primary_address: DF.TextEditor | None
primary_address: DF.Text | None
prospect_name: DF.Link | None
represents_company: DF.Link | None
sales_team: DF.Table[SalesTeam]
@@ -117,7 +117,6 @@ class Customer(TransactionBase):
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, 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:
count = frappe.db.sql(
"""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer

View File

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

View File

@@ -36,15 +36,6 @@ frappe.ui.form.on("Quotation", {
};
});
frm.set_query("warehouse", "items", (doc, cdt, cdn) => {
return {
filters: {
company: doc.company,
is_group: 0,
},
};
});
frm.set_indicator_formatter("item_code", function (doc) {
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
});

View File

@@ -1220,12 +1220,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
},
freeze: true,
callback: function (r) {
if (r.message.length === 0) {
if (!r.message) {
frappe.msgprint({
title: __("Work Order not created"),
message: __(
"No Items with Bill of Materials to Manufacture or all items already manufactured"
),
message: __("No Items with Bill of Materials to Manufacture"),
indicator: "orange",
});
return;
@@ -1235,24 +1233,19 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
label: __("Items"),
fieldtype: "Table",
fieldname: "items",
cannot_add_rows: true,
description: __("Select BOM and Qty for Production"),
fields: [
{
fieldtype: "Link",
fieldtype: "Read Only",
fieldname: "item_code",
options: "Item",
label: __("Item Code"),
in_list_view: 1,
read_only: 1,
},
{
fieldtype: "Data",
fieldtype: "Read Only",
fieldname: "item_name",
label: __("Item Name"),
in_list_view: 1,
read_only: 1,
fetch_from: "item_code.item_name",
},
{
fieldtype: "Link",
@@ -1278,7 +1271,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
reqd: 1,
label: __("Sales Order Item"),
hidden: 1,
read_only: 1,
},
],
data: r.message,

View File

@@ -1981,10 +1981,6 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
)
]
overproduction_percentage_for_sales_order = (
frappe.get_single_value("Manufacturing Settings", "overproduction_percentage_for_sales_order")
/ 100
)
for table in [so.items, so.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
@@ -1993,12 +1989,12 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
if not for_raw_material_request:
total_work_order_qty = flt(
qb.from_(wo)
.select(Sum(wo.qty - wo.process_loss_qty))
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name)
& (wo.sales_order_item == i.name)
& (wo.docstatus == 1)
& (wo.docstatus.lt(2))
& (wo.status != "Closed")
)
.run()[0][0]
@@ -2007,10 +2003,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
else:
pending_qty = stock_qty
if not pending_qty:
pending_qty = stock_qty * overproduction_percentage_for_sales_order
if pending_qty > 0 and i.item_code not in product_bundle_parents:
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,

View File

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

View File

@@ -651,9 +651,6 @@ erpnext.PointOfSale.Controller = class {
async on_cart_update(args) {
frappe.dom.freeze();
if (this.frm.doc.set_warehouse !== this.settings.warehouse) {
this.frm.set_value("set_warehouse", this.settings.warehouse);
}
let item_row = undefined;
try {
let { field, value, item } = args;

View File

@@ -11,16 +11,7 @@ from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import (
add_months,
cint,
formatdate,
get_first_day,
get_last_day,
get_link_to_form,
get_timestamp,
today,
)
from frappe.utils import add_months, cint, formatdate, get_first_day, get_link_to_form, get_timestamp, today
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
@@ -875,41 +866,31 @@ def install_country_fixtures(company, country):
def update_company_current_month_sales(company):
"""Update Company's Total Monthly Sales.
from_date = get_first_day(today())
to_date = get_first_day(add_months(from_date, 1))
Postgres compatibility:
- Avoid MariaDB-only DATE_FORMAT().
- Use a date range for the current month instead (portable + index-friendly).
"""
# Local imports so you don't have to touch file-level imports
from frappe.query_builder.functions import Sum
start_date = get_first_day(today())
end_date = get_last_day(today())
si = frappe.qb.DocType("Sales Invoice")
total_monthly_sales = (
frappe.qb.from_(si)
.select(Sum(si.base_grand_total))
.where(
(si.docstatus == 1)
& (si.company == company)
& (si.posting_date >= start_date)
& (si.posting_date <= end_date)
)
).run(pluck=True)[0] or 0
# Fieldname in standard ERPNext is `total_monthly_sales`
frappe.db.set_value(
"Company",
company,
"total_monthly_sales",
total_monthly_sales,
update_modified=False,
results = frappe.db.sql(
"""
SELECT
SUM(base_grand_total) AS total,
DATE_FORMAT(posting_date, '%%m-%%Y') AS month_year
FROM
`tabSales Invoice`
WHERE
posting_date >= %s
AND posting_date < %s
AND docstatus = 1
AND company = %s
GROUP BY
month_year
""",
(from_date, to_date, company),
as_dict=True,
)
monthly_total = results[0]["total"] if len(results) > 0 else 0
frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total)
def update_company_monthly_sales(company):
"""Cache past year monthly sales of every company based on sales invoices"""

View File

@@ -1,2 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
//--------- ONLOAD -------------
cur_frm.cscript.onload = function (doc, cdt, cdn) {};
cur_frm.cscript.refresh = function (doc, cdt, cdn) {};

View File

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

View File

@@ -53,14 +53,12 @@ def get_stock_value_by_item_group(company):
.inner_join(item_doctype)
.on(doctype.item_code == item_doctype.name)
.select(item_doctype.item_group, stock_value.as_("stock_value"))
.where(doctype.warehouse.isin(warehouses))
.groupby(item_doctype.item_group)
.orderby(stock_value, order=frappe.qb.desc)
.limit(10)
)
if warehouses:
query = query.where(doctype.warehouse.isin(warehouses))
results = query.run(as_dict=True)
labels = []

View File

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

View File

@@ -116,11 +116,6 @@ frappe.ui.form.on("Item", {
},
__("View")
);
frm.toggle_display(
["opening_stock"],
frappe.model.can_create("Stock Entry") && frappe.model.can_write("Stock Entry")
);
}
if (frm.doc.is_fixed_asset) {
@@ -244,8 +239,6 @@ frappe.ui.form.on("Item", {
},
};
});
frm.toggle_display(["standard_rate"], frappe.model.can_create("Item Price"));
},
validate: function (frm) {
@@ -1070,7 +1063,7 @@ frappe.tour["Item"] = [
fieldname: "valuation_rate",
title: "Valuation Rate",
description: __(
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.erpnext.com/docs/v13/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
),
},
{

View File

@@ -232,25 +232,8 @@ class Item(Document):
cint(frappe.get_single_value("Stock Settings", "clean_description_html"))
and self.description != self.item_name # perf: Avoid cleaning up a fallback
):
old_desc = 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):
if self.is_customer_provided_item:
if self.is_purchase_item:

View File

@@ -437,7 +437,7 @@ def get_vendor_invoices(doctype, txt, searchfield, start, page_len, filters):
query = get_vendor_invoice_query(filters)
if txt:
query = query.where(frappe.qb.DocType(doctype).name.like(f"%{txt}%"))
query = query.where(doctype.name.like(f"%{txt}%"))
if start:
query = query.limit(page_len).offset(start)

View File

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

View File

@@ -266,7 +266,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
);
}
cur_frm.add_custom_button(
__("Sample Retention Stock Entry"),
__("Retention Stock Entry"),
this.make_retention_stock_entry,
__("Create")
);

View File

@@ -4849,193 +4849,6 @@ class TestPurchaseReceipt(IntegrationTestCase):
self.assertEqual(return_entry.items[0].qty, -2)
self.assertEqual(return_entry.items[0].rejected_qty, 0) # 3-3=0
def test_do_not_use_batchwise_valuation_with_fifo(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = make_item(
"Test Item Do Not Use Batchwise Valuation with FIFO",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BN-TESTDNUBVWF-.#####",
"valuation_method": "FIFO",
},
).name
doc = frappe.new_doc("Batch")
doc.update(
{
"batch_id": "BN-TESTDNUBVWF-00001",
"item": item_code,
}
).insert()
doc.db_set("use_batchwise_valuation", 0)
doc.reload()
self.assertTrue(doc.use_batchwise_valuation == 0)
doc = frappe.new_doc("Batch")
doc.update(
{
"batch_id": "BN-TESTDNUBVWF-00002",
"item": item_code,
}
).insert()
self.assertTrue(doc.use_batchwise_valuation == 1)
warehouse = "_Test Warehouse - _TC"
make_stock_entry(
item_code=item_code,
qty=10,
rate=100,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
se1 = make_stock_entry(
item_code=item_code,
qty=10,
rate=200,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se1.name,
},
"stock_queue",
)
stock_queue = frappe.parse_json(stock_queue)
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
se2 = make_stock_entry(
item_code=item_code,
qty=10,
rate=2,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00002",
use_serial_batch_fields=1,
)
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se2.name,
},
"stock_queue",
)
stock_queue = frappe.parse_json(stock_queue)
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
se3 = make_stock_entry(
item_code=item_code,
qty=20,
source=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
ste_details = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se3.name,
},
["stock_queue", "stock_value_difference"],
as_dict=1,
)
stock_queue = frappe.parse_json(ste_details.stock_queue)
self.assertEqual(stock_queue, [])
self.assertEqual(ste_details.stock_value_difference, 3000 * -1)
se4 = make_stock_entry(
item_code=item_code,
qty=20,
rate=0,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
do_not_submit=1,
)
se4.items[0].basic_rate = 0.0
se4.items[0].allow_zero_valuation_rate = 1
se4.submit()
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se4.name,
},
"stock_queue",
)
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():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -12,7 +12,7 @@ import frappe.query_builder.functions
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import Concat_ws, Locate, Sum
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
cstr,
@@ -576,12 +576,14 @@ class SerialandBatchBundle(Document):
d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no)))
precision = d.precision("qty")
available_qty = flt(sn_obj.available_qty.get(d.batch_no), precision)
if self.docstatus == 1:
available_qty += flt(d.qty, precision)
for field in ["available_qty", "total_qty"]:
value = getattr(sn_obj, field)
available_qty = flt(value.get(d.batch_no), precision)
if self.docstatus == 1:
available_qty += flt(d.qty, precision)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty, field)
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -594,8 +596,8 @@ class SerialandBatchBundle(Document):
}
)
def validate_negative_batch(self, batch_no, available_qty):
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
def validate_negative_batch(self, batch_no, available_qty, field=None):
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty, field=field):
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
has negative stock
of quantity {bold(available_qty)} in the
@@ -603,7 +605,7 @@ class SerialandBatchBundle(Document):
frappe.throw(_(msg), BatchNegativeStockError)
def is_stock_reco_for_valuation_adjustment(self, available_qty):
def is_stock_reco_for_valuation_adjustment(self, available_qty, field=None):
if (
self.voucher_type == "Stock Reconciliation"
and self.type_of_transaction == "Outward"
@@ -611,6 +613,7 @@ class SerialandBatchBundle(Document):
and (
abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
== abs(available_qty)
or field == "total_qty"
)
):
return True
@@ -709,16 +712,17 @@ class SerialandBatchBundle(Document):
is_packed_item = True
stock_queue = []
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
batches = []
if prev_sle and prev_sle.stock_queue:
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
if prev_sle and prev_sle.stock_queue and parse_json(prev_sle.stock_queue):
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
@@ -745,7 +749,7 @@ class SerialandBatchBundle(Document):
if d.qty:
d.stock_value_difference = flt(d.qty) * d.incoming_rate
if valuation_method == "FIFO" and d.batch_no in batches and d.incoming_rate is not None:
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
stock_queue.append([d.qty, d.incoming_rate])
d.stock_queue = json.dumps(stock_queue)
@@ -1341,7 +1345,6 @@ class SerialandBatchBundle(Document):
def on_submit(self):
self.validate_docstatus()
self.validate_serial_nos_inventory()
self.validate_batch_quantity()
def validate_docstatus(self):
for row in self.entries:
@@ -1435,106 +1438,6 @@ class SerialandBatchBundle(Document):
def on_cancel(self):
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):
if self.voucher_type == "POS Invoice":
@@ -3083,15 +2986,7 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
def get_stock_ledgers_for_serial_nos(kwargs):
"""
Fetch stock ledger entries based on various filters.
:param kwargs: Filters including posting_datetime, creation, warehouse, item_code, serial_nos, ignore_voucher_detail_no, voucher_no. Joins with Serial and Batch Entry table to filter based on serial numbers.
:return: List of stock ledger entries as dictionaries.
:rtype: list[dict]
"""
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
serial_batch_entry = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(stock_ledger_entry)
@@ -3118,7 +3013,7 @@ def get_stock_ledgers_for_serial_nos(kwargs):
query = query.where(timestamp_condition)
for field in ["warehouse", "item_code"]:
for field in ["warehouse", "item_code", "serial_no"]:
if not kwargs.get(field):
continue
@@ -3127,27 +3022,6 @@ def get_stock_ledgers_for_serial_nos(kwargs):
else:
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
serial_nos = kwargs.get("serial_nos") or kwargs.get("serial_no")
if serial_nos and not isinstance(serial_nos, list):
serial_nos = [serial_nos]
if serial_nos:
query = (
query.left_join(serial_batch_entry)
.on(stock_ledger_entry.serial_and_batch_bundle == serial_batch_entry.parent)
.distinct()
)
bundle_match = serial_batch_entry.serial_no.isin(serial_nos)
padded_serial_no = Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n")
direct_match = None
for sn in serial_nos:
cond = Locate(f"\n{sn}\n", padded_serial_no) > 0
direct_match = cond if direct_match is None else (direct_match | cond)
query = query.where(bundle_match | direct_match)
if kwargs.ignore_voucher_detail_no:
query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no)

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