diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json
new file mode 100644
index 00000000000..410411aa670
--- /dev/null
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/nz_standard_chart_of_accounts.json
@@ -0,0 +1,449 @@
+{
+ "country_code": "nz",
+ "name": "New Zealand - Chart of Accounts with Account Numbers",
+ "disabled": "No",
+ "tree": {
+ "Application of Funds (Assets)": {
+ "Current Assets": {
+ "Bank Accounts": {
+ "Business Transaction Account": {
+ "account_number": "11011",
+ "account_type": "Bank"
+ },
+ "Business Savings Account": {
+ "account_number": "11012",
+ "account_type": "Bank"
+ },
+ "account_number": "11010",
+ "is_group": 1
+ },
+ "Cash on Hand": {
+ "account_number": "11020",
+ "account_type": "Cash"
+ },
+ "Accounts Receivable": {
+ "Debtors": {
+ "account_number": "11210",
+ "account_type": "Receivable"
+ },
+ "Provision for Doubtful Debts": {
+ "account_number": "11220"
+ },
+ "account_number": "11200",
+ "is_group": 1
+ },
+ "Inventory": {
+ "Stock on Hand": {
+ "account_number": "11311",
+ "account_type": "Stock"
+ },
+ "Work In Progress": {
+ "account_number": "11312",
+ "account_type": "Stock"
+ },
+ "account_number": "11310",
+ "account_type": "Stock",
+ "is_group": 1
+ },
+ "Prepayments": {
+ "Prepayments": {
+ "account_number": "11411"
+ },
+ "Supplier Advances": {
+ "account_number": "11412"
+ },
+ "Deferred Expense": {
+ "account_number": "11413"
+ },
+ "account_number": "11410",
+ "is_group": 1
+ },
+ "GST Receivable": {
+ "account_number": "11510",
+ "account_type": "Tax"
+ },
+ "Income Tax Receivable": {
+ "account_number": "11520",
+ "account_type": "Tax"
+ },
+ "account_number": "11000",
+ "is_group": 1
+ },
+ "Fixed Assets": {
+ "Plant & Equipment": {
+ "Plant & Equipment": {
+ "account_number": "16011",
+ "account_type": "Fixed Asset"
+ },
+ "Accumulated Depreciation - Plant & Equipment": {
+ "account_number": "16012",
+ "account_type": "Accumulated Depreciation"
+ },
+ "account_number": "16010",
+ "is_group": 1
+ },
+ "Motor Vehicles": {
+ "Motor Vehicles": {
+ "account_number": "16021",
+ "account_type": "Fixed Asset"
+ },
+ "Accumulated Depreciation - Motor Vehicles": {
+ "account_number": "16022",
+ "account_type": "Accumulated Depreciation"
+ },
+ "account_number": "16020",
+ "is_group": 1
+ },
+ "Office Equipment": {
+ "Office Equipment": {
+ "account_number": "16031",
+ "account_type": "Fixed Asset"
+ },
+ "Accumulated Depreciation - Office Equipment": {
+ "account_number": "16032",
+ "account_type": "Accumulated Depreciation"
+ },
+ "account_number": "16030",
+ "is_group": 1
+ },
+ "Buildings": {
+ "Buildings": {
+ "account_number": "16041",
+ "account_type": "Fixed Asset"
+ },
+ "Accumulated Depreciation - Buildings": {
+ "account_number": "16042",
+ "account_type": "Accumulated Depreciation"
+ },
+ "account_number": "16040",
+ "is_group": 1
+ },
+ "Computer Equipment": {
+ "Computer Equipment": {
+ "account_number": "16051",
+ "account_type": "Fixed Asset"
+ },
+ "Accumulated Depreciation - Computer Equipment": {
+ "account_number": "16052",
+ "account_type": "Accumulated Depreciation"
+ },
+ "account_number": "16050",
+ "is_group": 1
+ },
+ "Capital Work in Progress": {
+ "account_number": "16090",
+ "account_type": "Capital Work in Progress"
+ },
+ "account_number": "16000",
+ "is_group": 1
+ },
+ "account_number": "10000",
+ "root_type": "Asset"
+ },
+ "Source of Funds (Liabilities)": {
+ "Current Liabilities": {
+ "Accounts Payable": {
+ "Creditors": {
+ "account_number": "21010",
+ "account_type": "Payable"
+ },
+ "account_number": "21000",
+ "is_group": 1
+ },
+ "Goods Received Not Invoiced": {
+ "account_number": "21100",
+ "account_type": "Stock Received But Not Billed"
+ },
+ "Asset Received Not Invoiced": {
+ "account_number": "21110",
+ "account_type": "Asset Received But Not Billed"
+ },
+ "Service Received Not Invoiced": {
+ "account_number": "21120",
+ "account_type": "Service Received But Not Billed"
+ },
+ "Accrued Expenses": {
+ "account_number": "21200"
+ },
+ "Wages Payable": {
+ "account_number": "21300"
+ },
+ "PAYE Payable": {
+ "account_number": "22010"
+ },
+ "KiwiSaver Payable": {
+ "account_number": "22020"
+ },
+ "ACC Payable": {
+ "account_number": "22030"
+ },
+ "Credit Cards": {
+ "Business Credit Card": {
+ "account_number": "22110"
+ },
+ "account_number": "22100",
+ "is_group": 1
+ },
+ "Customer Advances": {
+ "account_number": "22200"
+ },
+ "Deferred Revenue": {
+ "account_number": "22210"
+ },
+ "Provisional Account": {
+ "account_number": "22220"
+ },
+ "Tax Liabilities": {
+ "GST Payable": {
+ "account_number": "22310",
+ "account_type": "Tax"
+ },
+ "GST Suspense": {
+ "account_number": "22320",
+ "account_type": "Tax"
+ },
+ "FBT Payable": {
+ "account_number": "22330",
+ "account_type": "Tax"
+ },
+ "Income Tax Payable": {
+ "account_number": "22340",
+ "account_type": "Tax"
+ },
+ "account_number": "22300",
+ "is_group": 1
+ },
+ "account_number": "21500",
+ "is_group": 1
+ },
+ "Non-Current Liabilities": {
+ "Bank Loans": {
+ "Bank Loan": {
+ "account_number": "25011"
+ },
+ "account_number": "25010",
+ "is_group": 1
+ },
+ "Lease Liabilities": {
+ "Lease Liability": {
+ "account_number": "25021"
+ },
+ "account_number": "25020",
+ "is_group": 1
+ },
+ "Shareholder Loans": {
+ "Shareholder Loan": {
+ "account_number": "25031"
+ },
+ "account_number": "25030",
+ "is_group": 1
+ },
+ "account_number": "25000",
+ "is_group": 1
+ },
+ "account_number": "20000",
+ "root_type": "Liability"
+ },
+ "Equity": {
+ "Share Capital": {
+ "account_number": "31010",
+ "account_type": "Equity"
+ },
+ "Drawings": {
+ "account_number": "31020",
+ "account_type": "Equity"
+ },
+ "Current Year Earnings": {
+ "account_number": "35010",
+ "account_type": "Equity"
+ },
+ "Retained Earnings": {
+ "account_number": "35020",
+ "account_type": "Equity"
+ },
+ "account_number": "30000",
+ "root_type": "Equity"
+ },
+ "Income": {
+ "Sales": {
+ "account_number": "41010",
+ "account_type": "Income Account"
+ },
+ "Other Income": {
+ "Interest Income": {
+ "account_number": "47010",
+ "account_type": "Income Account"
+ },
+ "Rounding Gain/Loss": {
+ "account_number": "47020",
+ "account_type": "Income Account"
+ },
+ "Foreign Exchange Gain": {
+ "account_number": "47030",
+ "account_type": "Income Account"
+ },
+ "account_number": "47000",
+ "is_group": 1
+ },
+ "account_number": "40000",
+ "root_type": "Income"
+ },
+ "Expenses": {
+ "Cost of Goods Sold": {
+ "Purchases": {
+ "account_number": "51010",
+ "account_type": "Cost of Goods Sold"
+ },
+ "Freight Inwards": {
+ "account_number": "51020",
+ "account_type": "Expenses Included In Valuation"
+ },
+ "Duty and Landing Costs": {
+ "account_number": "51030",
+ "account_type": "Expenses Included In Valuation"
+ },
+ "Stock Adjustment": {
+ "account_number": "51040",
+ "account_type": "Stock Adjustment"
+ },
+ "Stock Write Off": {
+ "account_number": "51050",
+ "account_type": "Stock Adjustment"
+ },
+ "account_number": "51000",
+ "account_type": "Cost of Goods Sold",
+ "is_group": 1
+ },
+ "Operating Expenses": {
+ "Wages & Salaries": {
+ "account_number": "61010",
+ "account_type": "Expense Account"
+ },
+ "KiwiSaver Employer Contribution": {
+ "account_number": "61020",
+ "account_type": "Expense Account"
+ },
+ "ACC Levies": {
+ "account_number": "61030",
+ "account_type": "Expense Account"
+ },
+ "Rent": {
+ "account_number": "65010",
+ "account_type": "Expense Account"
+ },
+ "Power": {
+ "account_number": "65020",
+ "account_type": "Expense Account"
+ },
+ "Telephone": {
+ "account_number": "66010",
+ "account_type": "Expense Account"
+ },
+ "Insurance": {
+ "account_number": "64010",
+ "account_type": "Expense Account"
+ },
+ "Accounting Fees": {
+ "account_number": "64020",
+ "account_type": "Expense Account"
+ },
+ "Legal Fees": {
+ "account_number": "64030",
+ "account_type": "Expense Account"
+ },
+ "Advertising and Marketing": {
+ "account_number": "65030",
+ "account_type": "Expense Account"
+ },
+ "Repairs and Maintenance": {
+ "account_number": "65040",
+ "account_type": "Expense Account"
+ },
+ "Freight and Courier": {
+ "account_number": "65050",
+ "account_type": "Expense Account"
+ },
+ "Operating Costs": {
+ "account_number": "65060",
+ "account_type": "Expense Account"
+ },
+ "account_number": "60000",
+ "is_group": 1
+ },
+ "Depreciation and Amortisation": {
+ "Depreciation - Plant & Equipment": {
+ "account_number": "62010",
+ "account_type": "Depreciation"
+ },
+ "Depreciation - Motor Vehicles": {
+ "account_number": "62020",
+ "account_type": "Depreciation"
+ },
+ "Depreciation - Office Equipment": {
+ "account_number": "62030",
+ "account_type": "Depreciation"
+ },
+ "Depreciation - Computer Equipment": {
+ "account_number": "62040",
+ "account_type": "Depreciation"
+ },
+ "account_number": "62000",
+ "is_group": 1
+ },
+ "Finance Costs": {
+ "Bank Charges": {
+ "account_number": "67010",
+ "account_type": "Expense Account"
+ },
+ "Interest Expense": {
+ "account_number": "67020",
+ "account_type": "Expense Account"
+ },
+ "Rounding Off": {
+ "account_number": "67030",
+ "account_type": "Round Off"
+ },
+ "Payment Discounts": {
+ "account_number": "67040",
+ "account_type": "Expense Account"
+ },
+ "account_number": "67000",
+ "is_group": 1
+ },
+ "Income Tax Expense": {
+ "account_number": "81010",
+ "account_type": "Expense Account"
+ },
+ "Foreign Exchange": {
+ "Exchange Gain/Loss": {
+ "account_number": "82010",
+ "account_type": "Expense Account"
+ },
+ "Unrealized Exchange Gain/Loss": {
+ "account_number": "82020",
+ "account_type": "Expense Account"
+ },
+ "account_number": "82000",
+ "is_group": 1
+ },
+ "Bad Debts": {
+ "account_number": "83010",
+ "account_type": "Expense Account"
+ },
+ "Write Off": {
+ "account_number": "83020",
+ "account_type": "Expense Account"
+ },
+ "Gain/Loss on Asset Disposal": {
+ "account_number": "83030",
+ "account_type": "Expense Account"
+ },
+ "Expenses Included In Asset Valuation": {
+ "account_number": "84010",
+ "account_type": "Expenses Included In Asset Valuation"
+ },
+ "account_number": "50000",
+ "root_type": "Expense"
+ }
+ }
+}
diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js
index 17cb8d00e48..7ce14b6c235 100644
--- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js
+++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.js
@@ -6,12 +6,14 @@ frappe.provide("erpnext.cheque_print");
frappe.ui.form.on("Cheque Print Template", {
refresh: function (frm) {
if (!frm.doc.__islocal) {
- frm.add_custom_button(
- frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
- function () {
- erpnext.cheque_print.view_cheque_print(frm);
- }
- ).addClass("btn-primary");
+ if (frappe.user.has_role("System Manager")) {
+ frm.add_custom_button(
+ frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
+ function () {
+ erpnext.cheque_print.view_cheque_print(frm);
+ }
+ ).addClass("btn-primary");
+ }
$(frm.fields_dict.cheque_print_preview.wrapper).empty();
diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json
index e9993c34268..155fc43c5f7 100644
--- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json
+++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.json
@@ -1,1102 +1,342 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:bank_name",
- "beta": 0,
- "creation": "2016-05-04 14:35:00.402544",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
+ "actions": [],
+ "allow_bulk_edit": 1,
+ "autoname": "field:bank_name",
+ "creation": "2016-05-04 14:35:00.402544",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "settings",
+ "has_print_format",
+ "primary_settings",
+ "bank_name",
+ "cheque_size",
+ "starting_position_from_top_edge",
+ "cheque_width",
+ "cheque_height",
+ "scanned_cheque",
+ "column_break_5",
+ "is_account_payable",
+ "acc_pay_dist_from_top_edge",
+ "acc_pay_dist_from_left_edge",
+ "message_to_show",
+ "date_and_payer_settings",
+ "date_settings",
+ "date_dist_from_top_edge",
+ "date_dist_from_left_edge",
+ "payer_settings",
+ "payer_name_from_top_edge",
+ "payer_name_from_left_edge",
+ "amount_in_words_and_figure_settings",
+ "html_19",
+ "amt_in_words_from_top_edge",
+ "amt_in_words_from_left_edge",
+ "amt_in_word_width",
+ "amt_in_words_line_spacing",
+ "amount_in_figure",
+ "amt_in_figures_from_top_edge",
+ "amt_in_figures_from_left_edge",
+ "account_number_and_signatory_settings",
+ "account_no_settings",
+ "acc_no_dist_from_top_edge",
+ "acc_no_dist_from_left_edge",
+ "signatory_position",
+ "signatory_from_top_edge",
+ "signatory_from_left_edge",
+ "preview",
+ "cheque_print_preview"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "description": "",
- "fieldname": "settings",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "options": "
\n
All dimensions in centimeter only
\n",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "settings",
+ "fieldtype": "HTML",
+ "options": "\n
All dimensions in centimeter only
\n"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "has_print_format",
- "fieldtype": "Check",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Has Print Format",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "has_print_format",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Has Print Format",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "primary_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Primary Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "primary_settings",
+ "fieldtype": "Section Break",
+ "label": "Primary Settings"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "bank_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Bank Name",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "bank_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Bank Name",
+ "no_copy": 1,
+ "reqd": 1,
+ "unique": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "Regular",
- "fieldname": "cheque_size",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Cheque Size",
- "length": 0,
- "no_copy": 0,
- "options": "\nRegular\nA4",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "Regular",
+ "fieldname": "cheque_size",
+ "fieldtype": "Select",
+ "label": "Cheque Size",
+ "options": "\nRegular\nA4"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "depends_on": "eval:doc.cheque_size==\"A4\"",
- "fieldname": "starting_position_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting position from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.cheque_size==\"A4\"",
+ "fieldname": "starting_position_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Starting position from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "20.00",
- "fieldname": "cheque_width",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Cheque Width",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "20.00",
+ "fieldname": "cheque_width",
+ "fieldtype": "Float",
+ "label": "Cheque Width",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "9.00",
- "fieldname": "cheque_height",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Cheque Height",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "9.00",
+ "fieldname": "cheque_height",
+ "fieldtype": "Float",
+ "label": "Cheque Height",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "",
- "fieldname": "scanned_cheque",
- "fieldtype": "Attach",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Scanned Cheque",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "scanned_cheque",
+ "fieldtype": "Attach",
+ "label": "Scanned Cheque"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "column_break_5",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "1",
- "fieldname": "is_account_payable",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Is Account Payable",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "1",
+ "fieldname": "is_account_payable",
+ "fieldtype": "Check",
+ "label": "Is Account Payable"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "1.00",
- "depends_on": "eval:doc.is_account_payable",
- "fieldname": "acc_pay_dist_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "1.00",
+ "depends_on": "eval:doc.is_account_payable",
+ "fieldname": "acc_pay_dist_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "9.00",
- "depends_on": "eval:doc.is_account_payable",
- "fieldname": "acc_pay_dist_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "9.00",
+ "depends_on": "eval:doc.is_account_payable",
+ "fieldname": "acc_pay_dist_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Distance from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "Acc. Payee",
- "depends_on": "eval:doc.is_account_payable",
- "fieldname": "message_to_show",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Message to show",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "Acc. Payee",
+ "depends_on": "eval:doc.is_account_payable",
+ "fieldname": "message_to_show",
+ "fieldtype": "Data",
+ "label": "Message to show"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "date_and_payer_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "date_and_payer_settings",
+ "fieldtype": "Section Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "date_settings",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Date Settings",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "date_settings",
+ "fieldtype": "HTML",
+ "label": "Date Settings",
+ "options": ""
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "1.00",
- "fieldname": "date_dist_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "1.00",
+ "fieldname": "date_dist_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "15.00",
- "depends_on": "",
- "fieldname": "date_dist_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting location from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "15.00",
+ "fieldname": "date_dist_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Starting location from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "payer_settings",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Payer Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "payer_settings",
+ "fieldtype": "Column Break",
+ "label": "Payer Settings"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "2.00",
- "fieldname": "payer_name_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "2.00",
+ "fieldname": "payer_name_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "3.00",
- "fieldname": "payer_name_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting location from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "3.00",
+ "fieldname": "payer_name_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Starting location from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "amount_in_words_and_figure_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "amount_in_words_and_figure_settings",
+ "fieldtype": "Section Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "html_19",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "html_19",
+ "fieldtype": "HTML",
+ "options": ""
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "3.00",
- "fieldname": "amt_in_words_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "3.00",
+ "fieldname": "amt_in_words_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "4.00",
- "fieldname": "amt_in_words_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting location from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "4.00",
+ "fieldname": "amt_in_words_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Starting location from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "15.00",
- "fieldname": "amt_in_word_width",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Width of amount in word",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "15.00",
+ "fieldname": "amt_in_word_width",
+ "fieldtype": "Float",
+ "label": "Width of amount in word",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "0.50",
- "fieldname": "amt_in_words_line_spacing",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Line spacing for amount in words",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "0.50",
+ "fieldname": "amt_in_words_line_spacing",
+ "fieldtype": "Float",
+ "label": "Line spacing for amount in words",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "amount_in_figure",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Amount In Figure",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "amount_in_figure",
+ "fieldtype": "Column Break",
+ "label": "Amount In Figure"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "3.50",
- "fieldname": "amt_in_figures_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "3.50",
+ "fieldname": "amt_in_figures_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "16.00",
- "fieldname": "amt_in_figures_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting location from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "16.00",
+ "fieldname": "amt_in_figures_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Starting location from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "account_number_and_signatory_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "account_number_and_signatory_settings",
+ "fieldtype": "Section Break"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "account_no_settings",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "account_no_settings",
+ "fieldtype": "HTML",
+ "options": ""
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "5.00",
- "fieldname": "acc_no_dist_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "5.00",
+ "fieldname": "acc_no_dist_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "4.00",
- "fieldname": "acc_no_dist_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting location from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "4.00",
+ "fieldname": "acc_no_dist_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Starting location from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "signatory_position",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Signatory Position",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "signatory_position",
+ "fieldtype": "Column Break",
+ "label": "Signatory Position"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "6.00",
- "fieldname": "signatory_from_top_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Distance from top edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "6.00",
+ "fieldname": "signatory_from_top_edge",
+ "fieldtype": "Float",
+ "label": "Distance from top edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "default": "15.00",
- "fieldname": "signatory_from_left_edge",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Starting location from left edge",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "2",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "15.00",
+ "fieldname": "signatory_from_left_edge",
+ "fieldtype": "Float",
+ "label": "Starting location from left edge",
+ "precision": "2"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "preview",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "label": "Preview",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "preview",
+ "fieldtype": "Section Break",
+ "label": "Preview"
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "cheque_print_preview",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "cheque_print_preview",
+ "fieldtype": "HTML"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 1,
- "modified": "2016-06-23 20:19:11.694932",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Cheque Print Template",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "max_attachments": 1,
+ "modified": "2026-06-08 12:10:35.829531",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Cheque Print Template",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_seen": 0
-}
\ No newline at end of file
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
index 4b1394ede17..258ea7bb8d7 100644
--- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
+++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
@@ -48,6 +48,8 @@ class ChequePrintTemplate(Document):
@frappe.whitelist()
def create_or_update_cheque_print_format(template_name):
+ frappe.only_for("System Manager")
+
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
cheque_print.update(
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 34120776681..5a2c90a89d5 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -2279,6 +2279,9 @@ def get_outstanding_reference_documents(args, validate=False):
if args.get("party_type") == "Member":
return
+ if args.get("party_type") and args.get("party"):
+ frappe.has_permission(args["party_type"], "read", args["party"], throw=True)
+
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
args["get_outstanding_invoices"] = True
@@ -2788,7 +2791,8 @@ def get_reference_details(
):
total_amount = outstanding_amount = exchange_rate = account = None
- ref_doc = frappe.get_doc(reference_doctype, reference_name)
+ frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
+ ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name)
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
# Only applies for Reverse Payment Entries
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 273aa6c03de..26a45bbdc55 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -99,6 +99,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.subject)
validate_template(self.body)
+ validate_template(self.pdf_name)
if not self.customers:
frappe.throw(_("Customers not selected."))
@@ -548,6 +549,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
@frappe.whitelist()
def send_auto_email():
+ frappe.has_permission("Process Statement Of Accounts", throw=True)
selected = frappe.get_list(
"Process Statement Of Accounts",
filters={"enable_auto_email": 1},
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index a25af18e5fc..195b29e38c6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -608,6 +608,25 @@ frappe.ui.form.on("Purchase Invoice", {
};
});
+ frm.set_query("write_off_account", function (doc) {
+ return {
+ filters: {
+ report_type: "Profit and Loss",
+ is_group: 0,
+ company: doc.company,
+ },
+ };
+ });
+
+ frm.set_query("write_off_cost_center", function (doc) {
+ return {
+ filters: {
+ is_group: 0,
+ company: doc.company,
+ },
+ };
+ });
+
frm.fields_dict["items"].grid.get_field("deferred_expense_account").get_query = function (doc) {
return {
filters: {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index b80eadc7ae0..0c5a73d6a01 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -287,6 +287,7 @@ class PurchaseInvoice(BuyingController):
self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
+ self.validate_write_off_cost_center()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.set_status()
self.validate_purchase_receipt_if_update_stock()
@@ -633,15 +634,16 @@ class PurchaseInvoice(BuyingController):
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
- stock_items = self.get_stock_items()
- if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes":
+ if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
+ stock_and_asset_items = self.get_stock_items()
+ stock_and_asset_items.extend(self.get_asset_items())
if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
):
return
for d in self.get("items"):
- if not d.purchase_receipt and d.item_code in stock_items:
+ if not d.purchase_receipt and d.item_code in stock_and_asset_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
msg += _(
@@ -657,6 +659,27 @@ class PurchaseInvoice(BuyingController):
if self.write_off_amount and not self.write_off_account:
throw(_("Please enter Write Off Account"))
+ if not self.write_off_account:
+ return
+
+ doc = frappe.db.get_value(
+ "Account", self.write_off_account, ["report_type", "is_group", "company"], as_dict=True
+ )
+
+ if not doc or doc.report_type != "Profit and Loss" or doc.is_group or doc.company != self.company:
+ throw(_("Please enter a valid Write Off Account"))
+
+ def validate_write_off_cost_center(self):
+ if not self.write_off_cost_center:
+ return
+
+ doc = frappe.db.get_value(
+ "Cost Center", self.write_off_cost_center, ["is_group", "company"], as_dict=True
+ )
+
+ if not doc or doc.is_group or doc.company != self.company:
+ throw(_("Please enter a valid Write Off Cost Center"))
+
def check_prev_docstatus(self):
for d in self.get("items"):
if d.purchase_order:
@@ -737,6 +760,7 @@ class PurchaseInvoice(BuyingController):
def validate_for_repost(self):
self.validate_write_off_account()
+ self.validate_write_off_cost_center()
self.validate_expense_account()
validate_docs_for_voucher_types(["Purchase Invoice"])
validate_docs_for_deferred_accounting([], [self.name])
@@ -850,7 +874,9 @@ class PurchaseInvoice(BuyingController):
if update_outstanding == "No":
update_voucher_outstanding(
voucher_type=self.doctype,
- voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
+ voucher_no=self.return_against
+ if (cint(self.is_return) and self.return_against)
+ else self.name,
account=self.credit_to,
party_type="Supplier",
party=self.supplier,
@@ -1533,6 +1559,9 @@ class PurchaseInvoice(BuyingController):
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
+ against_voucher = self.name
+ if self.is_return and self.return_against and not self.update_outstanding_for_self:
+ against_voucher = self.return_against
bank_account_currency = get_account_currency(self.cash_bank_account)
# CASH, make payment entries
gl_entries.append(
@@ -1547,9 +1576,7 @@ class PurchaseInvoice(BuyingController):
if self.party_account_currency == self.company_currency
else self.paid_amount,
"debit_in_transaction_currency": self.paid_amount,
- "against_voucher": self.return_against
- if cint(self.is_return) and self.return_against
- else self.name,
+ "against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 61b9cf1fc06..259cd970941 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -180,12 +180,31 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
: "Inter Company Purchase Invoice";
me.frm.add_custom_button(
- button_label,
+ __(button_label),
function () {
me.make_inter_company_invoice();
},
__("Create")
);
+
+ frappe.call({
+ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items",
+ args: {
+ reference_name: me.frm.doc.name,
+ doctype: "Purchase Invoice",
+ reference_fieldname: "sales_invoice_item",
+ },
+ callback: function (r) {
+ if (r.exc) return;
+ const received_items = r.message || {};
+ const has_pending_qty = me.frm.doc.items.some(
+ (item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0
+ );
+ if (!has_pending_qty) {
+ me.frm.remove_custom_button(__(button_label), __("Create"));
+ }
+ },
+ });
}
}
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 3e16c503e69..5d6676aa537 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -23,7 +23,12 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
get_party_tax_withholding_details,
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
-from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
+from erpnext.accounts.party import (
+ CROSS_PARTY_FIELD_NO_MAP,
+ get_due_date,
+ get_party_account,
+ get_party_details,
+)
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
@@ -2526,7 +2531,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"rate": "rate",
},
"postprocess": update_item,
- "condition": lambda doc: doc.qty > 0,
+ "condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0,
}
if doctype in ["Sales Invoice", "Sales Order"]:
@@ -2559,18 +2564,25 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
- "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
+ "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
},
doctype + " Item": item_field_map,
},
target_doc,
set_missing_values,
)
-
+ if not doclist.get("items"):
+ frappe.throw(
+ _(
+ "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. "
+ "Please check the existing linked {2}s."
+ ).format(target_doctype, doctype, target_doctype)
+ )
return doclist
-def get_received_items(reference_name, doctype, reference_fieldname):
+@frappe.whitelist()
+def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
@@ -2583,20 +2595,19 @@ def get_received_items(reference_name, doctype, reference_fieldname):
target_doctypes = frappe.get_all(
doctype,
filters=filters,
- as_list=True,
+ pluck="name",
)
-
+ received_items_map = {}
if target_doctypes:
- target_doctypes = list(target_doctypes[0])
-
- received_items_map = frappe._dict(
- frappe.get_all(
+ received_items_data = frappe.get_all(
doctype + " Item",
filters={"parent": ("in", target_doctypes)},
fields=[reference_fieldname, "qty"],
- as_list=1,
)
- )
+ for item in received_items_data:
+ key = item.get(reference_fieldname)
+ if key:
+ received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty)
return received_items_map
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 147bb76064d..e210903c450 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2690,6 +2690,95 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
+ def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self):
+ item_code_1 = "_Test IC Item 1"
+ item_code_2 = "_Test IC Item 2"
+
+ create_item(item_code_1, is_stock_item=1)
+ create_item(item_code_2, is_stock_item=1)
+
+ si = create_sales_invoice(
+ company="Wind Power LLC",
+ customer="_Test Internal Customer",
+ item_code=item_code_1,
+ debit_to="Debtors - WP",
+ warehouse="Stores - WP",
+ income_account="Sales - WP",
+ expense_account="Cost of Goods Sold - WP",
+ cost_center="Main - WP",
+ currency="USD",
+ qty=3,
+ do_not_save=1,
+ )
+ si.selling_price_list = "_Test Price List Rest of the World"
+ si.append(
+ "items",
+ {
+ "item_code": item_code_2,
+ "item_name": item_code_2,
+ "description": item_code_2,
+ "warehouse": "Stores - WP",
+ "qty": 2,
+ "uom": "Nos",
+ "stock_uom": "Nos",
+ "rate": 100,
+ "price_list_rate": 100,
+ "income_account": "Sales - WP",
+ "expense_account": "Cost of Goods Sold - WP",
+ "cost_center": "Main - WP",
+ "conversion_factor": 1,
+ },
+ )
+
+ si.submit()
+
+ target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+
+ for item in target_doc.items:
+ item.update(
+ {
+ "expense_account": "Cost of Goods Sold - _TC1",
+ "cost_center": "Main - _TC1",
+ }
+ )
+
+ target_doc.submit()
+ self.assertEqual(len(target_doc.items), 2)
+ self.assertEqual([item.qty for item in target_doc.items], [3, 2])
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "already been fully invoiced",
+ ):
+ make_inter_company_transaction("Sales Invoice", si.name)
+
+ def test_inter_company_transaction_does_not_inherit_party_fields(self):
+ """
+ Party-derived fields on SI (from Customer) must not leak into the mapped PI.
+ """
+ si = create_sales_invoice(
+ company="Wind Power LLC",
+ customer="_Test Internal Customer",
+ debit_to="Debtors - WP",
+ warehouse="Stores - WP",
+ income_account="Sales - WP",
+ expense_account="Cost of Goods Sold - WP",
+ cost_center="Main - WP",
+ currency="USD",
+ do_not_save=1,
+ )
+ si.selling_price_list = "_Test Price List Rest of the World"
+ si.tax_category = "_Test Tax Category 1"
+ si.language = "ar"
+ si.payment_terms_template = "_Test Payment Term Template"
+ si.submit()
+
+ pi = make_inter_company_transaction("Sales Invoice", si.name)
+
+ supplier = frappe.get_doc("Supplier", "_Test Internal Supplier")
+ self.assertEqual(pi.tax_category or None, supplier.tax_category or None)
+ self.assertEqual(pi.language or None, supplier.language or None)
+ self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None)
+
def test_inter_company_transaction_without_default_warehouse(self):
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
# setup
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index a5d940d7b7f..5c0860b4fd3 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -48,6 +48,25 @@ SALES_TRANSACTION_TYPES = {
}
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
+# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the
+# source and target documents belong to different parties (e.g. Sales Order →
+# Purchase Order or inter-company Sales Invoice → Purchase Invoice).
+CROSS_PARTY_FIELD_NO_MAP = [
+ "tax_category",
+ "tax_id",
+ "tax_withholding_category",
+ "taxes_and_charges",
+ "address_display",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "contact_person",
+ "shipping_address",
+ "dispatch_address",
+ "payment_terms_template",
+ "language",
+]
+
class DuplicatePartyAccountError(frappe.ValidationError):
pass
diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py
index df3fc48f9e1..6f90cb13398 100644
--- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py
+++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py
@@ -4,6 +4,7 @@
import frappe
from frappe import _
+from frappe.query_builder import CustomFunction
from frappe.utils import cint
@@ -94,19 +95,35 @@ def get_data(filters):
def get_sales_details(filters):
item_details_map = {}
- date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date"
+ if filters["based_on"] not in ("Sales Order", "Sales Invoice"):
+ frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"]))
- sales_data = frappe.db.sql(
- """
- select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date,
- DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order
- from `tab{doctype}` s, `tab{doctype} Item` si
- where s.name = si.parent and s.docstatus = 1
- order by days_since_last_order """.format( # nosec
- date_field=date_field, doctype=filters["based_on"]
- ),
- as_dict=1,
- )
+ parent = frappe.qb.DocType(filters["based_on"])
+ child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
+ child = frappe.qb.DocType(child_doctype)
+
+ date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
+ current_date = CustomFunction("CURRENT_DATE", [])
+
+ date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
+ days_since_last_order = date_diff(current_date(), date_col)
+
+ sales_data = (
+ frappe.qb.from_(parent)
+ .inner_join(child)
+ .on(parent.name == child.parent)
+ .select(
+ parent.territory,
+ parent.customer,
+ child.item_group,
+ child.item_code,
+ child.qty,
+ date_col.as_("last_order_date"),
+ days_since_last_order.as_("days_since_last_order"),
+ )
+ .where(parent.docstatus == 1)
+ .orderby(days_since_last_order)
+ ).run(as_dict=True)
for d in sales_data:
item_details_map.setdefault((d.territory, d.item_code), d)
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 557afc865fd..db820ca9154 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -278,6 +278,7 @@ def get_balance_on(
)
if party_type and party:
+ frappe.has_permission(party_type, "read", party, throw=True)
cond.append(
f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """
)
@@ -397,15 +398,13 @@ def add_ac(args=None):
if not args:
args = frappe.local.form_dict
+ args.pop("ignore_permissions", None)
+ frappe.has_permission("Account", "create", throw=True)
+
args.doctype = "Account"
args = make_tree_args(**args)
ac = frappe.new_doc("Account")
-
- if args.get("ignore_permissions"):
- ac.flags.ignore_permissions = True
- args.pop("ignore_permissions")
-
ac.update(args)
if not ac.parent_account:
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index ee3c9c840f0..7f60244e916 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -362,19 +362,22 @@
"fieldname": "supplier_primary_contact",
"fieldtype": "Link",
"label": "Supplier Primary Contact",
+ "no_copy": 1,
"options": "Contact"
},
{
"fetch_from": "supplier_primary_contact.mobile_no",
"fieldname": "mobile_no",
"fieldtype": "Read Only",
- "label": "Mobile No"
+ "label": "Mobile No",
+ "no_copy": 1
},
{
"fetch_from": "supplier_primary_contact.email_id",
"fieldname": "email_id",
"fieldtype": "Read Only",
- "label": "Email Id"
+ "label": "Email Id",
+ "no_copy": 1
},
{
"fieldname": "column_break_44",
@@ -384,6 +387,7 @@
"fieldname": "primary_address",
"fieldtype": "Text",
"label": "Primary Address",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -391,6 +395,7 @@
"fieldname": "supplier_primary_address",
"fieldtype": "Link",
"label": "Supplier Primary Address",
+ "no_copy": 1,
"options": "Address"
},
{
@@ -486,7 +491,7 @@
"link_fieldname": "party"
}
],
- "modified": "2026-02-06 12:58:01.398824",
+ "modified": "2026-05-29 16:52:59.441272",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index d0d7d58f30d..0d3f13dde8c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -7,7 +7,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold, qb, throw
-from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
+from frappe.model.workflow import get_workflow_name
from frappe.query_builder import Criterion, DocType
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
@@ -178,7 +178,7 @@ class AccountsController(TransactionBase):
if not get_meta(self.doctype).has_field("outstanding_amount"):
return
- if self.get("is_return") and self.return_against and not self.get("is_pos"):
+ if self.get("is_return") and self.return_against and not (self.get("is_pos") or self.get("is_paid")):
against_voucher_outstanding = frappe.get_value(
self.doctype, self.return_against, "outstanding_amount"
)
@@ -3815,7 +3815,9 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
@frappe.whitelist()
-def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
+def update_child_qty_rate(
+ parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
+):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
@@ -3841,14 +3843,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
current_state = doc.get(workflow_doc.workflow_state_field)
roles = frappe.get_roles()
- transitions = []
- for transition in workflow_doc.transitions:
- if transition.next_state == current_state and transition.allowed in roles:
- if not is_transition_condition_satisfied(transition, doc):
- continue
- transitions.append(transition.as_dict())
+ allowed = any(
+ state.state == current_state and (not state.allow_edit or state.allow_edit in roles)
+ for state in workflow_doc.states
+ )
- if not transitions:
+ if not allowed:
frappe.throw(
_("You are not allowed to update as per the conditions set in {} Workflow.").format(
get_link_to_form("Workflow", workflow)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index d5bd3501527..3383bd51fdd 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -53,6 +53,7 @@ class SellingController(StockController):
self.validate_for_duplicate_items()
self.validate_target_warehouse()
self.validate_auto_repeat_subscription_dates()
+ self.validate_sample_retention_warehouse()
for table_field in ["items", "packed_items"]:
if self.get(table_field):
self.set_serial_and_batch_bundle(table_field)
@@ -874,6 +875,26 @@ class SellingController(StockController):
validate_item_type(self, "is_sales_item", "sales")
+ def validate_sample_retention_warehouse(self):
+ if self.get("is_return"):
+ return
+
+ sample_retention_warehouse = frappe.db.get_single_value(
+ "Stock Settings", "sample_retention_warehouse"
+ )
+ if not sample_retention_warehouse:
+ return
+
+ items = self.get("items") + (self.get("packed_items"))
+ for item in items:
+ if item.get("warehouse") == sample_retention_warehouse:
+ frappe.throw(
+ _("Row {0}: Cannot sell item {1} from Sample Retention Warehouse {2}").format(
+ item.idx, frappe.bold(item.item_code), frappe.bold(sample_retention_warehouse)
+ ),
+ title=_("Not Allowed"),
+ )
+
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 4d141d346eb..f3f1f1b0651 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -178,13 +178,16 @@ def get_list_for_transactions(
def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length):
- data = frappe.db.sql(
- """select distinct parent as name, supplier from `tab{doctype}`
- where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""".format(
- doctype=parties_doctype, supplier=parties[0], start=limit_start, len=limit_page_length
- ),
- as_dict=1,
- )
+ party = frappe.qb.DocType(parties_doctype)
+ data = (
+ frappe.qb.from_(party)
+ .select(party.parent.as_("name"), party.supplier)
+ .distinct()
+ .where((party.supplier == party[0]) & (party.docstatus == 1))
+ .orderby(party.creation, order=frappe.qb.desc)
+ .limit(limit_page_length)
+ .offset(limit_start)
+ ).run(as_dict=True)
return post_process(doctype, data)
diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py
index 86521586b28..586ebe91eed 100644
--- a/erpnext/crm/frappe_crm_api.py
+++ b/erpnext/crm/frappe_crm_api.py
@@ -32,20 +32,16 @@ def create_custom_fields_for_frappe_crm():
@frappe.whitelist()
def create_prospect_against_crm_deal():
doc = frappe.form_dict
- prospect = frappe.get_doc(
- {
- "doctype": "Prospect",
- "company_name": doc.organization or doc.lead_name,
- "no_of_employees": doc.no_of_employees,
- "prospect_owner": doc.deal_owner,
- "company": doc.erpnext_company,
- "crm_deal": doc.crm_deal,
- "territory": doc.territory,
- "industry": doc.industry,
- "website": doc.website,
- "annual_revenue": doc.annual_revenue,
- }
- )
+ prospect = frappe.new_doc("Prospect")
+ prospect.company_name = doc.organization or doc.lead_name
+ prospect.no_of_employees = doc.no_of_employees
+ prospect.prospect_owner = doc.deal_owner
+ prospect.company = doc.erpnext_company
+ prospect.crm_deal = doc.crm_deal
+ prospect.territory = doc.territory
+ prospect.industry = doc.industry
+ prospect.website = doc.website
+ prospect.annual_revenue = doc.annual_revenue
try:
prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name})
@@ -151,6 +147,18 @@ def contact_exists(email, mobile_no):
return False
+CUSTOMER_ALLOWED_FIELDS = {
+ "customer_name",
+ "customer_group",
+ "customer_type",
+ "territory",
+ "default_currency",
+ "industry",
+ "website",
+ "crm_deal",
+}
+
+
@frappe.whitelist()
def create_customer(customer_data=None):
if not customer_data:
@@ -159,9 +167,11 @@ def create_customer(customer_data=None):
try:
customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")})
if not customer_name:
- customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert(
- ignore_permissions=True
- )
+ customer = frappe.new_doc("Customer")
+ for field in CUSTOMER_ALLOWED_FIELDS:
+ if customer_data.get(field) is not None:
+ customer.set(field, customer_data.get(field))
+ customer.insert(ignore_permissions=True)
customer_name = customer.name
contacts = json.loads(customer_data.get("contacts"))
diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
index 06c1b497551..41b03cc7793 100644
--- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
+++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
@@ -21,6 +21,7 @@
"min_order_qty",
"section_break_8",
"sales_order",
+ "main_item_code",
"bin_qty_section",
"actual_qty",
"requested_qty",
@@ -114,6 +115,14 @@
"options": "Sales Order",
"read_only": 1
},
+ {
+ "fieldname": "main_item_code",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Main Item Code",
+ "no_copy": 1,
+ "read_only": 1
+ },
{
"fieldname": "requested_qty",
"fieldtype": "Float",
@@ -213,4 +222,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py
index aa1c72294d3..02bb50617f5 100644
--- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py
+++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.py
@@ -20,6 +20,7 @@ class MaterialRequestPlanItem(Document):
from_warehouse: DF.Link | None
item_code: DF.Link
item_name: DF.Data | None
+ main_item_code: DF.Data | None
material_request_type: DF.Literal[
"", "Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided"
]
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 1d63f91caaf..67323d42d40 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1397,7 +1397,7 @@ def get_material_request_items(
"sales_order": sales_order,
"description": row.get("description"),
"uom": row.get("purchase_uom") or row.get("stock_uom"),
- "main_bom_item": row.get("main_bom_item"),
+ "main_item_code": row.get("main_bom_item"),
}
@@ -1557,6 +1557,8 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
"item_code": sa_row.production_item,
"required_qty": sa_row.qty,
"include_exploded_items": 0,
+ "sales_order": sa_row.sales_order,
+ "main_bom_item": sa_row.parent_item_code,
}
)
)
@@ -1660,6 +1662,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
"stock_uom": item_master.stock_uom,
"conversion_factor": conversion_factor,
"safety_stock": item_master.safety_stock,
+ "main_bom_item": data.get("main_bom_item"),
}
)
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 62aa4f6ea11..5af1fdb36b5 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -1254,8 +1254,10 @@ class TestProductionPlan(FrappeTestCase):
plan.get_sub_assembly_items()
mr_items = []
+ expected_main_item_by_mr_item = {"ChildPart1 For MR": "SubAssembly1-1 For MR"}
for row in plan.sub_assembly_items:
mr_items.append(row.production_item)
+ expected_main_item_by_mr_item[row.production_item] = row.parent_item_code
row.type_of_manufacturing = "Material Request"
plan.save()
@@ -1265,6 +1267,10 @@ class TestProductionPlan(FrappeTestCase):
for item_code in mr_items:
self.assertTrue(item_code in validate_mr_items)
+ main_item_by_mr_item = {item.get("item_code"): item.get("main_item_code") for item in items}
+ for item_code, main_item_code in expected_main_item_by_mr_item.items():
+ self.assertEqual(main_item_by_mr_item[item_code], main_item_code)
+
def test_resered_qty_for_production_plan_for_material_requests(self):
from erpnext.stock.utils import get_or_make_bin
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 5d8382e7ebd..9d3100302e7 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -406,7 +406,7 @@ class WorkOrder(Document):
elif self.docstatus == 1:
if status not in ["Closed", "Stopped"]:
status = "Not Started"
- if flt(self.material_transferred_for_manufacturing) > 0:
+ if flt(self.material_transferred_for_manufacturing) > 0 or self.skip_transfer:
status = "In Process"
precision = frappe.get_precision("Work Order", "produced_qty")
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 3e467b37b0f..239066f7c98 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -435,3 +435,4 @@ erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.depends_on_inv_dimensions
erpnext.patches.v16_0.clear_procedures_from_receivable_report
erpnext.patches.v16_0.migrate_address_contact_custom_fields
+erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item
diff --git a/erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py b/erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py
new file mode 100644
index 00000000000..74d024361b3
--- /dev/null
+++ b/erpnext/patches/v15_0/set_main_item_code_in_material_request_plan_item.py
@@ -0,0 +1,97 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("manufacturing", "doctype", "material_request_plan_item")
+
+ if not frappe.db.has_column("Material Request Plan Item", "main_item_code"):
+ return
+
+ for row in get_material_request_plan_items():
+ if row.main_item_code:
+ continue
+
+ main_item_code = get_main_item_code(row)
+ if main_item_code:
+ frappe.db.set_value(
+ "Material Request Plan Item",
+ row.name,
+ "main_item_code",
+ main_item_code,
+ update_modified=False,
+ )
+
+
+def get_material_request_plan_items():
+ return frappe.get_all(
+ "Material Request Plan Item",
+ fields=["name", "parent", "item_code", "sales_order", "main_item_code"],
+ )
+
+
+def get_main_item_code(row):
+ return (
+ get_main_item_code_from_sub_assembly(row)
+ or get_main_item_code_from_sub_assembly_bom(row)
+ or get_main_item_code_from_production_plan_bom(row)
+ )
+
+
+def get_main_item_code_from_sub_assembly(row):
+ sub_assembly = frappe.db.get_value(
+ "Production Plan Sub Assembly Item",
+ get_filters(
+ row,
+ {
+ "parent": row.parent,
+ "production_item": row.item_code,
+ },
+ ),
+ "parent_item_code",
+ )
+
+ return sub_assembly
+
+
+def get_main_item_code_from_sub_assembly_bom(row):
+ for sub_assembly in get_sub_assembly_items(row):
+ if item_exists_in_bom(row.item_code, sub_assembly.bom_no):
+ return frappe.db.get_value("BOM", sub_assembly.bom_no, "item")
+
+
+def get_main_item_code_from_production_plan_bom(row):
+ for production_plan_item in get_production_plan_items(row):
+ if item_exists_in_bom(row.item_code, production_plan_item.bom_no):
+ return frappe.db.get_value("BOM", production_plan_item.bom_no, "item")
+
+
+def get_sub_assembly_items(row):
+ return frappe.get_all(
+ "Production Plan Sub Assembly Item",
+ filters=get_filters(row, {"parent": row.parent}),
+ fields=["bom_no"],
+ )
+
+
+def get_production_plan_items(row):
+ return frappe.get_all(
+ "Production Plan Item",
+ filters=get_filters(row, {"parent": row.parent}),
+ fields=["bom_no"],
+ )
+
+
+def get_filters(row, filters):
+ if row.sales_order:
+ filters["sales_order"] = row.sales_order
+
+ return filters
+
+
+def item_exists_in_bom(item_code, bom_no):
+ if not bom_no:
+ return False
+
+ return frappe.db.exists("BOM Item", {"parent": bom_no, "item_code": item_code}) or frappe.db.exists(
+ "BOM Explosion Item", {"parent": bom_no, "item_code": item_code}
+ )
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 42163c1a6c3..e6eb9180eeb 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -418,7 +418,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
})
}
-
onload_post_render() {
if(this.frm.doc.__islocal && !(this.frm.doc.taxes || []).length
&& !this.frm.doc.__onload?.load_after_mapping) {
@@ -659,15 +658,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
async () => {
// for internal customer instead of pricing rule directly apply valuation rate on item
- const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value(
- "Accounts Settings", "fetch_valuation_rate_for_internal_transaction"
- );
- if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && fetch_valuation_rate_for_internal_transactions) {
- me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time,
- me.frm.doc.doctype, me.frm.doc.company);
- } else {
- me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
+ if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier)) {
+ const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value(
+ "Accounts Settings", "fetch_valuation_rate_for_internal_transaction"
+ );
+ if (fetch_valuation_rate_for_internal_transactions) {
+ me.get_incoming_rate(
+ item,
+ me.frm.posting_date,
+ me.frm.posting_time,
+ me.frm.doc.doctype,
+ me.frm.doc.company
+ );
+ return;
+ }
}
+
+ me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
},
() => {
if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 293f8e1324e..3ae0b3d1835 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -651,6 +651,7 @@ erpnext.utils.update_child_items = function (opts) {
read_only: 0,
disabled: 0,
label: __("Item Code"),
+ formatter: (value) => value,
get_query: function () {
let filters;
if (frm.doc.doctype == "Sales Order") {
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index dd1910ed15a..568aad1a36c 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -305,6 +305,7 @@
"fieldname": "customer_primary_contact",
"fieldtype": "Link",
"label": "Customer Primary Contact",
+ "no_copy": 1,
"options": "Contact"
},
{
@@ -312,6 +313,7 @@
"fieldname": "mobile_no",
"fieldtype": "Read Only",
"label": "Mobile No",
+ "no_copy": 1,
"options": "Mobile"
},
{
@@ -319,6 +321,7 @@
"fieldname": "email_id",
"fieldtype": "Read Only",
"label": "Email Id",
+ "no_copy": 1,
"options": "Email"
},
{
@@ -330,12 +333,14 @@
"fieldname": "customer_primary_address",
"fieldtype": "Link",
"label": "Customer Primary Address",
+ "no_copy": 1,
"options": "Address"
},
{
"fieldname": "primary_address",
"fieldtype": "Text",
"label": "Primary Address",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -590,14 +595,16 @@
"fieldname": "first_name",
"fieldtype": "Read Only",
"hidden": 1,
- "label": "First Name"
+ "label": "First Name",
+ "no_copy": 1
},
{
"fetch_from": "customer_primary_contact.last_name",
"fieldname": "last_name",
"fieldtype": "Read Only",
"hidden": 1,
- "label": "Last Name"
+ "label": "Last Name",
+ "no_copy": 1
}
],
"icon": "fa fa-user",
@@ -611,7 +618,7 @@
"link_fieldname": "party"
}
],
- "modified": "2026-01-21 17:23:42.151114",
+ "modified": "2026-05-29 16:52:59.441272",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index cb573b75452..edfcb0becfe 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -26,7 +26,7 @@ frappe.ui.form.on("Sales Order", {
let color;
if (!doc.qty && frm.doc.has_unit_price_items) {
color = "yellow";
- } else if (doc.stock_qty <= doc.actual_qty) {
+ } else if (doc.stock_qty - doc.delivered_qty <= doc.actual_qty) {
color = "green";
} else {
color = "orange";
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 856a75606bc..1dd62a65239 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -10,6 +10,7 @@ import frappe.utils
from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
+from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Sum
@@ -20,7 +21,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_linked_doc,
validate_inter_company_party,
)
-from erpnext.accounts.party import get_party_account
+from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_party_account
from erpnext.controllers.selling_controller import SellingController
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
validate_against_blanket_order,
@@ -1332,7 +1333,9 @@ def get_events(start, end, filters=None):
@frappe.whitelist()
-def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
+def make_purchase_order_for_default_supplier(
+ source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None
+):
"""Creates Purchase Order for each Supplier. Returns a list of doc objects."""
from erpnext.setup.utils import get_exchange_rate
@@ -1361,7 +1364,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
target.shipping_rule = ""
target.tc_name = ""
target.terms = ""
- target.payment_terms_template = ""
target.payment_schedule = []
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
@@ -1418,16 +1420,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
{
"Sales Order": {
"doctype": "Purchase Order",
- "field_no_map": [
- "address_display",
- "contact_display",
- "contact_mobile",
- "contact_email",
- "contact_person",
- "taxes_and_charges",
- "shipping_address",
- "dispatch_address",
- ],
+ "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP],
"validation": {"docstatus": ["=", 1]},
},
"Sales Order Item": {
@@ -1492,7 +1485,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
@frappe.whitelist()
-def make_purchase_order(source_name, selected_items=None, target_doc=None):
+def make_purchase_order(
+ source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None
+):
if not selected_items:
return
@@ -1520,7 +1515,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.shipping_rule = ""
target.tc_name = ""
target.terms = ""
- target.payment_terms_template = ""
target.payment_schedule = []
if is_drop_ship_order(target):
@@ -1559,16 +1553,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
{
"Sales Order": {
"doctype": "Purchase Order",
- "field_no_map": [
- "address_display",
- "contact_display",
- "contact_mobile",
- "contact_email",
- "contact_person",
- "taxes_and_charges",
- "shipping_address",
- "dispatch_address",
- ],
+ "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP],
"validation": {"docstatus": ["=", 1]},
},
"Sales Order Item": {
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index b5de5fdc99c..6f8befeb876 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -24,6 +24,8 @@ from erpnext.selling.doctype.sales_order.sales_order import (
create_pick_list,
make_delivery_note,
make_material_request,
+ make_purchase_order,
+ make_purchase_order_for_default_supplier,
make_raw_material_request,
make_sales_invoice,
make_work_orders,
@@ -692,6 +694,40 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
workflow.is_active = 0
workflow.save()
+ def test_update_child_qty_rate_follows_allow_edit(self):
+ from frappe.model.workflow import apply_workflow
+
+ workflow = make_sales_order_edit_perm_workflow()
+ so = make_sales_order(item_code="_Test Item", qty=1, rate=150, do_not_submit=1)
+ apply_workflow(so, "Approve")
+
+ trans_item = json.dumps(
+ [{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}]
+ )
+
+ mover = "test@example.com"
+ mover_user = frappe.get_doc("User", mover)
+ mover_user.add_roles("Sales User", "Test Junior Approver")
+ with self.set_user(mover):
+ # transitioned the doc into Approved but is not the configured editor
+ self.assertRaises(
+ frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name
+ )
+
+ editor = "test2@example.com"
+ editor_user = frappe.get_doc("User", editor)
+ editor_user.add_roles("Sales User", "Test Approver")
+ with self.set_user(editor):
+ # Test Approver is the "Only Allow Edit For" role on Approved
+ update_child_qty_rate("Sales Order", trans_item, so.name)
+ so.reload()
+ self.assertEqual(so.items[0].qty, 2)
+
+ mover_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
+ editor_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
+ workflow.is_active = 0
+ workflow.save()
+
def test_material_request_for_product_bundle(self):
# Create the Material Request from the sales order for the Packing Items
# Check whether the material request has the correct packing item or not.
@@ -1330,8 +1366,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
Tests if the the Product Bundles in the Items table of Sales Orders are replaced with
their child items(from the Packed Items table) on creating a Purchase Order from it.
"""
- from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
-
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
@@ -1360,8 +1394,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
"""
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
"""
- from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
-
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
@@ -2385,8 +2417,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
def test_item_tax_transfer_from_sales_to_purchase(self):
- from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
-
item_tax = frappe.new_doc("Item Tax Template")
item_tax.title = "Test Item Tax Template"
item_tax.company = "_Test Company"
@@ -2487,6 +2517,33 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertFalse(so.per_billed)
self.assertEqual(so.status, "To Deliver and Bill")
+ def test_make_purchase_order_does_not_inherit_party_fields(self):
+ """
+ Customer-derived fields must not leak from a drop-ship SO into the PO.
+ """
+ so_items = [
+ {
+ "item_code": "_Test Item",
+ "warehouse": "",
+ "qty": 1,
+ "rate": 100,
+ "delivered_by_supplier": 1,
+ "supplier": "_Test Supplier",
+ }
+ ]
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.tax_category = "_Test Tax Category 1"
+ so.language = "ar"
+ so.payment_terms_template = "_Test Payment Term Template"
+ so.submit()
+
+ po = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)[0]
+
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
+ self.assertEqual(po.tax_category or None, supplier.tax_category or None)
+ self.assertEqual(po.language or None, supplier.language or None)
+ self.assertEqual(po.payment_terms_template or None, supplier.payment_terms or None)
+
def test_pending_quantity_after_update_item_during_invoice_creation(self):
so = make_sales_order(qty=30, rate=100)
@@ -2683,3 +2740,41 @@ def make_sales_order_workflow():
workflow.insert(ignore_permissions=True)
return workflow
+
+
+def make_sales_order_edit_perm_workflow():
+ if frappe.db.exists("Workflow", "SO Edit Perm Workflow"):
+ doc = frappe.get_doc("Workflow", "SO Edit Perm Workflow")
+ doc.set("is_active", 1)
+ doc.save()
+ return doc
+
+ frappe.get_doc(doctype="Role", role_name="Test Junior Approver").insert(ignore_if_duplicate=True)
+ frappe.get_doc(doctype="Role", role_name="Test Approver").insert(ignore_if_duplicate=True)
+ frappe.cache().hdel("roles", frappe.session.user)
+
+ workflow = frappe.get_doc(
+ {
+ "doctype": "Workflow",
+ "workflow_name": "SO Edit Perm Workflow",
+ "document_type": "Sales Order",
+ "workflow_state_field": "workflow_state",
+ "is_active": 1,
+ "send_email_alert": 0,
+ }
+ )
+ workflow.append("states", dict(state="Pending", allow_edit="All"))
+ workflow.append("states", dict(state="Approved", allow_edit="Test Approver", doc_status=1))
+ workflow.append(
+ "transitions",
+ dict(
+ state="Pending",
+ action="Approve",
+ next_state="Approved",
+ allowed="Test Junior Approver",
+ allow_self_approval=1,
+ ),
+ )
+ workflow.insert(ignore_permissions=True)
+
+ return workflow
diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py
index 65fef8b5ac6..ed25dd5f153 100644
--- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py
+++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py
@@ -11,9 +11,9 @@ field_map = {
"name",
"address_line1",
"address_line2",
+ "pincode",
"city",
"state",
- "pincode",
"country",
"is_primary_address",
],
diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json
index 18c513b3c2b..7ee3a79b4a0 100644
--- a/erpnext/setup/setup_wizard/data/country_wise_tax.json
+++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json
@@ -262,7 +262,15 @@
},
"Belgium VAT 12%": {
"account_name": "VAT 12%",
- "tax_rate": 12
+ "tax_rate": 12.00
+ },
+ "Belgium VAT 6%": {
+ "account_name": "VAT 6%",
+ "tax_rate": 6.00
+ },
+ "Belgium VAT 0%": {
+ "account_name": "VAT 0%",
+ "tax_rate": 0.00
}
},
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index ba9aaa6c48a..6b2b1360cf9 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -11,7 +11,6 @@ from frappe.model.naming import make_autoname, revert_series_if_last
from frappe.query_builder.functions import CurDate, Sum
from frappe.utils import cint, flt, get_link_to_form
from frappe.utils.data import add_days
-from frappe.utils.jinja import render_template
class UnableToSelectBatchError(frappe.ValidationError):
@@ -225,10 +224,8 @@ class Batch(Document):
:return: The string that was generated.
"""
naming_series_prefix = _get_batch_prefix()
- # validate_template(naming_series_prefix)
- naming_series_prefix = render_template(str(naming_series_prefix), self.__dict__)
key = _make_naming_series_key(naming_series_prefix)
- name = make_autoname(key)
+ name = make_autoname(key, doc=self)
return name
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index c3764138a08..a450b82caff 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -506,6 +506,24 @@ class TestBatch(FrappeTestCase):
if not use_naming_series:
frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0)
+ def test_naming_series_prefix_is_not_rendered_as_jinja(self):
+ from frappe.model.naming import InvalidNamingSeriesError
+
+ stock_settings = frappe.get_single("Stock Settings")
+ use_naming_series = cint(stock_settings.use_naming_series)
+ original_prefix = stock_settings.naming_series_prefix
+
+ frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 1)
+ frappe.set_value("Stock Settings", "Stock Settings", "naming_series_prefix", "{{ 7*7 }}")
+
+ try:
+ self.assertRaises(
+ InvalidNamingSeriesError, self.make_new_batch, "_Test Stock Item For Batch SSTI"
+ )
+ finally:
+ frappe.set_value("Stock Settings", "Stock Settings", "naming_series_prefix", original_prefix)
+ frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", use_naming_series)
+
def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0):
batch = frappe.new_doc("Batch")
item = self.make_batch_item(item_name)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index f1fc54c751d..0ad8bc781a5 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -15,7 +15,7 @@ from frappe.query_builder import DocType
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, flt
-from erpnext.accounts.party import get_due_date
+from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_due_date
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.stock_ledger import validate_reserved_stock
@@ -1405,8 +1405,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
doctype: {
"doctype": target_doctype,
"postprocess": update_details,
- "field_no_map": ["taxes_and_charges", "set_warehouse"],
- "field_map": {"shipping_address_name": "shipping_address"},
+ "field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse"],
},
doctype + " Item": {
"doctype": target_doctype + " Item",
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 19585b9dabc..47dd2f50656 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -740,14 +740,18 @@ $.extend(erpnext.item, {
if (!row.disabled) {
if (row.numeric_values) {
- fieldtype = "Float";
- desc =
- "Min Value: " +
- row.from_range +
- " , Max Value: " +
- row.to_range +
- ", in Increments of: " +
- row.increment;
+ const all_are_int =
+ flt(row.from_range) === cint(row.from_range) &&
+ flt(row.to_range) === cint(row.to_range) &&
+ flt(row.increment) === cint(row.increment);
+ fieldtype = all_are_int ? "Int" : "Float";
+ const df = { fieldtype };
+ const options = all_are_int ? { inline: 1 } : { always_show_decimals: true, inline: 1 };
+ desc = __("Min Value: {0}, Max Value: {1}, in Increments of: {2}", [
+ frappe.format(row.from_range, df, options),
+ frappe.format(row.to_range, df, options),
+ frappe.format(row.increment, df, options),
+ ]);
} else {
fieldtype = "Data";
desc = "";
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index ce23acf85e2..3799e773a7d 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1050,6 +1050,44 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.cancel()
+ def test_inter_company_purchase_receipt_does_not_inherit_party_fields(self):
+ """
+ Party-derived fields on DN (from Customer) must not leak into the mapped PR.
+ """
+ from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ prepare_data_for_internal_transfer()
+
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ stock = make_purchase_receipt(warehouse="Stores - TCP1", company=company)
+
+ dn = create_delivery_note(
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=1,
+ rate=100,
+ warehouse="Stores - TCP1",
+ target_warehouse="Work In Progress - TCP1",
+ do_not_submit=True,
+ )
+ # Stamp customer-side party fields onto the DN
+ dn.tax_category = "_Test Tax Category 2"
+ dn.language = "ar"
+ dn.submit()
+
+ pr = make_inter_company_purchase_receipt(dn.name)
+
+ supplier = frappe.get_doc("Supplier", "_Test Internal Supplier 2")
+ self.assertEqual(pr.tax_category or None, supplier.tax_category or None)
+ self.assertEqual(pr.language or None, supplier.language or None)
+ dn.cancel()
+ stock.cancel()
+
def test_lcv_for_internal_transfer(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 1ac6afd15b1..39e5f1425f0 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -2169,7 +2169,16 @@ def get_type_of_transaction(parent_doc, child_row):
def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse=None) -> object:
+ frappe.has_permission("Serial and Batch Bundle", "write", throw=True)
doc = frappe.get_doc("Serial and Batch Bundle", bundle)
+
+ if doc.docstatus == 1:
+ doc.throw_error_message(
+ _("Serial and Batch Bundle {0} is submitted and its entries cannot be modified.").format(
+ frappe.bold(bundle)
+ )
+ )
+
doc.voucher_detail_no = child_row.name
doc.posting_date = parent_doc.posting_date
doc.posting_time = parent_doc.posting_time
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 5e33852badc..2c184cbb876 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -734,6 +734,60 @@ class TestSerialandBatchBundle(FrappeTestCase):
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
self.assertEqual(docstatus, 2)
+ def test_submitted_bundle_entries_cannot_be_mutated(self):
+ # A submitted Serial and Batch Bundle is the immutable source of truth for the stock
+ # ledger, live batch availability and repost/valuation replay. update_serial_batch_no_ledgers
+ # (which the whitelisted add_serial_batch_ledgers delegates to for an existing bundle) must
+ # refuse to rebuild -- and thereby inflate -- the quantities of an already submitted bundle.
+ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
+ update_serial_batch_no_ledgers,
+ )
+
+ item_code = make_item(
+ properties={
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TAMPER-SBB-.#####",
+ }
+ ).name
+
+ se = make_stock_entry(
+ item_code=item_code,
+ target="_Test Warehouse - _TC",
+ qty=10,
+ rate=100,
+ )
+
+ bundle = se.items[0].serial_and_batch_bundle
+ self.assertEqual(frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus"), 1)
+
+ original = frappe.db.get_value(
+ "Serial and Batch Entry", {"parent": bundle}, ["name", "batch_no", "qty"], as_dict=True
+ )
+ self.assertEqual(original.qty, 10)
+
+ # Attempt to forge the submitted bundle: keep the same batch but inflate qty. The guard
+ # fires immediately after the bundle is loaded (docstatus check), so child_row / parent_doc
+ # only need the minimal fields the function reads.
+ tampered_entries = [{"batch_no": original.batch_no, "qty": 1000}]
+ child_row = frappe._dict({"name": se.items[0].name})
+ parent_doc = frappe._dict({"posting_date": today(), "posting_time": nowtime()})
+
+ self.assertRaises(
+ frappe.ValidationError,
+ update_serial_batch_no_ledgers,
+ bundle,
+ tampered_entries,
+ child_row,
+ parent_doc,
+ )
+
+ # The on-disk quantity must be untouched by the rejected mutation attempt.
+ self.assertEqual(
+ frappe.db.get_value("Serial and Batch Entry", original.name, "qty"),
+ 10,
+ )
+
def test_batch_duplicate_entry(self):
item_code = make_item(properties={"has_batch_no": 1}).name
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index e6d6dab7426..87715e1e516 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -228,6 +228,13 @@ class StockEntry(StockController):
self.validate_warehouse()
self.validate_with_material_request()
+
+ # Disassembly rows are fully derived from the source manufacture entry / work order;
+ # verify the posted stock quantities have not been tampered with (raw-material minting).
+ # Must run after set_transfer_qty() so row.transfer_qty reflects qty * conversion_factor.
+ if self.purpose == "Disassemble":
+ self.validate_disassembly_quantities()
+
self.validate_batch()
self.validate_inspection()
self.validate_fg_completed_qty()
@@ -246,6 +253,7 @@ class StockEntry(StockController):
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
self.validate_component_and_quantities()
+ self.validate_finished_good_serial_batch_for_work_order()
if not self.get("purpose") == "Manufacture":
# ignore scrap item wh difference and empty source/target wh
@@ -256,6 +264,83 @@ class StockEntry(StockController):
self.validate_same_source_target_warehouse_during_material_transfer()
self.validate_raw_materials_exists()
+ def validate_finished_good_serial_batch_for_work_order(self):
+ if not (
+ self.work_order
+ and self.pro_doc
+ and self.pro_doc.get("track_semi_finished_goods") != 1
+ and cint(
+ frappe.db.get_single_value(
+ "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True
+ )
+ )
+ and (self.pro_doc.has_serial_no or self.pro_doc.has_batch_no)
+ ):
+ return
+
+ for row in self.items:
+ if not row.is_finished_item:
+ continue
+
+ if self.check_invalid_serial_batch_nos_for_finished_good_item(row):
+ self.reset_serial_batch_on_fg_row(row)
+ frappe.msgprint(
+ _(
+ "Row {0}: Serial/Batch has been reset to values linked with Work Order {1}"
+ " because the previously selected serial/batch does not belong to this Work Order."
+ ).format(row.idx, frappe.bold(self.work_order))
+ )
+
+ def check_invalid_serial_batch_nos_for_finished_good_item(self, row) -> bool:
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos_from_bundle
+
+ if self.pro_doc.has_serial_no:
+ serial_nos = get_serial_nos(row.serial_no) if row.serial_no else []
+ if not serial_nos and row.serial_and_batch_bundle:
+ serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
+ if serial_nos:
+ valid_serial_nos = frappe.get_all(
+ "Serial No",
+ filters={"name": ("in", serial_nos), "work_order": self.work_order},
+ pluck="name",
+ )
+ return bool(set(serial_nos) - set(valid_serial_nos))
+ else:
+ return True
+
+ if self.pro_doc.has_batch_no:
+ batch_nos = [row.batch_no] if row.batch_no else []
+ if not batch_nos and row.serial_and_batch_bundle:
+ batch_nos = list(get_batches_from_bundle(row.serial_and_batch_bundle).keys())
+ if batch_nos:
+ valid_batch_nos = frappe.get_all(
+ "Batch",
+ filters={"name": ("in", batch_nos), "reference_name": self.work_order},
+ pluck="name",
+ )
+ return bool(set(batch_nos) - set(valid_batch_nos))
+ else:
+ return True
+
+ def reset_serial_batch_on_fg_row(self, row):
+ item_details = frappe._dict(
+ {
+ "item_code": row.item_code,
+ "t_warehouse": row.t_warehouse,
+ "qty": row.qty,
+ }
+ )
+
+ row.serial_no = None
+ row.batch_no = None
+ row.serial_and_batch_bundle = None
+
+ if self.pro_doc.has_serial_no:
+ self.set_serial_no_batch_for_finished_good()
+ elif self.pro_doc.has_batch_no:
+ self.set_batchwise_finished_goods(item_details, None, existing_row=row)
+
def validate_repack_entry(self):
if self.purpose != "Repack":
return
@@ -602,7 +687,7 @@ class StockEntry(StockController):
amount += additional_cost_amt
project = frappe.get_doc("Project", self.project)
project.total_consumed_material_cost = amount
- project.save()
+ project.save(ignore_permissions=True)
def validate_item(self):
stock_items = self.get_stock_items()
@@ -853,6 +938,93 @@ class StockEntry(StockController):
title=_("Excess Disassembly"),
)
+ def validate_disassembly_quantities(self):
+ self.validate_finished_good_consumption()
+ self.validate_materials_against_source()
+
+ def validate_finished_good_consumption(self):
+ """The finished good consumed (in stock UOM) must equal the quantity to disassemble."""
+ precision = frappe.get_precision("Stock Entry Detail", "transfer_qty")
+ tolerance = _qty_tolerance(precision)
+
+ fg_stock_qty = sum(flt(row.transfer_qty) for row in self.items if row.is_finished_item)
+ fg_completed_qty = flt(self.fg_completed_qty)
+
+ if abs(flt(fg_stock_qty, precision) - flt(fg_completed_qty, precision)) > tolerance:
+ frappe.throw(
+ _(
+ "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity "
+ "to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the "
+ "finished good row."
+ ).format(flt(fg_stock_qty, precision), flt(fg_completed_qty, precision)),
+ title=_("Invalid Disassembly Quantity"),
+ )
+
+ def validate_materials_against_source(self):
+ """Every non-finished-good row's posted stock qty must equal the source qty x scale."""
+ scale_factor = self._get_disassembly_scale_factor()
+ if not scale_factor:
+ # Standalone BOM disassembly: no source entry to scale against. The finished-good
+ # invariant above still applies; raw-material amounts come from the BOM.
+ return
+
+ source_rows = self.get_items_from_manufacture_stock_entry()
+ source_by_name = {row.name: row for row in source_rows if row.get("name")}
+ source_by_item = defaultdict(float)
+ for row in source_rows:
+ source_by_item[row.item_code] += flt(row.transfer_qty)
+
+ precision = frappe.get_precision("Stock Entry Detail", "transfer_qty")
+ tolerance = _qty_tolerance(precision)
+
+ for row in self.items:
+ if row.is_finished_item:
+ continue # covered by validate_finished_good_consumption
+
+ if row.ste_detail and row.ste_detail in source_by_name:
+ expected = flt(source_by_name[row.ste_detail].transfer_qty) * scale_factor
+ elif row.item_code in source_by_item:
+ expected = source_by_item[row.item_code] * scale_factor
+ else:
+ frappe.throw(
+ _(
+ "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be "
+ "added to this disassembly."
+ ).format(row.idx, frappe.bold(row.item_code)),
+ title=_("Invalid Disassembly Item"),
+ )
+
+ if abs(flt(row.transfer_qty, precision) - flt(expected, precision)) > tolerance:
+ frappe.throw(
+ _(
+ "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity "
+ "derived from the source ({3}). Do not change the UOM, conversion factor or "
+ "quantity of disassembly rows."
+ ).format(
+ row.idx,
+ frappe.bold(row.item_code),
+ flt(row.transfer_qty, precision),
+ flt(expected, precision),
+ ),
+ title=_("Invalid Disassembly Quantity"),
+ )
+
+ def _get_disassembly_scale_factor(self) -> float:
+ disassemble_qty = flt(self.fg_completed_qty)
+ if self.source_stock_entry:
+ source_fg_qty = flt(
+ frappe.db.get_value("Stock Entry", self.source_stock_entry, "fg_completed_qty")
+ )
+ elif self.work_order:
+ source_fg_qty = flt(frappe.db.get_value("Work Order", self.work_order, "produced_qty"))
+ else:
+ return 0.0
+
+ if not source_fg_qty:
+ return 0.0
+
+ return disassemble_qty / source_fg_qty
+
def check_if_operations_completed(self):
"""Check if Time Sheets are completed against before manufacturing to capture operating costs."""
prod_order = frappe.get_doc("Work Order", self.work_order)
@@ -2465,15 +2637,16 @@ class StockEntry(StockController):
else:
self.add_finished_goods(args, item)
- def set_batchwise_finished_goods(self, args, item):
+ def set_batchwise_finished_goods(self, args, item, existing_row=None):
batches = get_empty_batches_based_work_order(self.work_order, self.pro_doc.production_item)
if not batches:
- self.add_finished_goods(args, item)
+ if not existing_row:
+ self.add_finished_goods(args, item)
else:
- self.add_batchwise_finished_good(batches, args, item)
+ self.add_batchwise_finished_good(batches, args, item, existing_row=existing_row)
- def add_batchwise_finished_good(self, batches, args, item):
+ def add_batchwise_finished_good(self, batches, args, item, existing_row=None):
qty = flt(self.fg_completed_qty)
row = frappe._dict({"batches_to_be_consume": defaultdict(float)})
@@ -2482,7 +2655,7 @@ class StockEntry(StockController):
if not row.batches_to_be_consume:
return
- id = create_serial_and_batch_bundle(
+ _id = create_serial_and_batch_bundle(
self,
row,
frappe._dict(
@@ -2492,9 +2665,13 @@ class StockEntry(StockController):
}
),
)
-
- args["serial_and_batch_bundle"] = id
- self.add_finished_goods(args, item)
+ if existing_row:
+ existing_row.serial_and_batch_bundle = _id
+ existing_row.use_serial_batch_fields = 0
+ else:
+ args["serial_and_batch_bundle"] = _id
+ args["use_serial_batch_fields"] = 0
+ self.add_finished_goods(args, item)
def add_finished_goods(self, args, item):
self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no)
@@ -3304,6 +3481,12 @@ def move_sample_to_retention_warehouse(company, items):
return stock_entry.as_dict()
+def _qty_tolerance(precision: int) -> float:
+ """One unit at the column's precision -- absorbs float rounding without letting a real
+ (whole-unit) quantity divergence slip through."""
+ return 1.0 / (10**precision)
+
+
@frappe.whitelist()
def make_stock_in_entry(source_name, target_doc=None):
def set_missing_values(source, target):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_list.js b/erpnext/stock/doctype/stock_entry/stock_entry_list.js
index 8a1c808cd91..3d5e066099a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_list.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_list.js
@@ -1,11 +1,13 @@
frappe.listview_settings["Stock Entry"] = {
add_fields: [
- "`tabStock Entry`.`from_warehouse`",
- "`tabStock Entry`.`to_warehouse`",
- "`tabStock Entry`.`purpose`",
- "`tabStock Entry`.`work_order`",
- "`tabStock Entry`.`bom_no`",
- "`tabStock Entry`.`is_return`",
+ "from_warehouse",
+ "to_warehouse",
+ "purpose",
+ "work_order",
+ "bom_no",
+ "is_return",
+ "add_to_transit",
+ "per_transferred",
],
get_indicator: function (doc) {
if (doc.is_return === 1 && doc.purpose === "Material Transfer for Manufacture") {
@@ -16,6 +18,17 @@ frappe.listview_settings["Stock Entry"] = {
];
} else if (doc.docstatus === 0) {
return [__("Draft"), "red", "docstatus,=,0"];
+ } else if (
+ doc.docstatus === 1 &&
+ doc.add_to_transit === 1 &&
+ doc.purpose === "Material Transfer" &&
+ doc.per_transferred < 100
+ ) {
+ return [
+ __("In Transit"),
+ "yellow",
+ "docstatus,=,1|add_to_transit,=,1|purpose,=,Material Transfer|per_transferred,<,100",
+ ];
} else if (doc.purpose === "Send to Warehouse" && doc.per_transferred < 100) {
// not delivered & overdue
return [__("Goods In Transit"), "grey", "per_transferred,<,100"];
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 073a3e2d117..870091932b6 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -235,7 +235,15 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry)
self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail)
- # create add to transit
+ transit_entry.reload()
+ self.assertEqual(transit_entry.per_transferred, 0)
+
+ end_transit_entry.to_warehouse = "Test To Warehouse - _TC"
+ end_transit_entry.items[0].t_warehouse = "Test To Warehouse - _TC"
+ end_transit_entry.save().submit()
+
+ transit_entry.reload()
+ self.assertEqual(transit_entry.per_transferred, 100)
def test_material_receipt_gl_entry(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -2236,6 +2244,47 @@ class TestStockEntry(FrappeTestCase):
se.save()
se.submit()
+ def test_disassemble_blocks_finished_good_qty_tampering(self):
+ # A disassembly consuming N finished goods must consume exactly N (in stock UOM).
+ # Switching the finished-good row to a larger UOM with a tiny conversion_factor previously
+ # let a user consume ~0 finished goods while still producing the full raw materials --
+ # minting inventory. The quantity invariant must reject this.
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ fg_item = make_item("_Disassemble Mint FG", properties={"is_stock_item": 1}).name
+ rm_item1 = make_item("_Disassemble Mint RM1", properties={"is_stock_item": 1}).name
+ rm_item2 = make_item("_Disassemble Mint RM2", properties={"is_stock_item": 1}).name
+ warehouse = "_Test Warehouse - _TC"
+
+ # Give the finished good a non-stock UOM. When uom == stock_uom the system resets the
+ # conversion_factor to 1, so the tamper is only possible (and worth guarding) on a
+ # non-stock UOM, where the user-supplied conversion_factor is preserved.
+ if not frappe.db.get_value("UOM Conversion Detail", {"parent": fg_item, "uom": "Box"}):
+ item_doc = frappe.get_doc("Item", fg_item)
+ item_doc.append("uoms", {"uom": "Box", "conversion_factor": 0.01})
+ item_doc.save(ignore_permissions=True)
+
+ make_stock_entry(item_code=fg_item, target=warehouse, qty=100, purpose="Material Receipt")
+ bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
+
+ se = make_stock_entry(item_code=fg_item, qty=100, purpose="Disassemble", do_not_save=True)
+ se.from_bom = 1
+ se.use_multi_level_bom = 1
+ se.bom_no = bom_no
+ se.fg_completed_qty = 100
+ se.from_warehouse = warehouse
+ se.to_warehouse = warehouse
+ se.get_items()
+
+ # Tamper the finished-good row: a tiny conversion factor on the larger UOM means only
+ # 100 * 0.01 = 1 unit is actually consumed, while raw materials are still produced at full
+ # quantity. The finished-good consumption invariant must reject the save.
+ fg_row = next(d for d in se.items if d.is_finished_item)
+ fg_row.uom = "Box"
+ fg_row.conversion_factor = 0.01
+
+ self.assertRaises(frappe.ValidationError, se.save)
+
def test_raw_material_missing_validation(self):
original_value = frappe.db.get_single_value("Manufacturing Settings", "material_consumption")
frappe.db.set_single_value("Manufacturing Settings", "material_consumption", 0)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 085383f4abf..b3e330a0a85 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -9,14 +9,14 @@ from frappe.utils import add_to_date, cint, cstr, flt, get_datetime, now
import erpnext
from erpnext.accounts.utils import get_company_default
-from erpnext.controllers.stock_controller import StockController, create_repost_item_valuation_entry
+from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_available_batches, get_batch_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_available_serial_nos,
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_stock_balance
+from erpnext.stock.utils import get_incoming_rate, get_stock_balance
class OpeningEntryAccountError(frappe.ValidationError):
@@ -842,22 +842,6 @@ class StockReconciliation(StockController):
sl_entries.append(args)
- def update_valuation_rate_for_serial_no(self):
- for d in self.items:
- if not d.serial_no:
- continue
-
- serial_nos = get_serial_nos(d.serial_no)
- self.update_valuation_rate_for_serial_nos(d, serial_nos)
-
- def update_valuation_rate_for_serial_nos(self, row, serial_nos):
- valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate
- if valuation_rate is None:
- return
-
- for d in serial_nos:
- frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate)
-
def get_sle_for_items(self, row, serial_nos=None, current_bundle=True):
"""Insert Stock Ledger Entries"""
@@ -1011,11 +995,6 @@ class StockReconciliation(StockController):
d.quantity_difference = flt(d.qty) - flt(d.current_qty)
d.amount_difference = flt(d.amount) - flt(d.current_amount)
- def get_items_for(self, warehouse):
- self.items = []
- for item in get_items(warehouse, self.posting_date, self.posting_time, self.company):
- self.append("items", item)
-
def submit(self):
if len(self.items) > 100:
msgprint(
@@ -1038,145 +1017,6 @@ class StockReconciliation(StockController):
else:
self._cancel()
- def add_missing_stock_ledger_entry(self, row, voucher_detail_no, sle_creation):
- if row.current_qty == 0:
- return
-
- new_sle = frappe.get_doc(self.get_sle_for_items(row))
- new_sle.actual_qty = row.current_qty * -1
- new_sle.valuation_rate = row.current_valuation_rate
- new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
- new_sle.flags.ignore_permissions = 1
- new_sle.submit()
-
- creation = add_to_date(sle_creation, seconds=-1)
- new_sle.db_set("creation", creation)
-
- if not frappe.db.exists(
- "Repost Item Valuation",
- {"item": row.item_code, "warehouse": row.warehouse, "docstatus": 1, "status": "Queued"},
- ):
- create_repost_item_valuation_entry(
- {
- "based_on": "Item and Warehouse",
- "item_code": row.item_code,
- "warehouse": row.warehouse,
- "company": self.company,
- "allow_negative_stock": 1,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- }
- )
-
- def has_negative_stock_allowed(self):
- allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- if allow_negative_stock:
- return True
-
- if any(
- ((d.serial_and_batch_bundle or d.batch_no) and flt(d.qty) == flt(d.current_qty))
- for d in self.items
- ):
- allow_negative_stock = True
-
- return allow_negative_stock
-
- def get_current_qty_for_serial_or_batch(self, row, sle_creation):
- doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
- current_qty = 0.0
- if doc.has_serial_no:
- current_qty = self.get_current_qty_for_serial_nos(doc, sle_creation)
- elif doc.has_batch_no:
- current_qty = self.get_current_qty_for_batch_nos(doc, sle_creation)
-
- return abs(current_qty)
-
- def get_current_qty_for_serial_nos(self, doc, sle_creation):
- serial_nos_details = get_available_serial_nos(
- frappe._dict(
- {
- "item_code": doc.item_code,
- "warehouse": doc.warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "creation": sle_creation,
- "voucher_no": self.name,
- "ignore_warehouse": 1,
- }
- )
- )
-
- if not serial_nos_details:
- return 0.0
-
- doc.delete_serial_batch_entries()
- current_qty = 0.0
- for serial_no_row in serial_nos_details:
- current_qty += 1
- doc.append(
- "entries",
- {
- "serial_no": serial_no_row.serial_no,
- "qty": -1,
- "warehouse": doc.warehouse,
- "batch_no": serial_no_row.batch_no,
- },
- )
-
- doc.set_incoming_rate(save=True)
- doc.calculate_qty_and_amount(save=True)
- doc.db_update_all()
-
- return current_qty
-
- def get_current_qty_for_batch_nos(self, doc, sle_creation):
- current_qty = 0.0
- precision = doc.entries[0].precision("qty")
- for d in doc.entries:
- qty = (
- get_batch_qty(
- d.batch_no,
- doc.warehouse,
- creation=sle_creation,
- posting_date=doc.posting_date,
- posting_time=doc.posting_time,
- ignore_voucher_nos=[doc.voucher_no],
- for_stock_levels=True,
- consider_negative_batches=True,
- do_not_check_future_batches=True,
- )
- or 0
- ) * -1
-
- if flt(d.qty, precision) != flt(qty, precision):
- d.db_set("qty", qty)
-
- current_qty += qty
-
- return current_qty
-
-
-def get_batch_qty_for_stock_reco(
- item_code, warehouse, batch_no, posting_date, posting_time, voucher_no, sle_creation
-):
- qty = (
- get_batch_qty(
- batch_no,
- warehouse,
- item_code,
- creation=sle_creation,
- posting_date=posting_date,
- posting_time=posting_time,
- ignore_voucher_nos=[voucher_no],
- for_stock_levels=True,
- consider_negative_batches=True,
- do_not_check_future_batches=True,
- )
- or 0
- )
-
- return flt(qty)
-
@frappe.whitelist()
def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False):
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 93fac84d60a..238b72fa957 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -11,6 +11,7 @@ from frappe.query_builder.functions import Abs, Count
from frappe.utils import cint, date_diff, flt, get_datetime
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
from erpnext.stock.valuation import round_off_if_near_zero
Filters = frappe._dict
@@ -936,9 +937,7 @@ class FIFOSlots:
)
)
- for field in ["item_code"]:
- if self.filters.get(field):
- query = query.where(sle[field] == self.filters.get(field))
+ query = self._apply_filter(query, sle, "item_code")
if self.filters.get("warehouse"):
query = self._get_warehouse_conditions(sle, query)
@@ -985,9 +984,7 @@ class FIFOSlots:
)
)
- for field in ["item_code"]:
- if self.filters.get(field):
- query = query.where(sle[field] == self.filters.get(field))
+ query = self._apply_filter(query, sle, "item_code")
if self.filters.get("warehouse"):
query = self._get_warehouse_conditions(sle, query)
@@ -1020,27 +1017,25 @@ class FIFOSlots:
"has_batch_no",
)
- if self.filters.get("item_code"):
- item = item.where(item_table.item_code == self.filters.get("item_code"))
+ item = self._apply_filter(item, item_table, "item_code")
if self.filters.get("brand"):
item = item.where(item_table.brand == self.filters.get("brand"))
return item
+ def _apply_filter(self, query, table, fieldname: str):
+ filter_value = self.filters.get(fieldname)
+ if not filter_value:
+ return query
+
+ if isinstance(filter_value, list | tuple | set):
+ return query.where(table[fieldname].isin(filter_value))
+
+ return query.where(table[fieldname] == filter_value)
+
def _get_warehouse_conditions(self, sle, sle_query) -> str:
- warehouse = frappe.qb.DocType("Warehouse")
- lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"])
-
- warehouse_results = (
- frappe.qb.from_(warehouse)
- .select("name")
- .where((warehouse.lft >= lft) & (warehouse.rgt <= rgt))
- .run()
- )
- warehouse_results = [x[0] for x in warehouse_results]
-
- return sle_query.where(sle.warehouse.isin(warehouse_results))
+ return apply_warehouse_filter(sle_query, sle, self.filters)
def prepare_stock_reco_voucher_wise_count(self):
self.stock_reco_voucher_wise_count = frappe._dict()
diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
index 601524c74e9..1c61306125e 100644
--- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py
@@ -129,6 +129,18 @@ class TestStockAgeing(FrappeTestCase):
self.assertEqual(queue[0][0], 10.0)
self.assertEqual(queue[1][0], 10.0)
+ def test_item_filter_supports_multi_select_values(self):
+ bundle = frappe.qb.DocType("Serial and Batch Bundle")
+ query = frappe.qb.from_(bundle).select(bundle.name)
+
+ filtered_query = FIFOSlots(frappe._dict(item_code=["Item A"]), [])._apply_filter(
+ query, bundle, "item_code"
+ )
+
+ sql = filtered_query.get_sql()
+ self.assertIn(" IN ", sql)
+ self.assertNotIn("=[", sql)
+
def test_basic_stock_reconciliation(self):
"""
Ledger (same wh): [+30, reco reset >> 50, -10]
diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py
index 446437bbb32..8f153105e3e 100644
--- a/erpnext/templates/pages/projects.py
+++ b/erpnext/templates/pages/projects.py
@@ -64,21 +64,6 @@ def get_tasks(project, start=0, search=None, item_status=None):
return list(filter(lambda x: not x.parent_task, tasks))
-@frappe.whitelist()
-def get_task_html(project, start=0, item_status=None):
- return frappe.render_template(
- "erpnext/templates/includes/projects/project_tasks.html",
- {
- "doc": {
- "name": project,
- "project_name": project,
- "tasks": get_tasks(project, start, item_status=item_status),
- }
- },
- is_path=True,
- )
-
-
def get_timesheets(project, start=0, search=None):
filters = {"project": project}
if search:
@@ -104,15 +89,6 @@ def get_timesheets(project, start=0, search=None):
return timesheets
-@frappe.whitelist()
-def get_timesheet_html(project, start=0):
- return frappe.render_template(
- "erpnext/templates/includes/projects/project_timesheets.html",
- {"doc": {"timesheets": get_timesheets(project, start)}},
- is_path=True,
- )
-
-
def get_attachments(project):
return frappe.get_all(
"File",