Compare commits

..

1 Commits

Author SHA1 Message Date
barredterra
ad0d3e1fbf fix(manufacturing): classify MRP rows by purchase item, not missing BOM 2026-08-27 17:55:11 +02:00
331 changed files with 8928 additions and 21720 deletions

View File

@@ -4,36 +4,6 @@ set -e
cd ~ || exit
# Authenticate git against github.com with the job token: anonymous git-over-HTTPS from the
# runners gets throttled to a 401, which kills whichever clone is in flight — the frappe fetch
# below, or payments under `bench get-app`. See the PR description.
#
# A credential helper rather than a url.insteadOf rewrite, because `git clone` PERSISTS a
# rewritten URL into the new repo's .git/config: an insteadOf would leave the token sitting in
# apps/payments/.git/config on the runner. A helper is consulted only when github.com actually
# challenges, and leaves the stored remote URL untouched. Passing it through GIT_CONFIG_* keeps
# the token out of ~/.gitconfig too, and child processes inherit it (bench shells out to git).
ci_github_token=${CI_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}
if [ -n "$ci_github_token" ]; then
export CI_GITHUB_TOKEN="$ci_github_token"
export GIT_CONFIG_COUNT=3
# Reset first: git runs EVERY configured helper and calls `store` on them after a successful
# auth, so a `credential.helper=store` inherited from the image's gitconfig would write the
# token to ~/.git-credentials. An empty value clears the list before ours is added.
export GIT_CONFIG_KEY_0="credential.helper"
export GIT_CONFIG_VALUE_0=""
export GIT_CONFIG_KEY_1="credential.https://github.com.username"
export GIT_CONFIG_VALUE_1="x-access-token"
export GIT_CONFIG_KEY_2="credential.https://github.com.helper"
# Single-quoted: $CI_GITHUB_TOKEN is expanded by the shell git runs the helper in, so the
# token is read from the environment at call time and never stored anywhere. Answering only
# `get` makes the helper inert for git's `store`/`erase` calls.
export GIT_CONFIG_VALUE_2='!f() { test "$1" = get && echo "password=$CI_GITHUB_TOKEN"; }; f'
fi
# Whatever happens, never sit on a credential prompt: fail fast and legibly instead.
export GIT_TERMINAL_PROMPT=0
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-}
@@ -218,7 +188,7 @@ restore_warm_bench() {
# Phase 1 already fetched ~/frappe to the exact live develop SHA. Fetch that commit
# straight from it (bench init names the remote 'upstream', not 'origin', and points
# it at this local clone — so a plain `git fetch origin` does not work).
git fetch --no-tags --update-shallow "$HOME/frappe" HEAD || exit 1
git fetch --no-tags "$HOME/frappe" HEAD || exit 1
git checkout --force FETCH_HEAD || exit 1
); then
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"

View File

@@ -121,8 +121,6 @@ jobs:
env:
DB: mariadb
TYPE: server
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
- name: Run Patch Tests
run: |

View File

@@ -129,8 +129,6 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
- name: Run Tests
run: |

View File

@@ -102,8 +102,6 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
DB_HOST: 127.0.0.1
DB_USER_HOST: '%'
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb

View File

@@ -103,8 +103,6 @@ jobs:
DB: postgres
TYPE: server
FRAPPE_BRANCH: develop
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
BENCH_CACHE_DIR: /home/runner/bench-cache
- name: Warm up test data

View File

@@ -1489,10 +1489,10 @@ balanced-match@^4.0.2:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
baseline-browser-mapping@^2.11.12:
version "2.11.20"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz#26078c7a4b08299656ea7ddceaebec955dc44303"
integrity sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==
baseline-browser-mapping@^2.10.38:
version "2.10.40"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz#f372c8eb36ff4ad0b5e7ae467014abef124554ba"
integrity sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==
brace-expansion@^5.0.5:
version "5.0.7"
@@ -1509,15 +1509,15 @@ brace-expansion@^5.0.8:
balanced-match "^4.0.2"
browserslist@^4.24.0:
version "4.28.8"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.8.tgz#a3c79ceb70028527e5da7dafc887f3200b5168c0"
integrity sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==
version "4.28.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.4.tgz#dd8b8167a32845ff5f8cd6ce13f5abba16cd04c9"
integrity sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==
dependencies:
baseline-browser-mapping "^2.11.12"
caniuse-lite "^1.0.30001809"
electron-to-chromium "^1.5.402"
node-releases "^2.0.53"
update-browserslist-db "^1.3.0"
baseline-browser-mapping "^2.10.38"
caniuse-lite "^1.0.30001799"
electron-to-chromium "^1.5.376"
node-releases "^2.0.48"
update-browserslist-db "^1.2.3"
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
@@ -1527,10 +1527,10 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
es-errors "^1.3.0"
function-bind "^1.1.2"
caniuse-lite@^1.0.30001809:
version "1.0.30001810"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz#4970b477dea3278374de9bc43aa8f5d39fc3cda2"
integrity sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==
caniuse-lite@^1.0.30001799:
version "1.0.30001800"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz#b896c773e1c39400809415162bb5320371291b36"
integrity sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==
ccount@^2.0.0:
version "2.0.1"
@@ -1697,10 +1697,10 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
electron-to-chromium@^1.5.402:
version "1.5.420"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz#fc66d26a722d6f227e2092acdf38dd55b198cb44"
integrity sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==
electron-to-chromium@^1.5.376:
version "1.5.383"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz#5bd22306497d454103b289b0fef97260c56d0855"
integrity sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==
engine.io-client@~6.5.1:
version "6.5.4"
@@ -3012,10 +3012,10 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
node-releases@^2.0.53:
version "2.0.54"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.54.tgz#09af17d5647aa9f221ec5cf2becb95b68a981afe"
integrity sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==
node-releases@^2.0.48:
version "2.0.50"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969"
integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==
object-assign@^4.1.1:
version "4.1.1"
@@ -3589,10 +3589,10 @@ unist-util-visit@^5.0.0:
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
update-browserslist-db@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz#9d99fbff56c50bb11ba5fd35cece5916da595836"
integrity sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==
update-browserslist-db@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"

View File

@@ -122,7 +122,6 @@
"description": "Setting Account Type helps in selecting this Account in transactions.",
"fieldname": "account_type",
"fieldtype": "Select",
"in_preview": 1,
"in_standard_filter": 1,
"label": "Account Type",
"oldfieldname": "account_type",
@@ -204,7 +203,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-09-03 12:59:42.190900",
"modified": "2026-08-21 23:11:37.851001",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",

View File

@@ -52,42 +52,6 @@ frappe.treeview_settings["Account"] = {
],
root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the account number renders as a badge (see
// onrender) instead of being glued into the name
return frappe.utils.escape_html(node.data.account_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.account_number) {
flags.push(frappe.ui.badge({ label: node.data.account_number }));
}
const company = frappe.treeview_settings["Account"].treeview?.page?.fields_dict?.company?.get_value();
const company_currency = company && erpnext.get_currency(company);
if (
node.data.account_currency &&
company_currency &&
node.data.account_currency !== company_currency
) {
flags.push(frappe.ui.badge({ label: node.data.account_currency, theme: "blue" }));
}
if (node.data.freeze_account === "Yes") {
flags.push(
frappe.ui.badge({
label: __("Frozen"),
icon: "lock",
title: __("Frozen - entries restricted"),
theme: "orange",
})
);
}
erpnext.utils.render_tree_node_flags(node, flags);
},
on_node_render: function (node, deep) {
const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
@@ -268,7 +232,7 @@ frappe.treeview_settings["Account"] = {
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
if (treeview.can_create) {
treeview.page.set_primary_action(
{ label: __("Add Account"), short_label: __("Add") },
__("New"),
function () {
let root_company = treeview.page.fields_dict.root_company.get_value();
if (root_company) {
@@ -279,14 +243,13 @@ frappe.treeview_settings["Account"] = {
treeview.new_node();
}
},
"plus"
"add"
);
}
},
toolbar: [
{
label: __("Add Child"),
icon: "plus",
condition: function (node) {
return (
frappe.boot.user.can_create.indexOf("Account") !== -1 &&
@@ -309,7 +272,6 @@ frappe.treeview_settings["Account"] = {
return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1;
},
label: __("View Ledger"),
icon: "book-open",
click: function (node, btn) {
frappe.route_options = {
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
@@ -324,106 +286,6 @@ frappe.treeview_settings["Account"] = {
},
btnClass: "hidden-xs",
},
{
// same label and mechanism as the Account form's Actions button:
// NOT frappe's generic rename (Allow Rename stays off) — this is
// ERPNext's controlled update that rebuilds the derived
// "number - name - abbr" document name
label: __("Update Account Name / Number"),
icon: "text-cursor-input",
condition: function (node) {
return !node.is_root && frappe.model.can_write("Account");
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __("Update Account Number / Name"),
fields: [
{
fieldtype: "Data",
fieldname: "account_name",
label: __("Account Name"),
reqd: 1,
default: node.data.account_name,
},
{
fieldtype: "Data",
fieldname: "account_number",
label: __("Account Number"),
default: node.data.account_number,
},
],
primary_action_label: __("Update"),
primary_action(values) {
dialog.hide();
frappe.dom.freeze(__("Updating {0}", [node.label]));
frappe.call({
method: "erpnext.accounts.doctype.account.account.update_account_number",
args: {
name: node.label,
account_name: values.account_name,
account_number: values.account_number,
},
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees["Account"];
node.parent_node && treeview.tree.load_children(node.parent_node);
},
always: function () {
frappe.dom.unfreeze();
},
});
},
});
dialog.show();
},
},
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Account");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Account")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -4,18 +4,29 @@
import frappe
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(ERPNextTestSuite):
def setUp(self):
frappe.clear_cache()
create_warehouse(
warehouse_name="_Test Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"},
company="_Test Company",
)
create_item("_Test Item")
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
make_bank_account()
add_transactions()
@@ -128,8 +139,11 @@ def add_transactions():
def make_payment_entry():
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
supplier = create_supplier(supplier_name="_Test Supplier")
pi = make_purchase_invoice(
supplier="_Test Supplier",
supplier=supplier.name,
supplier_warehouse="_Test Warehouse - _TC",
expense_account="Cost of Goods Sold - _TC",
uom="Nos",
@@ -144,6 +158,10 @@ def make_payment_entry():
def make_pos_sales_invoice():
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
@@ -152,13 +170,13 @@ def make_pos_sales_invoice():
)
mode_of_payment.save()
customer = make_customer(customer="_Test Customer")
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
si = create_sales_invoice(
customer="_Test Customer", item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1
)
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
si.set("payments", [])
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
si.insert()

View File

@@ -23,6 +23,8 @@ from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed
class TestBankTransaction(ERPNextTestSuite):
def setUp(self):
make_pos_profile()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
@@ -30,7 +32,6 @@ class TestBankTransaction(ERPNextTestSuite):
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
)
make_pos_profile()
add_transactions(bank_account=bank_account)
add_vouchers(gl_account=gl_account)
@@ -46,7 +47,7 @@ class TestBankTransaction(ERPNextTestSuite):
from_date=bank_transaction.date,
to_date=utils.today(),
)
self.assertIn("Conrad Electronic", [payment["party"] for payment in linked_payments])
self.assertEqual(linked_payments[0]["party"], "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):

View File

@@ -729,7 +729,6 @@ def get_ordered_amount(params):
(child.item_code == item_code)
& (parent.docstatus == 1)
& (child.amount > child.billed_amt)
& (child.closed == 0)
& (parent.status != "Closed")
& Criterion.all(get_other_condition(params, child, parent, "Purchase Order"))
)

View File

@@ -16,8 +16,6 @@ frappe.ui.form.on("Chart of Accounts Importer", {
() => generate_tree_preview(frm),
() => create_import_button(frm),
() => frm.set_df_property("chart_preview", "hidden", 0),
// the preview is the point of this page — open it right away
() => frm.fields_dict.chart_preview.collapse(false),
]);
}
@@ -130,6 +128,7 @@ var create_import_button = function (frm) {
freeze_message: __("Creating Accounts..."),
callback: function (r) {
if (!r.exc) {
clearInterval(frm.page["interval"]);
frm.page.set_indicator(__("Import Successful"), "blue");
create_reset_button(frm);
}
@@ -143,95 +142,42 @@ var create_reset_button = function (frm) {
frm.page
.set_primary_action(__("Reset"), function () {
frm.page.clear_primary_action();
delete frm.page["show_import_button"];
frm.reload_doc();
})
.addClass("btn btn-primary");
};
var generate_tree_preview = function (frm) {
let parent = __("All Accounts");
const wrapper = $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
// search + expand/collapse-all lean on frappe.ui.Tree helpers added with
// row mode; when running against an older frappe that predates them, skip
// this toolbar so the preview still renders (just without the extras)
const has_row_helpers =
typeof frappe.ui.Tree.prototype.get_expansion_state === "function" &&
typeof frappe.ui.Tree.prototype.filter_nodes === "function";
let tree;
let deep_loaded = false;
let search_text = "";
let update_buttons = () => {};
if (has_row_helpers) {
// same toolbar anatomy as the tree view: search on the left,
// expand/collapse-all on the right (three-state: fully collapsed ->
// Expand All, fully expanded -> Collapse All, partially expanded -> both)
const $toolbar = $('<div class="flex items-center gap-2 mb-2"></div>').appendTo(wrapper);
const search_control = frappe.ui.form.make_control({
df: { fieldtype: "Data", fieldname: "preview_search", placeholder: __("Search") },
parent: $toolbar,
only_input: true,
});
search_control.refresh();
$(search_control.wrapper).addClass("m-0").css("width", "220px");
search_control.$input.addClass("input-xs");
search_control.$input.on(
"input",
frappe.utils.debounce(() => {
search_text = search_control.$input.val();
const run = () => {
// a newer keystroke superseded this one while the deep load ran
if (search_text !== search_control.$input.val()) return;
tree.filter_nodes(search_text);
};
if (!search_text || deep_loaded) {
run();
return;
var validate_coa = function (frm) {
if (frm.doc.import_file) {
let parent = __("All Accounts");
return frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
args: {
file_name: frm.doc.import_file,
parent: parent,
doctype: "Chart of Accounts Importer",
file_type: frm.doc.file_type,
for_validate: 1,
},
callback: function (r) {
if (r.message["show_import_button"]) {
frm.page["show_import_button"] = Boolean(r.message["show_import_button"]);
}
tree.load_children(tree.root_node, true).then(() => {
deep_loaded = true;
run();
});
}, 300)
);
const $actions = $('<div class="ms-auto flex items-center gap-1"></div>').appendTo($toolbar);
update_buttons = () => {
const state = tree.get_expansion_state();
$expand_all.prop("disabled", !(state === "collapsed" || state === "partial"));
$collapse_all.prop("disabled", !(state === "expanded" || state === "partial"));
};
// tooltip on a wrapper: a disabled es-button has pointer-events:none,
// so hover falls through to the wrapper and the tooltip still shows
const make_action = (icon, label, onclick) => {
const $btn = $(
frappe.ui.button({ icon, disabled: true, onclick, attrs: { "aria-label": label } })
);
const $wrapper = $('<span class="inline-flex"></span>').append($btn).appendTo($actions);
frappe.ui.tooltip($wrapper, { text: label });
return $btn;
};
var $expand_all = make_action("chevrons-up-down", __("Expand All"), () => {
tree.load_children(tree.root_node, true).then(() => {
deep_loaded = true;
});
});
var $collapse_all = make_action("chevrons-down-up", __("Collapse All"), () => {
tree.load_children(tree.root_node, false);
},
});
}
};
var generate_tree_preview = function (frm) {
let parent = __("All Accounts");
$(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
// generate tree structure based on the csv data
tree = new frappe.ui.Tree({
parent: wrapper,
return new frappe.ui.Tree({
parent: $(frm.fields_dict["chart_tree"].wrapper),
label: parent,
expandable: true,
// read-only preview: row-mode visuals without actions or hover cards
// (ignored by an older frappe, which renders the legacy tree)
row_style: true,
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
args: {
file_name: frm.doc.import_file,
@@ -239,9 +185,8 @@ var generate_tree_preview = function (frm) {
doctype: "Chart of Accounts Importer",
file_type: frm.doc.file_type,
},
on_node_render: () => update_buttons(),
// expanded flips right after this callback — check on the next tick
on_click: () => setTimeout(update_buttons, 0),
onclick: function (node) {
parent = node.value;
},
});
return tree;
};

View File

@@ -12,19 +12,6 @@ frappe.treeview_settings["Cost Center"] = {
],
root_label: "Cost Centers",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the number renders as a badge (see onrender)
return frappe.utils.escape_html(node.data.cost_center_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.cost_center_number) {
flags.push(frappe.ui.badge({ label: node.data.cost_center_number }));
}
erpnext.utils.render_tree_node_flags(node, flags);
},
add_tree_node: "erpnext.accounts.utils.add_cc",
menu_items: [
{
@@ -55,37 +42,6 @@ frappe.treeview_settings["Cost Center"] = {
},
],
ignore_fields: ["parent_cost_center"],
toolbar: [
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Cost Center");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Cost Center")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
onload: function (treeview) {
function get_company() {
return treeview.page.fields_dict.company.get_value();
@@ -126,22 +82,3 @@ frappe.treeview_settings["Cost Center"] = {
);
},
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -234,10 +234,8 @@ frappe.ui.form.on("Dunning", {
dn: frm.doc.name,
},
callback: function (r) {
if (!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
},
});
},

View File

@@ -31,4 +31,11 @@ class TestFinanceBook(ERPNextTestSuite):
def create_finance_book():
return frappe.get_doc("Finance Book", "Test Finance Book 1")
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
finance_book = frappe.get_doc(
{"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
).insert()
else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
return finance_book

View File

@@ -32,7 +32,6 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_validat
AccountFilterValidator,
CalculationFormulaValidator,
DependencyValidator,
get_valid_api_method,
)
from erpnext.accounts.report.financial_statements import (
get_columns,
@@ -1183,12 +1182,10 @@ class RowProcessor:
def _process_api_row(self, row) -> RowData:
api_path = row.calculation_formula
method = get_valid_api_method(api_path)
# TODO
try:
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-codeinjection-eval
values = frappe.call(method, filters=self.context.filters, periods=self.period_list, row=row)
values = frappe.call(api_path, filters=self.context.filters, periods=self.period_list, row=row)
if row.reverse_sign:
values = [-1 * v for v in values]

View File

@@ -236,8 +236,6 @@ async function refresh_tree_view(dialog, account_rows) {
parent: wrapper,
label: company,
root_value: company,
// read-only preview: row-mode visuals without actions
row_style: true,
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
toolbar: [],
@@ -372,7 +370,7 @@ function update_formula_description(frm, data_source) {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Custom API Setup</h5>
<p ${text_style}>Path to your custom whitelisted method that returns financial data. It must permit GET requests.</p>
<p ${text_style}>Path to your custom method that returns financial data.</p>
<h6 ${subtitle_style}>Format:</h6>
<ul ${list_style}>
@@ -382,8 +380,7 @@ function update_formula_description(frm, data_source) {
<h6 ${subtitle_style}>Method Signature:</h6>
<div ${code_style}>
<!-- &#10; is used for line breaks since frappe.render replaces newlines with spaces -->
<pre ${pre_style} class="language-python">@frappe.whitelist(methods=["GET"])&#10;def get_custom_data(filters, periods, row):&#10; # filters: dict — report filters (company, period, etc.)&#10; # periods: list[dict] — period definitions&#10; # row: dict — the current report row&#10;&#10; return [1000.0, 1200.0, 1150.0] # one value per period</pre>
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br>&nbsp; # filters: dict — report filters (company, period, etc.) <br>&nbsp; # periods: list[dict] — period definitions <br>&nbsp; # row: dict — the current report row <br><br>&nbsp; return [1000.0, 1200.0, 1150.0] # one value per period</pre>
</div>
<h6 ${subtitle_style}>Return Format:</h6>

View File

@@ -8,25 +8,10 @@ from dataclasses import dataclass, field
from typing import Any
import frappe
from frappe import _, is_whitelisted
from frappe import _
from frappe.database.operator_map import OPERATOR_MAP
def get_valid_api_method(api_path: str):
"""Resolve `api_path`, ensuring it is whitelisted and permits GET (i.e. read-only)."""
method = frappe.get_attr(api_path)
is_whitelisted(method)
if "GET" not in frappe.allowed_http_methods_for_whitelisted_func.get(method, ()):
frappe.throw(
_("Method {0} must permit GET requests").format(frappe.bold(api_path)),
frappe.PermissionError,
title=_("Method Not Allowed"),
)
return method
def get_formula_field_label(data_source: str) -> str:
# Must mirror the `labels` map in financial_report_template.js (update_formula_label),
labels = {
@@ -190,10 +175,8 @@ class TemplateStructureValidator(Validator):
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message=_("{0} is required when {1} is {2}").format(
get_formula_field_label(row.data_source),
row.meta.get_translated_label("data_source"),
_(row.data_source),
message=_("{0} is required for {1}").format(
get_formula_field_label(row.data_source), row.data_source
),
row_idx=row.idx,
)
@@ -221,14 +204,7 @@ class DependencyValidator(Validator):
for row in self.template.rows:
if row.reference_code and row.data_source == "Calculated Amount" and row.calculation_formula:
# skip self-reference, `CalculationFormulaValidator` already reports it
deps = [
code
for code in extract_reference_codes_from_formula(
row.calculation_formula, list(available_codes)
)
if code != row.reference_code
]
deps = extract_reference_codes_from_formula(row.calculation_formula, list(available_codes))
if deps:
graph[row.reference_code] = deps
@@ -290,9 +266,7 @@ class DependencyValidator(Validator):
row_idx = self._get_row_idx(ref_code)
result.add_error(
ValidationIssue(
message=_("Line references undefined in {0}: {1}").format(
get_formula_field_label("Calculated Amount"), ", ".join(undefined)
),
message=_("Line References undefined in Formula: {0}").format(", ".join(undefined)),
row_idx=row_idx,
)
)
@@ -319,6 +293,17 @@ class CalculationFormulaValidator(Validator):
if row.data_source != "Calculated Amount":
return result
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message=_("{0} is required for Calculated Amount").format(
get_formula_field_label(row.data_source)
),
row_idx=row.idx,
)
)
return result
formula = self._preprocess_formula(row.calculation_formula)
row.calculation_formula = formula
@@ -343,6 +328,16 @@ class CalculationFormulaValidator(Validator):
)
)
# Check undefined references
undefined = set(refs) - set(available_codes)
if undefined:
result.add_error(
ValidationIssue(
message=_("Formula references undefined codes: {0}").format(", ".join(undefined)),
row_idx=row.idx,
)
)
# Try to evaluate with dummy values
eval_error = self._test_formula_evaluation(formula, available_codes)
if eval_error:
@@ -405,6 +400,17 @@ class AccountFilterValidator(Validator):
if row.data_source != "Account Data":
return result
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message=_("{0} is required for Account Data").format(
get_formula_field_label(row.data_source)
),
row_idx=row.idx,
)
)
return result
try:
filter_config = json.loads(row.calculation_formula)
error = self._validate_filter_structure(
@@ -416,9 +422,7 @@ class AccountFilterValidator(Validator):
if error:
result.add_error(
ValidationIssue(
message=_("[{0}] {1}", context="Financial Report Template").format(
get_formula_field_label(row.data_source), error
),
message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error),
row_idx=row.idx,
)
)
@@ -426,9 +430,8 @@ class AccountFilterValidator(Validator):
except json.JSONDecodeError as e:
result.add_error(
ValidationIssue(
message=_("[{0}] {1}", context="Financial Report Template").format(
get_formula_field_label(row.data_source),
_("Invalid JSON format: {0}").format(str(e)),
message=_("{0}: Invalid JSON format: {1}").format(
get_formula_field_label(row.data_source), str(e)
),
row_idx=row.idx,
)
@@ -528,24 +531,29 @@ class FormulaValidator(Validator):
)
return result
# Method exists?
try:
get_valid_api_method(api_path)
module_path, method_name = api_path.rsplit(".", 1)
module = frappe.get_module(module_path)
if not hasattr(module, method_name):
result.add_error(
ValidationIssue(
message=_(
"{0}: Method '{1}' not found in module '{2}' (might be environment-specific)"
).format(get_formula_field_label(row.data_source), method_name, module_path),
row_idx=row.idx,
)
)
except Exception as e:
if isinstance(e, frappe.PermissionError | frappe.ValidationError):
# frappe.throw inside get_valid_api_method logs a message that would pop up in UI
frappe.clear_last_message()
if isinstance(e, frappe.PermissionError):
message = _("[{0}] {1}", context="Financial Report Template").format(
get_formula_field_label(row.data_source),
_("Method '{0}' must be whitelisted and permit GET requests").format(api_path),
result.add_error(
ValidationIssue(
message=_("Could not validate {0}: {1}").format(
get_formula_field_label(row.data_source), str(e)
),
row_idx=row.idx,
)
else:
message = _("Could not validate {0}: {1}").format(
get_formula_field_label(row.data_source), str(e)
)
result.add_error(ValidationIssue(message=message, row_idx=row.idx))
)
return result

View File

@@ -2,12 +2,7 @@
# For license information, please see license.txt
import frappe
from frappe.tests.utils import whitelist_for_tests
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import (
FormulaValidator,
get_valid_api_method,
)
from erpnext.tests.utils import ERPNextTestSuite
@@ -77,90 +72,3 @@ class FinancialReportTemplateTestCase(ERPNextTestSuite):
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
)
return template
def not_whitelisted_method(**kwargs):
return [42.0]
@whitelist_for_tests(methods=["POST"])
def whitelisted_post_only_method(**kwargs):
return [42.0]
@whitelist_for_tests(methods=["GET"])
def whitelisted_get_method(**kwargs):
return [42.0]
class TestCustomAPIValidation(FinancialReportTemplateTestCase):
"""Custom API rows must point to whitelisted methods that permit GET"""
TEST_MODULE = "erpnext.accounts.doctype.financial_report_template.test_financial_report_template"
NOT_WHITELISTED = f"{TEST_MODULE}.not_whitelisted_method"
WHITELISTED_POST_ONLY = f"{TEST_MODULE}.whitelisted_post_only_method"
WHITELISTED_GET = f"{TEST_MODULE}.whitelisted_get_method"
def create_api_template(self, api_path):
template = self.create_test_template_with_rows(
[
{
"reference_code": "API001",
"display_name": "API Row",
"data_source": "Custom API",
"calculation_formula": api_path,
}
]
)
template.report_type = "Profit and Loss Statement"
return template
def test_get_valid_api_method(self):
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.NOT_WHITELISTED)
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.WHITELISTED_POST_ONLY)
self.assertEqual(get_valid_api_method(self.WHITELISTED_GET), frappe.get_attr(self.WHITELISTED_GET))
def test_save_rejects_invalid_api_methods(self):
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
template = self.create_api_template(api_path)
self.assertRaises(frappe.ValidationError, template.insert)
def test_save_allows_get_whitelisted_method(self):
template = self.create_api_template(self.WHITELISTED_GET)
template.insert()
template.delete()
def test_engine_rejects_invalid_api_methods(self):
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
ReportContext,
RowProcessor,
)
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
template = self.create_api_template(api_path)
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
processor = RowProcessor(context)
self.assertRaises(frappe.PermissionError, processor._process_api_row, template.rows[0])
def test_engine_calls_valid_api_method(self):
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
ReportContext,
RowProcessor,
)
template = self.create_api_template(self.WHITELISTED_GET)
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
processor = RowProcessor(context)
row_data = processor._process_api_row(template.rows[0])
self.assertEqual(row_data.values, [42.0])
def test_validation_keeps_message_log_clean(self):
validator = FormulaValidator(frappe._dict(rows=[]))
message_count = len(frappe.local.message_log)
# last path raises AppNotInstalledError, which also logs a message via frappe.throw
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY, "missing_app.api.method"):
row = frappe._dict(data_source="Custom API", calculation_formula=api_path, idx=1)
result = validator.validate(row)
self.assertFalse(result.is_valid)
self.assertEqual(len(frappe.local.message_log), message_count)

View File

@@ -27,7 +27,6 @@ def get_payment_entry_against_order(
) -> dict | Document:
"""Build an advance-payment Journal Entry against an unbilled Sales/Purchase Order."""
ref_doc = frappe.get_doc(dt, dn)
ref_doc.check_permission()
if flt(ref_doc.per_billed, 2) > 0:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
@@ -79,8 +78,6 @@ def get_payment_entry_against_invoice(
) -> dict | Document:
"""Build a payment Journal Entry against a Sales/Purchase Invoice's outstanding amount."""
ref_doc = frappe.get_doc(dt, dn)
ref_doc.check_permission()
if dt == "Sales Invoice":
party_type = "Customer"
party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to
@@ -121,8 +118,6 @@ def get_payment_entry(ref_doc, args: dict) -> dict | Document:
Returns the Journal Entry document when `args["journal_entry"]` is truthy, otherwise its
dict (for client calls).
"""
frappe.has_permission("Journal Entry", ptype="create", throw=True)
je = frappe.new_doc("Journal Entry")
je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")})

View File

@@ -318,8 +318,9 @@ class TestJournalEntry(ERPNextTestSuite):
)
# the guard must not disclose the reversal to a user who cannot read the entry
with self.set_user("Guest"):
self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name)
frappe.set_user("Guest")
self.addCleanup(frappe.set_user, "Administrator")
self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name)
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD

View File

@@ -56,9 +56,7 @@ class LedgerMerge(Document):
@frappe.whitelist()
def form_start_merge(docname: str):
lm_doc = frappe.get_doc("Ledger Merge", docname)
lm_doc.check_permission("write")
return lm_doc.start_merge()
return frappe.get_doc("Ledger Merge", docname).start_merge()
def start_merge(docname):

View File

@@ -1,6 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import patch
import unittest
import frappe
from frappe.query_builder.functions import Sum
@@ -196,7 +196,7 @@ class TestLoyaltyProgram(ERPNextTestSuite):
for d in company_wise_info:
self.assertTrue(d.get("loyalty_points"))
@patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
@unittest.mock.patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
def test_tier_selection(self, mock_get_loyalty_details):
# Create a new loyalty program with multiple tiers
loyalty_program = frappe.get_doc(

View File

@@ -297,9 +297,6 @@ def start_import(invoices):
invoice_number = d.invoice_number
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
# the outstanding amount is entered inclusive of tax, so taxes must not
# be added on top of it
doc.flags.dont_auto_add_taxes = True
doc.insert(set_name=invoice_number)
doc.submit()
if not frappe.in_test:

View File

@@ -4,11 +4,9 @@
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
from erpnext.accounts.doctype.tax_rule.test_tax_rule import make_tax_rule
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.tests.utils import ERPNextTestSuite
@@ -128,55 +126,6 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
for invoice in invoices:
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"add_taxes_from_taxes_and_charges_template": 1, "add_taxes_from_item_tax_template": 0},
)
def test_opening_invoice_creation_without_taxes(self):
company = "_Test Opening Invoice Company"
template = frappe.get_doc(
{
"doctype": "Sales Taxes and Charges Template",
"company": company,
"title": "_Test Opening Invoice Tax",
"taxes": [
{
"charge_type": "On Net Total",
"account_head": create_account(
account_name="_Test Opening Tax Account",
parent_account="Duties and Taxes - _TOIC",
account_type="Tax",
company=company,
),
"description": "Test taxes",
"rate": 9,
}
],
}
).insert()
# makes the template the default for the party, as it would be on a live site
make_tax_rule(tax_type="Sales", company=company, sales_tax_template=template.name, save=1)
tool = self.make_invoices(company=company, return_doc=True)
invoices = tool.make_invoices()
self.assertEqual(len(invoices), 2)
# outstanding amount is entered inclusive of tax, so taxes must not be added on top of it
for invoice in invoices:
si = frappe.get_doc("Sales Invoice", invoice)
self.assertFalse(si.taxes)
self.assertEqual(si.grand_total, 200)
self.assertEqual(si.outstanding_amount, 200)
# the same invoice created outside the tool keeps the default taxes,
# since adding them there is the user's decision
si = frappe.get_doc(tool.get_invoices()[0])
si.flags.ignore_mandatory = True
si.insert()
self.assertTrue(si.taxes)
self.assertEqual(si.grand_total, 218)
def test_opening_entry_project_linking(self):
doc = self.make_invoices(
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True

View File

@@ -2626,11 +2626,7 @@ def get_payment_entry(
reference_date: str | date | None = None,
created_from_payment_request: bool | None = None,
):
frappe.has_permission("Payment Entry", ptype="create", throw=True)
doc = frappe.get_doc(dt, dn)
doc.check_permission()
over_billing_allowance = frappe.get_single_value("Accounts Settings", "over_billing_allowance")
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))

View File

@@ -789,6 +789,7 @@ class TestPaymentEntry(ERPNextTestSuite):
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "bank_charges_account", bank_charges_account)
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "bank_charges_account", "")
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Internal Transfer"
@@ -833,6 +834,7 @@ class TestPaymentEntry(ERPNextTestSuite):
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "bank_charges_account", bank_charges_account)
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "bank_charges_account", "")
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Internal Transfer"
@@ -1107,6 +1109,8 @@ class TestPaymentEntry(ERPNextTestSuite):
)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_account", gain_account)
frappe.db.set_value("Company", "_Test Company", "exchange_loss_account", loss_account)
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_gain_account", "")
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_loss_account", "")
si_gain = create_sales_invoice(
customer="_Test Customer USD",

View File

@@ -92,7 +92,6 @@ def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, pag
@frappe.whitelist()
def make_payment_records(name: str, supplier: str, mode_of_payment: str | None = None):
doc = frappe.get_doc("Payment Order", name)
doc.check_permission()
make_journal_entry(doc, supplier, mode_of_payment)

View File

@@ -201,6 +201,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
frappe.db.set_value("Company", self.company, "exchange_gain_account", gain_account)
frappe.db.set_value("Company", self.company, "exchange_loss_account", loss_account)
self.addCleanup(frappe.db.set_value, "Company", self.company, "exchange_gain_account", "")
self.addCleanup(frappe.db.set_value, "Company", self.company, "exchange_loss_account", "")
return gain_account, loss_account
def create_foreign_currency_sales_invoice(self, conversion_rate):
@@ -1329,6 +1331,15 @@ class TestPaymentReconciliation(ERPNextTestSuite):
test_user = "test@example.com"
permitted_ccs = ["_Test Cost Center - _TC", "_Test Cost Center 2 - _TC"]
restricted_cc = "_Test Write Off Cost Center - _TC"
existing_apply_strict_user_permissions = cint(
frappe.db.get_single_value("System Settings", "apply_strict_user_permissions")
)
self.addCleanup(
frappe.db.set_single_value,
"System Settings",
"apply_strict_user_permissions",
existing_apply_strict_user_permissions,
)
transaction_date = nowdate()
rate = 100

View File

@@ -1359,7 +1359,6 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
@frappe.whitelist()
def get_available_payment_schedules(reference_doctype: str, reference_name: str):
ref_doc = frappe.get_doc(reference_doctype, reference_name)
ref_doc.check_permission()
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return []

View File

@@ -29,9 +29,6 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestSuite
PAYMENT_URL = "https://example.com/payment"
SEND_EMAIL_MOCK = MagicMock(return_value=None)
GET_PAYMENT_URL_MOCK = MagicMock(return_value=PAYMENT_URL)
GET_PAYMENT_GATEWAY_CONTROLLER_MOCK = MagicMock()
payment_gateways = [
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
@@ -74,18 +71,6 @@ payment_method = [
]
@patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
new=SEND_EMAIL_MOCK,
)
@patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
new=GET_PAYMENT_URL_MOCK,
)
@patch(
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
new=GET_PAYMENT_GATEWAY_CONTROLLER_MOCK,
)
class TestPaymentRequest(ERPNextTestSuite):
def setUp(self):
for payment_gateway in payment_gateways:
@@ -104,11 +89,24 @@ class TestPaymentRequest(ERPNextTestSuite):
):
frappe.get_doc(method).insert(ignore_permissions=True)
for mock in (SEND_EMAIL_MOCK, GET_PAYMENT_URL_MOCK, GET_PAYMENT_GATEWAY_CONTROLLER_MOCK):
mock.reset_mock()
self.send_email = SEND_EMAIL_MOCK
self.get_payment_url = GET_PAYMENT_URL_MOCK
self._get_payment_gateway_controller = GET_PAYMENT_GATEWAY_CONTROLLER_MOCK
send_email = patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
return_value=None,
)
self.send_email = send_email.start()
self.addCleanup(send_email.stop)
get_payment_url = patch(
# this also shadows one (1) call to _get_payment_gateway_controller
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
return_value=PAYMENT_URL,
)
self.get_payment_url = get_payment_url.start()
self.addCleanup(get_payment_url.stop)
_get_payment_gateway_controller = patch(
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
)
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
self.addCleanup(_get_payment_gateway_controller.stop)
def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR", do_not_save=True)

View File

@@ -263,15 +263,12 @@ def get_cashiers(doctype: str, txt: str, searchfield: str, start: int, page_len:
@frappe.whitelist()
def get_invoices(start: str | datetime, end: str | datetime, pos_profile: str, user: str):
invoice_doctype = frappe.db.get_single_value("POS Settings", "invoice_type")
frappe.has_permission("POS Profile", doc=pos_profile, throw=True)
frappe.has_permission("Sales Invoice", throw=True)
sales_inv_query = build_invoice_query("Sales Invoice", user, pos_profile, start, end)
query = sales_inv_query
if invoice_doctype == "POS Invoice":
frappe.has_permission("POS Invoice", throw=True)
pos_inv_query = build_invoice_query("POS Invoice", user, pos_profile, start, end)
query = query + pos_inv_query

View File

@@ -21,12 +21,13 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPOSClosingEntry(ERPNextTestSuite):
def setUp(self):
self.test_user, self.pos_profile = init_user_and_profile()
init_user_and_profile()
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
def test_pos_closing_entry(self):
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -58,7 +59,8 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
Test if POS Closing Entry is created without item code
"""
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -77,9 +79,10 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
test_item_qty = get_test_item_qty(self.pos_profile)
test_item_qty = get_test_item_qty(pos_profile)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -101,11 +104,13 @@ class TestPOSClosingEntry(ERPNextTestSuite):
pcv_doc.flags.in_test = True
pcv_doc.submit()
test_item_qty_after_sales = get_test_item_qty(self.pos_profile)
opening_entry = create_opening_entry(pos_profile, test_user.name)
test_item_qty_after_sales = get_test_item_qty(pos_profile)
self.assertEqual(test_item_qty_after_sales, test_item_qty - 1)
def test_cancelling_of_pos_closing_entry(self):
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -164,7 +169,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
pos_profile.insert()
self.assertTrue(frappe.db.exists("POS Profile", pos_profile.name))
opening_entry = create_opening_entry(pos_profile, self.test_user.name)
test_user = init_user_and_profile(do_not_create_pos_profile=1)
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
@@ -188,6 +195,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
def test_merging_into_sales_invoice_for_batched_item(self):
frappe.flags.print_message = False
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.stock.doctype.batch.batch import get_batch_qty
item_doc = make_item(
@@ -210,7 +220,8 @@ class TestPOSClosingEntry(ERPNextTestSuite):
)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv = create_pos_invoice(
item_code=item_code,
@@ -280,17 +291,18 @@ class TestPOSClosingEntry(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("POS Settings", {"invoice_type": "Sales Invoice"})
def test_closing_entries_with_sales_invoice(self):
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_si = create_sales_invoice(
qty=10, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
qty=10, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
)
pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
pos_si.save()
pos_si.submit()
pos_si2 = create_sales_invoice(
qty=5, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=11
qty=5, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=11
)
pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
pos_si2.save()
@@ -320,10 +332,12 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
pos_si1, pos_si2 = create_multiple_sales_invoices(self.pos_profile)
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
pos_inv = create_pos_invoice(rate=100, do_not_save=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
@@ -343,13 +357,13 @@ class TestPOSClosingEntry(ERPNextTestSuite):
self.assertEqual(pos_si2.pos_closing_entry, pcv_doc1.name)
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
opening_entry2 = create_opening_entry(pos_profile, test_user.name)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.pos_profile)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
# Trying to create Sales Invoice when invoice_type is set to POS Invoice.
pos_si3 = create_sales_invoice(
qty=1, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
)
pos_si3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
self.assertRaises(frappe.ValidationError, pos_si3.save)
@@ -380,14 +394,16 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
test_user, pos_profile = init_user_and_profile()
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.pos_profile)
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
# Trying to create Sales Invoice when invoice_type is set to POS Invoice.
pos_sinv = create_sales_invoice(
qty=1, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
)
pos_sinv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
self.assertRaises(frappe.ValidationError, pos_sinv.save)
@@ -405,9 +421,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
self.assertEqual(pcv_doc1.grand_total, 300)
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
opening_entry2 = create_opening_entry(pos_profile, test_user.name)
pos_si1, pos_si2 = create_multiple_sales_invoices(self.pos_profile)
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
pos_inv3 = create_pos_invoice(rate=100, do_not_save=1)
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})

View File

@@ -4,7 +4,6 @@ import copy
import frappe
from frappe import _
from frappe.utils import add_to_date
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
@@ -54,14 +53,14 @@ class TestPOSInvoice(POSInvoiceTestMixin):
w2 = frappe.get_doc(w.doctype, w.name)
import time
time.sleep(1)
w.save()
frappe.db.set_value(
w.doctype,
w.name,
"modified",
add_to_date(w.modified, seconds=1),
update_modified=False,
)
import time
time.sleep(1)
self.assertRaises(frappe.TimestampMismatchError, w2.save)
def test_change_naming_series(self):
@@ -903,6 +902,9 @@ class TestPOSInvoice(POSInvoiceTestMixin):
self.assertEqual(pos_inv.items[0].rate, 300)
def test_delivered_serial_no_case(self):
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -914,6 +916,8 @@ class TestPOSInvoice(POSInvoiceTestMixin):
self.assertEqual(serial_no, delivered_serial_no)
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=[serial_no],
@@ -927,9 +931,13 @@ class TestPOSInvoice(POSInvoiceTestMixin):
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_stock_balance
init_user_and_profile()
frappe.set_user("Administrator")
@@ -951,18 +959,9 @@ class TestPOSInvoice(POSInvoiceTestMixin):
is_stock_item=1,
)
# Set initial stock to SubA=5 and SubB=2, even when this test is rerun on the same site.
for item_code, target_qty in ((sub_item_a, 5), (sub_item_b, 2)):
balance = get_stock_balance(item_code, warehouse)
difference = target_qty - balance
if difference:
make_stock_entry(
item_code=item_code,
to_warehouse=warehouse if difference > 0 else None,
from_warehouse=warehouse if difference < 0 else None,
qty=abs(difference),
company=company,
)
# Add initial stock: SubA=5, SubB=2
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
bundle_item = "_Test Bundle"
@@ -1011,19 +1010,16 @@ class TestPOSInvoice(POSInvoiceTestMixin):
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile_name = args.pos_profile
if not pos_profile_name:
pos_profile_name = frappe.db.exists("POS Profile", "_Test POS Profile")
if not pos_profile_name:
pos_profile = make_pos_profile()
pos_profile.save()
pos_profile_name = pos_profile.name
pos_profile = None
if not args.pos_profile:
pos_profile = make_pos_profile()
pos_profile.save()
pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1
pos_inv.is_pos = 1
pos_inv.pos_profile = pos_profile_name
pos_inv.pos_profile = args.pos_profile or pos_profile.name
if args.posting_date:
pos_inv.set_posting_time = 1

View File

@@ -26,10 +26,14 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
pos_inv.save()
@@ -51,10 +55,14 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
pos_inv.append(
@@ -99,6 +107,9 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
@@ -110,6 +121,7 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
make_item(item, {"is_stock_item": 1})
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
pos_inv.append(

View File

@@ -289,11 +289,6 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
user = frappe.session["user"]
company = filters.get("company") or frappe.defaults.get_user_default("company")
allowed_pos_profiles = frappe.get_list("POS Profile", pluck="name")
if not allowed_pos_profiles:
return {}
pf = frappe.qb.DocType("POS Profile")
pfu = frappe.qb.DocType("POS Profile User")
@@ -303,7 +298,6 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
.on(pfu.parent == pf.name)
.select(pf.name)
.where((pfu.user == user) & (pf.company == company) & pf.name.like(f"%{txt}%") & (pf.disabled == 0))
.where(pf.name.isin(allowed_pos_profiles))
.limit(page_len)
.offset(start)
.run()
@@ -320,7 +314,6 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
& (pf.company == company)
& pf.name.like(f"%{txt}%")
& (pf.disabled == 0)
& (pf.name.isin(allowed_pos_profiles))
)
.run()
)

View File

@@ -12,9 +12,8 @@
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"read_only": 1
"hidden": 1,
"label": "Fieldname"
},
{
"fieldname": "field",
@@ -27,7 +26,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-08-31 20:41:12.000000",
"modified": "2025-07-29 18:08:40.323579",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Search Fields",

View File

@@ -1,9 +1,40 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
function is_valid_invoice_field(df) {
return frappe.model.no_value_type.indexOf(df.fieldtype) === -1 || df.fieldtype === "Button";
}
let search_fields_datatypes = [
"Data",
"Link",
"Dynamic Link",
"Long Text",
"Select",
"Small Text",
"Text",
"Text Editor",
];
let do_not_include_fields = [
"naming_series",
"item_code",
"item_name",
"stock_uom",
"asset_naming_series",
"default_material_request_type",
"valuation_method",
"warranty_period",
"weight_uom",
"batch_number_series",
"serial_no_series",
"purchase_uom",
"customs_tariff_number",
"sales_uom",
"deferred_revenue_account",
"deferred_expense_account",
"quality_inspection_template",
"route",
"slideshow",
"website_image_alt",
"thumbnail",
"web_long_description",
];
frappe.ui.form.on("POS Settings", {
onload: function (frm) {
@@ -11,97 +42,74 @@ frappe.ui.form.on("POS Settings", {
frm.trigger("add_search_options");
},
invoice_type: function (frm) {
frm.trigger("get_invoice_fields");
},
get_invoice_fields: function (frm) {
const invoice_type = frm.doc.invoice_type;
if (!invoice_type) return;
frappe.model.with_doctype(invoice_type, () => {
// the invoice type can change again while the meta loads
if (frm.doc.invoice_type !== invoice_type) return;
const fields = frappe.get_doc("DocType", invoice_type).fields.filter(is_valid_invoice_field);
frappe.model.with_doctype("POS Invoice", () => {
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
if (
frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
["Button"].includes(d.fieldtype)
) {
return { label: d.label + " (" + d.fieldtype + ")", value: d.fieldname };
} else {
return null;
}
});
frm.fields_dict.invoice_fields.grid.update_docfield_property(
"fieldname",
"options",
[""].concat(
fields.map((df) => {
return { label: `${df.label} (${df.fieldtype})`, value: df.fieldname };
})
)
[""].concat(fields)
);
frm.trigger("validate_invoice_fields");
});
},
validate_invoice_fields: function (frm) {
const valid_fieldnames = frappe
.get_doc("DocType", frm.doc.invoice_type)
.fields.filter(is_valid_invoice_field)
.map((df) => df.fieldname);
const invalid_fields = (frm.doc.invoice_fields || [])
.filter((row) => row.fieldname && !valid_fieldnames.includes(row.fieldname))
.map((row) => `#${row.idx} ${row.fieldname}`);
if (!invalid_fields.length) return;
frappe.msgprint({
title: __("Invalid POS Fields"),
indicator: "orange",
message: __("The following rows are not valid fields of {0} and have to be removed: {1}", [
frm.doc.invoice_type.bold(),
invalid_fields.join(", "),
]),
});
},
add_search_options: function (frm) {
frappe.call({
method: "erpnext.accounts.doctype.pos_settings.pos_settings.get_pos_search_field_options",
callback: ({ message }) => {
const fields = message || [];
frappe.model.with_doctype("Item", () => {
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
if (
search_fields_datatypes.includes(d.fieldtype) &&
!do_not_include_fields.includes(d.fieldname)
) {
return [d.label];
} else {
return null;
}
});
frm.searchable_item_fields = Object.fromEntries(
fields.map((df) => [df.option, df.fieldname])
);
frm.fields_dict.pos_search_fields.grid.update_docfield_property(
"field",
"options",
[""].concat(fields.map((df) => df.option))
);
},
fields.unshift("");
frm.fields_dict.pos_search_fields.grid.update_docfield_property("field", "options", fields);
});
},
});
frappe.ui.form.on("POS Search Fields", {
field: function (frm, doctype, name) {
const doc = frappe.get_doc(doctype, name);
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
return d;
} else {
return null;
}
})[0];
doc.fieldname = frm.searchable_item_fields?.[doc.field] || "";
frm.refresh_field("pos_search_fields");
doc.fieldname = df.fieldname;
frm.refresh_field("fields");
},
});
frappe.ui.form.on("POS Field", {
fieldname: function (frm, doctype, name) {
const doc = frappe.get_doc(doctype, name);
const invoice_meta = frappe.get_doc("DocType", frm.doc.invoice_type);
const df = invoice_meta?.fields.find((d) => d.fieldname === doc.fieldname);
if (!df) return;
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
return doc.fieldname == d.fieldname ? d : null;
})[0];
doc.label = df.label;
doc.reqd = df.reqd;
doc.options = df.options;
doc.fieldtype = df.fieldtype;
doc.default_value = df.default;
frm.refresh_field("invoice_fields");
frm.refresh_field("fields");
},
});

View File

@@ -5,46 +5,8 @@ from collections import Counter
import frappe
from frappe import _
from frappe.model import no_value_fields
from frappe.model.document import Document
SEARCH_FIELD_TYPES = (
"Data",
"Link",
"Dynamic Link",
"Long Text",
"Select",
"Small Text",
"Text",
"Text Editor",
)
# Item fields that are of a searchable fieldtype, but are not meaningful to search a POS item by
DO_NOT_INCLUDE_FIELDS = (
"naming_series",
"item_code",
"item_name",
"stock_uom",
"asset_naming_series",
"default_material_request_type",
"valuation_method",
"warranty_period",
"weight_uom",
"batch_number_series",
"serial_no_series",
"purchase_uom",
"customs_tariff_number",
"sales_uom",
"deferred_revenue_account",
"deferred_expense_account",
"quality_inspection_template",
"route",
"slideshow",
"website_image_alt",
"thumbnail",
"web_long_description",
)
class POSSettings(Document):
# begin: auto-generated types
@@ -70,10 +32,17 @@ class POSSettings(Document):
if old_doc.invoice_type != self.invoice_type:
self.validate_invoice_type()
self.validate_duplicate_invoice_fields()
self.validate_invoice_fields()
self.validate_duplicate_pos_search_fields()
self.validate_pos_search_fields()
def validate_invoice_fields(self):
invoice_fields = [field.fieldname for field in self.invoice_fields]
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
if len(duplicate_invoice_fields):
for field in duplicate_invoice_fields:
frappe.throw(
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
)
def validate_invoice_type(self):
pos_opening_entries_count = frappe.db.count(
@@ -86,94 +55,3 @@ class POSSettings(Document):
),
title=_("Invoice Document Type Selection Error"),
)
def validate_duplicate_invoice_fields(self):
invoice_fields = [field.fieldname for field in self.invoice_fields]
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
if len(duplicate_invoice_fields):
for field in duplicate_invoice_fields:
frappe.throw(
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
)
def validate_invoice_fields(self):
if not self.invoice_type:
return
meta = frappe.get_meta(self.invoice_type)
for field in self.invoice_fields:
df = meta.get_field(field.fieldname)
if not df or not is_valid_invoice_field(df):
frappe.throw(
title=_("Invalid POS Field"),
msg=_("Row #{0}: '{1}' is not a valid field of {2}.").format(
field.idx, frappe.bold(field.fieldname or ""), frappe.bold(_(self.invoice_type))
),
)
# read only in the form, so keep them in sync with the invoice
field.label = df.label
field.fieldtype = df.fieldtype
field.options = df.options
def validate_duplicate_pos_search_fields(self):
fieldnames = [field.fieldname for field in self.pos_search_fields]
duplicate_fieldnames = {key for key, value in Counter(fieldnames).items() if value > 1}
for fieldname in duplicate_fieldnames:
frappe.throw(
title=_("Duplicate POS Search Fields"),
msg=_("'{0}' has been already added.").format(fieldname),
)
def validate_pos_search_fields(self):
searchable_fields = {df.fieldname: df for df in get_searchable_item_fields()}
for field in self.pos_search_fields:
df = searchable_fields.get(field.fieldname)
if not df:
frappe.throw(
title=_("Invalid POS Search Field"),
msg=_("Row #{0}: '{1}' cannot be used to search items.").format(
field.idx, frappe.bold(field.fieldname or "")
),
)
if field.field != get_search_field_option(df):
frappe.throw(
title=_("Invalid POS Search Field"),
msg=_("Row #{0}: '{1}' does not match {2}.").format(
field.idx, frappe.bold(field.field or ""), frappe.bold(df.fieldname)
),
)
def is_valid_invoice_field(df):
return df.fieldtype not in no_value_fields or df.fieldtype == "Button"
def get_searchable_item_fields():
return [
df
for df in frappe.get_meta("Item").fields
if df.fieldtype in SEARCH_FIELD_TYPES and df.fieldname not in DO_NOT_INCLUDE_FIELDS
]
def get_search_field_option(df):
# the fieldname keeps the option unique, two Item fields can share a label
return f"{df.label} ({df.fieldname})"
@frappe.whitelist()
def get_pos_search_field_options():
frappe.has_permission("POS Settings", throw=True)
return [
{"option": get_search_field_option(df), "fieldname": df.fieldname}
for df in get_searchable_item_fields()
]

View File

@@ -1,135 +1,8 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.patches.v16_0.append_fieldname_to_pos_search_fields import execute as append_fieldname
from erpnext.tests.utils import ERPNextTestSuite
class TestPOSSettings(ERPNextTestSuite):
def setUp(self):
self.settings = frappe.get_single("POS Settings")
self.settings.invoice_fields = []
self.settings.pos_search_fields = []
def assertInvalid(self, message):
with self.assertRaises(frappe.ValidationError) as context:
self.settings.save()
self.assertIn(message, str(context.exception))
def test_invoice_field_is_validated_against_invoice_type(self):
# consolidated_invoice exists on POS Invoice only
self.settings.invoice_type = "POS Invoice"
self.settings.append("invoice_fields", {"fieldname": "consolidated_invoice"})
self.settings.save()
self.settings.invoice_type = "Sales Invoice"
self.assertInvalid("is not a valid field of")
def test_field_common_to_both_invoice_types_is_allowed(self):
for invoice_type in ("POS Invoice", "Sales Invoice"):
self.settings.invoice_type = invoice_type
self.settings.invoice_fields = []
self.settings.append("invoice_fields", {"fieldname": "po_no"})
self.settings.save()
def test_unknown_invoice_field_is_not_allowed(self):
self.settings.append("invoice_fields", {"fieldname": "not_a_field"})
self.assertInvalid("is not a valid field of")
def test_layout_invoice_field_is_not_allowed(self):
self.settings.append("invoice_fields", {"fieldname": "accounting_dimensions_section"})
self.assertInvalid("is not a valid field of")
def test_invoice_field_properties_are_set_from_the_invoice(self):
self.settings.append(
"invoice_fields", {"fieldname": "customer", "label": "Tampered", "fieldtype": "Data"}
)
self.settings.save()
field = self.settings.invoice_fields[0]
self.assertEqual(field.label, "Customer")
self.assertEqual(field.fieldtype, "Link")
self.assertEqual(field.options, "Customer")
def test_searchable_item_field_is_allowed(self):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.settings.save()
self.assertEqual(self.settings.pos_search_fields[0].fieldname, "description")
def test_excluded_search_field_is_not_allowed(self):
self.settings.append(
"pos_search_fields", {"field": "Item Name (item_name)", "fieldname": "item_name"}
)
self.assertInvalid("cannot be used to search items")
def test_search_field_of_unsearchable_type_is_not_allowed(self):
# maintain stock is a Check field
self.settings.append(
"pos_search_fields", {"field": "Maintain Stock (is_stock_item)", "fieldname": "is_stock_item"}
)
self.assertInvalid("cannot be used to search items")
def test_unknown_search_field_is_not_allowed(self):
self.settings.append(
"pos_search_fields", {"field": "Nope (not_an_item_field)", "fieldname": "not_an_item_field"}
)
self.assertInvalid("cannot be used to search items")
def test_search_field_without_a_fieldname_is_not_allowed(self):
# the form fills the fieldname in, it cannot be picked on its own
self.settings.append("pos_search_fields", {"field": "Description (description)"})
self.assertInvalid("cannot be used to search items")
def test_search_field_option_must_match_its_fieldname(self):
self.settings.append("pos_search_fields", {"field": "Brand (brand)", "fieldname": "description"})
self.assertInvalid("does not match")
def test_bare_label_is_not_accepted_as_a_search_field(self):
# the stored option carries the fieldname, the patch backfills older rows
self.settings.append("pos_search_fields", {"field": "Description", "fieldname": "description"})
self.assertInvalid("does not match")
def test_duplicate_search_fields_are_not_allowed(self):
for _ in range(2):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.assertInvalid("has been already added")
def test_patch_appends_the_fieldname_to_a_legacy_search_field(self):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.settings.save()
row = self.settings.pos_search_fields[0].name
frappe.db.set_value("POS Search Fields", row, "field", "Description", update_modified=False)
append_fieldname()
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
def test_patch_leaves_an_already_migrated_search_field_alone(self):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.settings.save()
append_fieldname()
row = self.settings.pos_search_fields[0].name
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
def test_invoice_fields_are_skipped_when_no_invoice_type_is_selected(self):
self.settings.invoice_type = None
self.settings.append("invoice_fields", {"fieldname": "customer"})
self.settings.save()
self.assertEqual(self.settings.invoice_fields[0].fieldname, "customer")
pass

View File

@@ -142,8 +142,6 @@ def start_pcv_processing(docname: str):
@frappe.whitelist()
def pause_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
@@ -159,8 +157,6 @@ def pause_pcv_processing(docname: str):
@frappe.whitelist()
def cancel_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="cancel", doc=docname, throw=True)
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
@@ -175,8 +171,6 @@ def cancel_pcv_processing(docname: str):
@frappe.whitelist()
def resume_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()

View File

@@ -10,7 +10,6 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.mapper import get_qty_already_mapped
@frappe.whitelist()
@@ -53,11 +52,6 @@ def make_purchase_receipt(
args = {}
args = frappe.parse_json(args)
mapped_qty_by_item = get_qty_already_mapped(target_doc, "purchase_invoice_item")
def received_and_mapped_qty(obj):
return flt(obj.received_qty) + flt(mapped_qty_by_item.get(obj.name, 0))
def post_parent_process(source_parent, target_parent):
remove_items_with_zero_qty(target_parent)
set_missing_values(source_parent, target_parent)
@@ -81,13 +75,15 @@ def make_purchase_receipt(
or {}
)
pending_qty = flt(obj.qty) - received_and_mapped_qty(obj)
target.qty = pending_qty - flt(returned_qty_map.get("qty"))
target.received_qty = pending_qty
target.stock_qty = (pending_qty - flt(returned_qty_map.get("qty"))) * flt(obj.conversion_factor)
target.amount = pending_qty * flt(obj.rate)
target.base_amount = pending_qty * flt(obj.rate) * flt(source_parent.conversion_rate)
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
obj.conversion_factor
)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
)
def select_item(d):
filtered_items = args.get("filtered_children", [])
@@ -117,8 +113,7 @@ def make_purchase_receipt(
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: abs(received_and_mapped_qty(doc)) < abs(doc.qty)
and select_item(doc),
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",

View File

@@ -235,9 +235,6 @@ class PurchaseInvoice(BuyingController):
"overflow_type": "billing",
}
]
self.closed_source_links = [
("Purchase Invoice Item", "pr_detail", "Purchase Receipt Item", "Purchase Receipt")
]
def onload(self):
super().onload()

View File

@@ -277,15 +277,6 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
if not (entry.amount or entry.base_amount):
continue
lcv_account_currency = get_account_currency(entry.expense_account)
credit_in_transaction_currency = (
flt(entry.amount)
if lcv_account_currency == doc.currency
else flt(
entry.base_amount / doc.conversion_rate, item.precision("net_amount")
)
)
gl_dict = self.get_gl_dict(
{
"account": entry.expense_account,
@@ -294,7 +285,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(entry.base_amount),
"credit_in_account_currency": flt(entry.amount),
"credit_in_transaction_currency": credit_in_transaction_currency,
"credit_in_transaction_currency": item.net_amount,
"project": entry.dimensions.project or item.project or doc.project,
},
item=item,

View File

@@ -578,7 +578,17 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
self.addCleanup(
frappe.db.set_single_value,
"Buying Settings",
"set_landed_cost_based_on_purchase_invoice_rate",
original_value,
)
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",
@@ -606,7 +616,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
self.addCleanup(
frappe.db.set_single_value,
"Buying Settings",
"set_landed_cost_based_on_purchase_invoice_rate",
original_value,
)
pr = frappe.new_doc("Purchase Receipt")
pr.currency = "USD"
@@ -3526,6 +3545,7 @@ def make_purchase_invoice_against_cost_center(**args):
def setup_provisional_accounting(**args):
args = frappe._dict(args)
create_item("_Test Non Stock Item", is_stock_item=0)
company = args.company or "_Test Company"
provisional_account = create_account(
account_name=args.account_name or "Provision Account",

View File

@@ -27,7 +27,6 @@ def start_payment_ledger_repost(docname: str | None = None):
"""
if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
repost_doc.check_permission("submit")
if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
try:
for entry in repost_doc.repost_vouchers:

View File

@@ -278,9 +278,6 @@ class SalesInvoice(SellingController):
"overflow_type": "billing",
}
]
self.closed_source_links = [
("Sales Invoice Item", "dn_detail", "Delivery Note Item", "Delivery Note")
]
def set_indicator(self):
"""Set indicator for portal"""

View File

@@ -476,7 +476,7 @@ class SalesInvoiceGLComposer(BaseGLComposer):
for payment_mode in doc.payments:
if skip_change_gl_entries and payment_mode.account == doc.account_for_change_amount:
payment_mode.base_amount -= flt(doc.base_change_amount)
payment_mode.base_amount -= flt(doc.change_amount)
if payment_mode.base_amount:
# POS, make payment entries

View File

@@ -7,7 +7,7 @@ import json
import frappe
from frappe import qb
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.utils import add_days, add_to_date, cint, flt, format_date, getdate, nowdate, today
from frappe.utils import add_days, cint, flt, format_date, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -129,14 +129,14 @@ class TestSalesInvoice(ERPNextTestSuite):
w2 = frappe.get_doc(w.doctype, w.name)
import time
time.sleep(1)
w.save()
frappe.db.set_value(
w.doctype,
w.name,
"modified",
add_to_date(w.modified, seconds=1),
update_modified=False,
)
import time
time.sleep(1)
self.assertRaises(frappe.TimestampMismatchError, w2.save)
def test_sales_invoice_change_naming_series(self):
@@ -1583,35 +1583,6 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
def test_pos_change_amount_multi_currency_gl_entry(self):
from erpnext.accounts.doctype.sales_invoice.services.gl_composer import SalesInvoiceGLComposer
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 0)
si = create_sales_invoice(do_not_save=True)
si.is_pos = 1
si.currency = "USD"
si.conversion_rate = 50
si.party_account_currency = "USD"
si.account_for_change_amount = "Cash - _TC"
si.change_amount = 50
si.base_change_amount = 2500
si.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 150, "base_amount": 7500},
)
gl_entries = []
SalesInvoiceGLComposer(si).make_pos_gl_entries(gl_entries)
debtors_entry = next(entry for entry in gl_entries if entry["account"] == si.debit_to)
cash_entry = next(entry for entry in gl_entries if entry["account"] == "Cash - _TC")
self.assertEqual(flt(debtors_entry["credit"]), 5000.0)
self.assertEqual(flt(cash_entry["debit"]), 5000.0)
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
def test_stock_delivered_but_not_billed_gl_on_invoice(self):
company = "_Test SDBNB Company"
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -3846,12 +3817,25 @@ class TestSalesInvoice(ERPNextTestSuite):
# enable common party accounting
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
# make the shared department dimension mandatory
dim = frappe.get_doc("Accounting Dimension", {"document_type": "Department"})
dim.disabled = False
dim.dimension_defaults = []
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
dim.save()
# create a dimension and make it mandatory
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
dim = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
}
)
dim.save()
else:
dim = frappe.get_doc(
"Accounting Dimension",
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
)
dim.disabled = False
dim.dimension_defaults = []
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
dim.save()
# create a sales invoice
si = create_sales_invoice(
@@ -5805,6 +5789,12 @@ def create_internal_parties():
allowed_to_interact_with="Wind Power LLC",
)
create_internal_customer(
customer_name="_Test Internal Customer 2",
represents_company="_Test Company with perpetual inventory",
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_customer(
customer_name="_Test Internal Customer 3",
represents_company="_Test Company",
@@ -5825,6 +5815,12 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company 1",
)
create_internal_supplier(
supplier_name="_Test Internal Supplier 2",
represents_company="_Test Company with perpetual inventory",
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_supplier(
supplier_name="_Test Internal Customer 3",
represents_company="_Test Company",

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import patch
import frappe
from frappe.utils.data import (
@@ -659,15 +658,23 @@ class TestSubscription(ERPNextTestSuite):
sub2 = create_subscription(start_date="2018-01-02")
processed = []
original_process = Subscription.process
original_rollback = frappe.db.rollback
def patched(self, posting_date=None):
processed.append(self.name)
if self.name == sub1.name:
raise frappe.ValidationError("forced failure")
# Stub transaction recovery so the test can observe the complete iteration in isolation.
with patch.object(Subscription, "process", patched), patch.object(frappe.db, "rollback"):
Subscription.process = patched
# process_all calls frappe.db.rollback() on error which would otherwise wipe
# the test transaction; stub it so we can observe the iteration in isolation.
frappe.db.rollback = lambda *a, **kw: None
try:
process_all([sub1.name, sub2.name])
finally:
Subscription.process = original_process
frappe.db.rollback = original_rollback
self.assertEqual(processed, [sub1.name, sub2.name])
@@ -1066,6 +1073,12 @@ def create_plan(**kwargs):
def create_parties():
if not frappe.db.exists("Supplier", "_Test Supplier"):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = "_Test Supplier"
supplier.supplier_group = "All Supplier Groups"
supplier.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer"

View File

@@ -63,6 +63,25 @@ class TestTaxRule(ERPNextTestSuite):
def test_for_parent_supplier_group(self):
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template",
"title": "_Test Purchase Taxes and Charges Template",
"company": "_Test Company",
"taxes": [
{
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Purchase Taxes and Charges",
"cost_center": "Main - _TC",
"rate": 6,
}
],
}
).insert()
make_tax_rule(
supplier_group="All Supplier Groups",
tax_type="Purchase",

View File

@@ -869,7 +869,9 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(rows_b[0].future_amount, 50.0)
def test_sales_person(self):
sales_person = frappe.get_doc("Sales Person", "_Test Sales Person")
sales_person = frappe.get_doc(
{"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}
).insert()
si = self.create_sales_invoice(do_not_submit=True)
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
si.save().submit()
@@ -1492,8 +1494,17 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def test_receivable_filtered_by_sales_partner(self):
frappe.set_user("Administrator")
partner_a = "_Test Sales Partner India - 1"
partner_b = "_Test Sales Partner India - 2"
partner_a, partner_b = "_Test AR Sales Partner A", "_Test AR Sales Partner B"
for partner in (partner_a, partner_b):
if not frappe.db.exists("Sales Partner", partner):
frappe.get_doc(
{
"doctype": "Sales Partner",
"partner_name": partner,
"commission_rate": 0,
"territory": "All Territories",
}
).insert()
def _si(sales_partner):
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True, qty=2)

View File

@@ -193,7 +193,16 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(rpt_output), 0)
def test_03_summary_sales_partner_column(self):
partner = "_Test Sales Partner India - 1"
partner = "_Test AR Summary Sales Partner"
if not frappe.db.exists("Sales Partner", partner):
frappe.get_doc(
{
"doctype": "Sales Partner",
"partner_name": partner,
"commission_rate": 0,
"territory": "All Territories",
}
).insert()
si = create_sales_invoice(
item=self.item,

View File

@@ -21,8 +21,10 @@ class TestGeneralLedger(ERPNextTestSuite):
from frappe.utils import today
frappe.db.set_single_value("Accounts Settings", "general_ledger_remarks_length", 50)
self.addCleanup(frappe.db.set_single_value, "Accounts Settings", "general_ledger_remarks_length", 0)
create_sales_invoice(company=self.company)
si = create_sales_invoice(company=self.company)
self.addCleanup(self._cancel_and_delete, "Sales Invoice", si.name)
columns, data = execute(
frappe._dict(
@@ -40,6 +42,15 @@ class TestGeneralLedger(ERPNextTestSuite):
self.assertTrue(data)
self.assertTrue(any("remarks" in row for row in data))
@staticmethod
def _cancel_and_delete(doctype, name):
if not frappe.db.exists(doctype, name):
return
doc = frappe.get_doc(doctype, name)
if doc.docstatus == 1:
doc.cancel()
frappe.delete_doc(doctype, name, force=1)
def clear_old_entries(self):
doctype_list = [
"GL Entry",

View File

@@ -642,7 +642,7 @@ class TestGrossProfit(ERPNextTestSuite):
self.assertEqual(total.get("gross_profit_%"), -50.0)
def test_sales_person_wise_gross_profit(self):
sales_person = frappe.get_doc("Sales Person", "_Test Sales Person")
sales_person = make_sales_person("_Test Sales Person")
posting_date = get_first_day(nowdate())
qty = 10
@@ -1194,3 +1194,19 @@ class TestGrossProfit(ERPNextTestSuite):
self.assertEqual(base_rate, 220.0) # avg selling rate = 220/1
self.assertEqual(gross_profit, 120.0) # 220 - 100
self.assertAlmostEqual(gp_percent, 54.545, places=2) # 120/220 * 100
def make_sales_person(sales_person_name="_Test Sales Person"):
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
sales_person_doc = frappe.get_doc(
{
"doctype": "Sales Person",
"is_group": 0,
"parent_sales_person": "Sales Team",
"sales_person_name": sales_person_name,
}
).insert(ignore_permissions=True)
else:
sales_person_doc = frappe.get_doc("Sales Person", {"sales_person_name": sales_person_name})
return sales_person_doc

View File

@@ -12,7 +12,7 @@ COMPANY = "_Test Company"
class TestShareBalanceReport(ERPNextTestSuite):
def setUp(self):
self.share_type = create_share_type("_Test Share Balance Equity")
self.shareholder = get_shareholder("Iron Man", COMPANY)
self.shareholder = create_shareholder("_Test Share Balance Holder", COMPANY)
def test_date_filter_is_mandatory(self):
self.assertRaises(frappe.ValidationError, execute, frappe._dict({"shareholder": self.shareholder}))
@@ -96,7 +96,7 @@ class TestShareBalanceReport(ERPNextTestSuite):
self.assertEqual(row[4], 3000)
def test_balance_reduces_after_transfer_out(self):
other_holder = get_shareholder("Thor", COMPANY)
other_holder = create_shareholder("_Test Share Balance Holder 2", COMPANY)
create_share_transfer(
transfer_type="Issue",
to_shareholder=self.shareholder,
@@ -187,8 +187,9 @@ def create_share_type(title):
return title
def get_shareholder(title, company):
return frappe.db.get_value("Shareholder", {"title": title, "company": company}, "name")
def create_shareholder(title, company):
shareholder = frappe.get_doc({"doctype": "Shareholder", "title": title, "company": company}).insert()
return shareholder.name
def create_share_transfer(**kwargs):

View File

@@ -23,7 +23,7 @@ COL_SHARE_TRANSFER = 8
class TestShareLedger(ERPNextTestSuite):
def setUp(self):
self.shareholder = self.get_shareholder("Iron Man")
self.shareholder = self.create_shareholder("_Test Share Ledger Holder")
# Issue 100 shares on 2026-06-01, then another 50 on 2026-06-10.
self.first = self.issue_shares(date="2026-06-01", from_no=1, to_no=100, rate=10)
self.second = self.issue_shares(date="2026-06-10", from_no=101, to_no=150, rate=12)
@@ -72,7 +72,7 @@ class TestShareLedger(ERPNextTestSuite):
self.assertEqual(data[0][COL_NO_OF_SHARES], 100)
def test_transfer_type_label_when_shareholder_is_seller(self):
buyer = self.get_shareholder("Thor")
buyer = self.create_shareholder("_Test Share Ledger Buyer")
transfer = self.make_transfer(
from_shareholder=self.shareholder,
to_shareholder=buyer,
@@ -87,7 +87,7 @@ class TestShareLedger(ERPNextTestSuite):
self.assertEqual(row[COL_TRANSFER_TYPE], f"Transfer to {buyer}")
def test_transfer_type_label_when_shareholder_is_buyer(self):
seller = self.get_shareholder("Hulk")
seller = self.create_shareholder("_Test Share Ledger Seller")
# the seller must own shares before it can transfer them
self.issue_shares(date="2026-06-12", from_no=201, to_no=300, rate=10, shareholder=seller)
transfer = self.make_transfer(
@@ -119,8 +119,15 @@ class TestShareLedger(ERPNextTestSuite):
self.assertIsNotNone(row, f"Share Transfer {transfer_name} missing from ledger")
return row
def get_shareholder(self, title):
return frappe.db.get_value("Shareholder", {"title": title, "company": COMPANY}, "name")
def create_shareholder(self, title):
doc = frappe.get_doc(
{
"doctype": "Shareholder",
"title": title,
"company": COMPANY,
}
).insert()
return doc.name
def issue_shares(self, date, from_no, to_no, rate, shareholder=None):
doc = frappe.get_doc(

View File

@@ -9,7 +9,6 @@ from frappe.utils import cint, flt
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
@@ -44,7 +43,6 @@ def get_data(filters, show_party_name):
account_filter = get_accounts_with_children(filters.get("account"))
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
precision = get_currency_precision()
opening_balances = get_opening_balances(filters, account_filter)
balances_within_period = get_balances_within_period(filters, account_filter)
@@ -67,17 +65,14 @@ def get_data(filters, show_party_name):
# opening
opening_debit, opening_credit = opening_balances.get(party.name, [0, 0])
opening_debit, opening_credit = flt(opening_debit, precision), flt(opening_credit, precision)
row.update({"opening_debit": opening_debit, "opening_credit": opening_credit})
# within period
debit, credit = balances_within_period.get(party.name, [0, 0])
debit, credit = flt(debit, precision), flt(credit, precision)
row.update({"debit": debit, "credit": credit})
# closing
closing_debit, closing_credit = toggle_debit_credit(opening_debit + debit, opening_credit + credit)
closing_debit, closing_credit = flt(closing_debit, precision), flt(closing_credit, precision)
row.update({"closing_debit": closing_debit, "closing_credit": closing_credit})
row.update({"currency": company_currency})

View File

@@ -309,9 +309,6 @@ def get_payment_entries(filters, args):
pe.mode_of_payment,
pe.project,
pe.cost_center,
pe.payment_type,
pe.source_exchange_rate,
pe.target_exchange_rate,
)
.where(
(pe.docstatus == 1)
@@ -322,22 +319,6 @@ def get_payment_entries(filters, args):
)
query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True)
payment_entries = query.run(as_dict=True)
if payment_entries:
ded = frappe.qb.DocType("Payment Entry Deduction")
deduction_totals = frappe._dict(
frappe.qb.from_(ded)
.select(ded.parent, Sum(ded.amount))
.where(ded.parent.isin([d.name for d in payment_entries]) & (ded.is_exchange_gain_loss == 0))
.groupby(ded.parent)
.run()
)
for d in payment_entries:
exchange_rate = (
d.source_exchange_rate if d.payment_type == "Receive" else d.target_exchange_rate
) or 1
d.base_grand_total = flt(d.base_grand_total) + flt(deduction_totals.get(d.name)) / exchange_rate
return payment_entries

View File

@@ -32,13 +32,12 @@ class ChildItemUpdater:
self.child_docname = child_docname
self.parent = frappe.get_doc(parent_doctype, parent_doctype_name)
self.allow_zero_qty = get_allow_zero_qty(parent_doctype)
self._transacted_stock_qty: dict | None = None
self._ordered_items: dict | None = None
self._purchased_items: dict | None = None
def update(self, trans_items: str | list) -> None:
"""Process item additions, edits, and deletions from trans_items JSON."""
from erpnext.buying.doctype.supplier_quotation.mapper import (
get_ordered_items as get_ordered_supplier_quotation_items,
)
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
from erpnext.selling.doctype.quotation.mapper import get_ordered_items
data = frappe.parse_json(trans_items)
@@ -49,15 +48,11 @@ class ChildItemUpdater:
self._check_permissions("write")
if self.parent_doctype == "Quotation":
self._transacted_stock_qty = get_ordered_items(self.parent.name)
items_added_or_removed |= validate_and_delete_children(
self.parent, data, self._transacted_stock_qty
)
self._ordered_items = get_ordered_items(self.parent.name)
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items)
elif self.parent_doctype == "Supplier Quotation":
self._transacted_stock_qty = get_ordered_supplier_quotation_items(self.parent.name)
items_added_or_removed |= validate_and_delete_children(
self.parent, data, self._transacted_stock_qty
)
self._purchased_items = get_purchased_items(self.parent.name)
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items)
else:
items_added_or_removed |= validate_and_delete_children(self.parent, data)
@@ -76,7 +71,6 @@ class ChildItemUpdater:
else:
self._check_permissions("write")
child_item = frappe.get_doc(self.parent_doctype + " Item", d.get("docname"))
d["conversion_factor"] = self._get_new_conversion_factor(child_item, d)
change_state = get_child_item_change_state(self.parent_doctype, child_item, d)
rate_unchanged = change_state.rate_unchanged
@@ -84,13 +78,6 @@ class ChildItemUpdater:
if is_child_item_unchanged(change_state):
continue
if child_item.get("closed"):
frappe.throw(
_(
"Row #{0}: Cannot change item {1} because it is closed. Reopen the row first."
).format(child_item.idx, child_item.item_code)
)
self._validate_quantity_and_rate(child_item, d, rate_unchanged)
if flt(child_item.get("qty")) != flt(d.get("qty")):
@@ -264,22 +251,6 @@ class ChildItemUpdater:
item_row,
)
def _get_new_conversion_factor(self, child_item, new_data: dict) -> float:
current_factor = flt(child_item.get("conversion_factor")) or 1
uom = new_data.get("uom") or child_item.get("uom")
if uom == child_item.get("stock_uom"):
return 1
requested_factor = flt(new_data.get("conversion_factor"))
if requested_factor:
return requested_factor
if uom == child_item.get("uom"):
return current_factor
return flt(get_conversion_factor(child_item.item_code, uom).get("conversion_factor")) or 1
def _validate_quantity_and_rate(self, child_item, new_data: dict, rate_unchanged: bool | None) -> None:
if not flt(new_data.get("qty")) and not self.allow_zero_qty:
frappe.throw(
@@ -293,24 +264,24 @@ class ChildItemUpdater:
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")),
}
old_conversion_factor = flt(child_item.get("conversion_factor")) or 1
new_conversion_factor = flt(new_data.get("conversion_factor")) or old_conversion_factor
new_stock_qty = flt(new_data.get("qty")) * new_conversion_factor
if self.parent_doctype in qty_limits:
qty_field, error_message = qty_limits[self.parent_doctype]
old_stock_qty = flt(child_item.get(qty_field)) * old_conversion_factor
if new_stock_qty < old_stock_qty:
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
frappe.throw(
_("Row #{0}:").format(new_data.get("idx")) + error_message,
title=_("Invalid Qty"),
)
if not self._transacted_stock_qty:
if self.parent_doctype not in ("Quotation", "Supplier Quotation"):
return
old_stock_qty = self._transacted_stock_qty.get(child_item.name)
if not old_stock_qty:
items_map = self._ordered_items if self.parent_doctype == "Quotation" else self._purchased_items
if not items_map:
return
qty_to_check = items_map.get(child_item.name)
if not qty_to_check:
return
if not rate_unchanged:
@@ -320,7 +291,7 @@ class ChildItemUpdater:
).format(frappe.bold(new_data.get("item_code")))
)
if new_stock_qty < old_stock_qty:
if flt(new_data.get("qty")) < qty_to_check:
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
def _validate_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None:
@@ -487,11 +458,7 @@ def update_bin_on_delete(row, doctype: str) -> None:
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
"""Delete child rows not present in data; return True if any were removed."""
updated_item_names = [d.get("docname") for d in data]
# A closed row is left out of the payload rather than deleted, so its absence
# must not be read as a removal.
deleted_children = [
item for item in parent.items if item.name not in updated_item_names and not item.get("closed")
]
deleted_children = [item for item in parent.items if item.name not in updated_item_names]
for d in deleted_children:
validate_child_on_delete(d, parent, ordered_item)
@@ -614,18 +581,22 @@ def update_child_item_rate_and_discount(
def update_child_item_uom_and_weight(child_item, new_data) -> None:
conv_fac_precision = child_item.precision("conversion_factor") or 2
if new_data.get("conversion_factor"):
if child_item.stock_uom == child_item.uom:
child_item.conversion_factor = 1
else:
child_item.conversion_factor = flt(new_data.get("conversion_factor"))
child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision)
if new_data.get("uom"):
child_item.uom = new_data.get("uom")
conversion_factor = flt(
get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")
)
child_item.conversion_factor = flt(new_data.get("conversion_factor")) or conversion_factor
child_item.conversion_factor = (
flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor
)
if child_item.get("weight_per_unit"):
child_item.total_weight = flt(

View File

@@ -53,11 +53,6 @@ class TaxService:
if doc.get("taxes") or doc.get("is_pos"):
return
# set by the Opening Invoice Creation Tool, where the outstanding amount
# entered against a party is already inclusive of tax
if doc.flags.dont_auto_add_taxes:
return
if frappe.get_single_value(
"Accounts Settings", "add_taxes_from_taxes_and_charges_template"
) and hasattr(doc, "taxes_and_charges"):

File diff suppressed because it is too large Load Diff

View File

@@ -1359,28 +1359,21 @@ def get_children(
parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
fields = ["name as value", "is_group as expandable"]
filters = [["docstatus", "<", 2]]
if frappe.db.has_column(doctype, "disabled"):
if include_disabled:
# the tree marks disabled rows, so it needs the flag
fields.append("disabled")
else:
filters.append(["disabled", "=", False])
# extra columns the tree views render as badges / clean labels
node_fields = {
"Account": ["root_type", "account_name", "account_number", "account_currency", "freeze_account"],
"Cost Center": ["cost_center_name", "cost_center_number"],
}
fields += node_fields.get(doctype, [])
if frappe.db.has_column(doctype, "disabled") and not include_disabled:
filters.append(["disabled", "=", False])
if is_root:
filters.append(IfNull(Field(parent_fieldname), "") == "")
filters.append(["company", "=", company])
if doctype == "Account":
fields.append("report_type")
else:
filters.append([parent_fieldname, "=", parent])
fields.append(parent_fieldname + " as parent")
if is_root:
fields += ["root_type", "report_type", "account_currency"] if doctype == "Account" else []
filters.append(["company", "=", company])
else:
fields += ["root_type", "account_currency"] if doctype == "Account" else []
fields += [parent_fieldname + " as parent"]
acc = frappe.get_list(doctype, fields=fields, filters=filters)

View File

@@ -4,9 +4,25 @@
{
"chart_name": "Profit and Loss",
"label": "Profit and Loss"
},
{
"chart_name": "Accounts Receivable Ageing",
"label": "Accounts Receivable Ageing"
},
{
"chart_name": "Accounts Payable Ageing",
"label": "Accounts Payable Ageing"
},
{
"chart_name": "Bank Balance",
"label": "Bank Balance"
},
{
"chart_name": "Budget Variance",
"label": "Budget Variance"
}
],
"content": "[{\"id\": \"a17de17773\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Profit and Loss\", \"col\": 12}}, {\"id\": \"9a0e234f25\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Bills\", \"col\": 4}}, {\"id\": \"58b384d2dd\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Incoming Bills\", \"col\": 4}}, {\"id\": \"575d11919a\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Incoming Payment\", \"col\": 4}}]",
"content": "[{\"id\":\"acc_ov_hdr1\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Accounting Overview</b></span>\",\"col\":12}},{\"id\":\"acc_ov_nc01\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Bills\",\"col\":3}},{\"id\":\"acc_ov_nc02\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Bills\",\"col\":3}},{\"id\":\"acc_ov_nc03\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Payment\",\"col\":3}},{\"id\":\"acc_ov_nc04\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Payment\",\"col\":3}},{\"id\":\"acc_ov_ch01\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"acc_ov_ch02\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Accounts Receivable Ageing\",\"col\":6}},{\"id\":\"acc_ov_ch03\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Accounts Payable Ageing\",\"col\":6}},{\"id\":\"acc_ov_ch04\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Bank Balance\",\"col\":6}},{\"id\":\"acc_ov_ch05\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Budget Variance\",\"col\":6}}]",
"creation": "2026-07-14 12:00:00",
"custom_blocks": [],
"docstatus": 0,
@@ -20,23 +36,27 @@
"label": "Accounting",
"link_type": "DocType",
"links": [],
"modified": "2026-08-26 18:34:03.000000",
"modified": "2026-07-14 14:28:55.763394",
"modified_by": "Administrator",
"module": "Accounts",
"module_onboarding": "Accounting Onboarding",
"name": "Accounting",
"number_cards": [
{
"label": "Total Outgoing Bills",
"label": "Outgoing Bills",
"number_card_name": "Total Outgoing Bills"
},
{
"label": "Total Incoming Bills",
"label": "Incoming Bills",
"number_card_name": "Total Incoming Bills"
},
{
"label": "Total Incoming Payment",
"label": "Incoming Payment",
"number_card_name": "Total Incoming Payment"
},
{
"label": "Outgoing Payment",
"number_card_name": "Total Outgoing Payment"
}
],
"owner": "Administrator",

View File

@@ -2,11 +2,11 @@
"app": "erpnext",
"charts": [
{
"chart_name": "Budget Variance",
"label": "Budget Variance"
"chart_name": "Profit and Loss",
"label": "Profit and Loss"
}
],
"content": "[{\"id\": \"12e194822e\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Budget Variance\", \"col\": 12}}, {\"id\": \"1e3181e0cb\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Bills\", \"col\": 4}}, {\"id\": \"94f620327b\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Incoming Bills\", \"col\": 4}}, {\"id\": \"e3e5de6c45\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Payment\", \"col\": 4}}]",
"content": "[{\"id\":\"tS7ZWzC24I\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"8Ej2KxPxOt\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Ledgers\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2024-01-05 16:09:16.766939",
"custom_blocks": [],
"docstatus": 0,
@@ -18,26 +18,260 @@
"indicator_color": "",
"is_hidden": 0,
"label": "Financial Reports",
"links": [],
"modified": "2026-08-26 18:34:03.000000",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Profitability",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Gross Profit",
"link_count": 0,
"link_to": "Gross Profit",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Profitability Analysis",
"link_count": 0,
"link_to": "Profitability Analysis",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Invoice Trends",
"link_count": 0,
"link_to": "Sales Invoice Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Purchase Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Invoice Trends",
"link_count": 0,
"link_to": "Purchase Invoice Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Financial Statements",
"link_count": 5,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Trial Balance",
"link_count": 0,
"link_to": "Trial Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Profit and Loss Statement",
"link_count": 0,
"link_to": "Profit and Loss Statement",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Balance Sheet",
"link_count": 0,
"link_to": "Balance Sheet",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Cash Flow",
"link_count": 0,
"link_to": "Cash Flow",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Consolidated Financial Statement",
"link_count": 0,
"link_to": "Consolidated Financial Statement",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Ledgers",
"link_count": 3,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "General Ledger",
"link_count": 0,
"link_to": "General Ledger",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Ledger Summary",
"link_count": 0,
"link_to": "Customer Ledger Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Supplier Ledger Summary",
"link_count": 0,
"link_to": "Supplier Ledger Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Other Reports",
"link_count": 7,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Trial Balance for Party",
"link_count": 0,
"link_to": "Trial Balance for Party",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Journal Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Payment Period Based On Invoice Date",
"link_count": 0,
"link_to": "Payment Period Based On Invoice Date",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Partners Commission",
"link_count": 0,
"link_to": "Sales Partners Commission",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Customer",
"hidden": 0,
"is_query_report": 1,
"label": "Customer Credit Balance",
"link_count": 0,
"link_to": "Customer Credit Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Sales Invoice",
"hidden": 0,
"is_query_report": 1,
"label": "Sales Payment Summary",
"link_count": 0,
"link_to": "Sales Payment Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Address",
"hidden": 0,
"is_query_report": 1,
"label": "Address And Contacts",
"link_count": 0,
"link_to": "Address And Contacts",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "UAE VAT 201",
"link_count": 0,
"link_to": "UAE VAT 201",
"link_type": "Report",
"onboard": 0,
"only_for": "United Arab Emirates",
"type": "Link"
}
],
"modified": "2026-07-03 13:44:08.095321",
"modified_by": "Administrator",
"module": "Accounts",
"module_onboarding": "Accounting Onboarding",
"name": "Financial Reports",
"number_cards": [
{
"label": "Total Outgoing Bills",
"number_card_name": "Total Outgoing Bills"
},
{
"label": "Total Incoming Bills",
"number_card_name": "Total Incoming Bills"
},
{
"label": "Total Outgoing Payment",
"number_card_name": "Total Outgoing Payment"
}
],
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,

View File

@@ -2,11 +2,11 @@
"app": "erpnext",
"charts": [
{
"chart_name": "Outgoing Bills (Sales Invoice)",
"label": "Outgoing Bills (Sales Invoice)"
"chart_name": "Profit and Loss",
"label": "Profit and Loss"
}
],
"content": "[{\"id\": \"89faa04783\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Bills (Sales Invoice)\", \"col\": 12}}, {\"id\": \"aa2a9ef0d3\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Bills\", \"col\": 4}}, {\"id\": \"f24f8645d6\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Incoming Bills\", \"col\": 4}}, {\"id\": \"a2cd2dc7f7\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Payment\", \"col\": 4}}]",
"content": "[{\"id\":\"nDhfcJYbKH\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Outgoing Payment\",\"col\":3}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Tax Masters\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Banking\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [],
"docstatus": 0,
@@ -18,23 +18,595 @@
"indicator_color": "",
"is_hidden": 0,
"label": "Invoicing",
"links": [],
"modified": "2026-08-26 18:34:03.000000",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Multi Currency",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Currency",
"link_count": 0,
"link_to": "Currency",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Currency Exchange",
"link_count": 0,
"link_to": "Currency Exchange",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Exchange Rate Revaluation",
"link_count": 0,
"link_to": "Exchange Rate Revaluation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Subscription Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Subscription Plan",
"link_count": 0,
"link_to": "Subscription Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Subscription",
"link_count": 0,
"link_to": "Subscription",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Subscription Settings",
"link_count": 0,
"link_to": "Subscription Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Share Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shareholder",
"link_count": 0,
"link_to": "Shareholder",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Share Transfer",
"link_count": 0,
"link_to": "Share Transfer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Share Transfer",
"hidden": 0,
"is_query_report": 1,
"label": "Share Ledger",
"link_count": 0,
"link_to": "Share Ledger",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Share Transfer",
"hidden": 0,
"is_query_report": 1,
"label": "Share Balance",
"link_count": 0,
"link_to": "Share Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center and Budgeting",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Chart of Cost Centers",
"link_count": 0,
"link_to": "Cost Center",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Budget",
"link_count": 0,
"link_to": "Budget",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Accounting Dimension",
"link_count": 0,
"link_to": "Accounting Dimension",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center Allocation",
"link_count": 0,
"link_to": "Cost Center Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 1,
"label": "Budget Variance Report",
"link_count": 0,
"link_to": "Budget Variance Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Monthly Distribution",
"link_count": 0,
"link_to": "Monthly Distribution",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Opening and Closing",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Opening Invoice Creation Tool",
"link_count": 0,
"link_to": "Opening Invoice Creation Tool",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Chart of Accounts Importer",
"link_count": 0,
"link_to": "Chart of Accounts Importer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Period Closing Voucher",
"link_count": 0,
"link_to": "Period Closing Voucher",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Banking",
"link_count": 6,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank",
"link_count": 0,
"link_to": "Bank",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Account",
"link_count": 0,
"link_to": "Bank Account",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Clearance",
"link_count": 0,
"link_to": "Bank Clearance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Reconciliation Tool",
"link_count": 0,
"link_to": "Bank Reconciliation Tool",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Bank Reconciliation Statement",
"link_count": 0,
"link_to": "Bank Reconciliation Statement",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Plaid Settings",
"link_count": 0,
"link_to": "Plaid Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Tax Masters",
"link_count": 7,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Sales Taxes and Charges Template",
"link_count": 0,
"link_to": "Sales Taxes and Charges Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Taxes and Charges Template",
"link_count": 0,
"link_to": "Purchase Taxes and Charges Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Item Tax Template",
"link_count": 0,
"link_to": "Item Tax Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Tax Category",
"link_count": 0,
"link_to": "Tax Category",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Tax Rule",
"link_count": 0,
"link_to": "Tax Rule",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Tax Withholding Category",
"link_count": 0,
"link_to": "Tax Withholding Category",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Lower Deduction Certificate",
"link_count": 0,
"link_to": "Lower Deduction Certificate",
"link_type": "DocType",
"onboard": 0,
"only_for": "India",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Accounting Masters",
"link_count": 8,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Company",
"link_count": 0,
"link_to": "Company",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Chart of Accounts",
"link_count": 0,
"link_to": "Account",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Accounts Settings",
"link_count": 0,
"link_to": "Accounts Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Fiscal Year",
"link_count": 0,
"link_to": "Fiscal Year",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Accounting Dimension",
"link_count": 0,
"link_to": "Accounting Dimension",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Finance Book",
"link_count": 0,
"link_to": "Finance Book",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Accounting Period",
"link_count": 0,
"link_to": "Accounting Period",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Payment Term",
"link_count": 0,
"link_to": "Payment Term",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payments",
"link_count": 5,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Payment Entry",
"link_count": 0,
"link_to": "Payment Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Journal Entry",
"link_count": 0,
"link_to": "Journal Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Journal Entry Template",
"link_count": 0,
"link_to": "Journal Entry Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Terms and Conditions",
"link_count": 0,
"link_to": "Terms and Conditions",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Mode of Payment",
"link_count": 0,
"link_to": "Mode of Payment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-07-03 13:44:08.471142",
"modified_by": "Administrator",
"module": "Accounts",
"module_onboarding": "Accounting Onboarding",
"name": "Invoicing",
"number_cards": [
{
"label": "Total Outgoing Bills",
"label": "Outgoing Bills",
"number_card_name": "Total Outgoing Bills"
},
{
"label": "Total Incoming Bills",
"label": "Incoming Bills",
"number_card_name": "Total Incoming Bills"
},
{
"label": "Total Outgoing Payment",
"label": "Incoming Payment",
"number_card_name": "Total Incoming Payment"
},
{
"label": "Outgoing Payment",
"number_card_name": "Total Outgoing Payment"
}
],

View File

@@ -1,12 +1,7 @@
{
"app": "erpnext",
"charts": [
{
"chart_name": "Bank Balance",
"label": "Bank Balance"
}
],
"content": "[{\"id\": \"8ad97059fb\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Bank Balance\", \"col\": 12}}, {\"id\": \"0290c3d3fb\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Incoming Payment\", \"col\": 4}}, {\"id\": \"4751e5b274\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Payment\", \"col\": 4}}, {\"id\": \"4852db07c3\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Outgoing Bills\", \"col\": 4}}]",
"charts": [],
"content": "[]",
"creation": "2026-06-11 11:51:21.886461",
"custom_blocks": [],
"docstatus": 0,
@@ -20,25 +15,12 @@
"label": "Payments",
"link_type": "DocType",
"links": [],
"modified": "2026-08-26 18:34:03.000000",
"modified": "2026-07-14 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"module_onboarding": "Accounting Onboarding",
"name": "Payments",
"number_cards": [
{
"label": "Total Incoming Payment",
"number_card_name": "Total Incoming Payment"
},
{
"label": "Total Outgoing Payment",
"number_card_name": "Total Outgoing Payment"
},
{
"label": "Total Outgoing Bills",
"number_card_name": "Total Outgoing Bills"
}
],
"number_cards": [],
"owner": "Administrator",
"public": 1,
"quick_lists": [],

View File

@@ -2109,17 +2109,13 @@ def create_asset_category(enable_cwip=1):
def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_asset=0, asset_category=None):
item_code = item_code or "Macbook Pro"
if frappe.db.exists("Item", item_code):
return frappe.get_doc("Item", item_code)
meta = frappe.get_meta("Asset")
naming_series = meta.get_field("naming_series").options.splitlines()[0] or "ACC-ASS-.YYYY.-"
try:
item = frappe.get_doc(
{
"doctype": "Item",
"item_code": item_code,
"item_code": item_code or "Macbook Pro",
"item_name": "Macbook Pro",
"description": "Macbook Pro Retina Display",
"asset_category": asset_category or "Computers",

View File

@@ -54,16 +54,14 @@ class AssetCapitalizationGLComposer(BaseStockGLComposer):
for item_row in doc.stock_items:
sle_list = self.sle_map.get(item_row.name)
if sle_list:
_inv_dict = doc.get_inventory_account_dict(item_row, self.inventory_account_map)
for sle in sle_list:
stock_value_difference = flt(sle.stock_value_difference, self.precision)
if erpnext.is_perpetual_inventory_enabled(doc.company):
_inv_dict = doc.get_inventory_account_dict(item_row, self.inventory_account_map)
account = _inv_dict["account"]
account_currency = _inv_dict["account_currency"]
else:
account = doc.get_company_default("default_expense_account")
account_currency = None
target_against.add(account)
gl_entries.append(
@@ -76,7 +74,7 @@ class AssetCapitalizationGLComposer(BaseStockGLComposer):
"remarks": doc.get("remarks") or "Accounting Entry for Stock",
"credit": -1 * stock_value_difference,
},
account_currency,
_inv_dict["account_currency"],
item=item_row,
)
)

View File

@@ -440,11 +440,7 @@ def create_asset_capitalization(**args):
target_asset = frappe.get_doc("Asset", args.target_asset) if args.target_asset else frappe._dict()
target_item_code = target_asset.item_code or args.target_item_code
company = target_asset.company or args.company or "_Test Company"
warehouse = args.warehouse or (
"_Test Warehouse - _TC"
if company == "_Test Company"
else create_warehouse("_Test Warehouse", company=company)
)
warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company)
source_warehouse = args.source_warehouse or warehouse
asset_capitalization = frappe.new_doc("Asset Capitalization")

View File

@@ -20,6 +20,7 @@ from erpnext.assets.doctype.asset.test_asset import (
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_serial_nos_from_bundle,
make_serial_batch_bundle,
@@ -31,6 +32,7 @@ class TestAssetRepair(ERPNextTestSuite):
def setUp(self):
self.load_test_records("Stock Entry")
set_depreciation_settings_in_company()
create_item("_Test Stock Item")
def test_asset_status(self):
date = nowdate()

View File

@@ -1,371 +0,0 @@
{
"app": "erpnext",
"creation": "2026-08-16 00:00:00.000000",
"docstatus": 0,
"doctype": "Sidebar",
"header_icon": "archive",
"idx": 0,
"items": [
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "house",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Home",
"link_to": "Assets",
"link_type": "Workspace",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "chart-column",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Dashboard",
"link_to": "Asset",
"link_type": "Dashboard",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "laptop",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset",
"link_to": "Asset",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "trending-down",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Depreciation Schedule",
"link_to": "Asset Depreciation Schedule",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "sprout",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Capitalization",
"link_to": "Asset Capitalization",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "move-horizontal",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Movement",
"link_to": "Asset Movement",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "rocket",
"indent": 1,
"is_default_module": 0,
"keep_closed": 1,
"label": "Maintenance",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Maintenance Team",
"link_to": "Asset Maintenance Team",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Maintenance",
"link_to": "Asset Maintenance",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Maintenance Log",
"link_to": "Asset Maintenance Log",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Value Adjustment",
"link_to": "Asset Value Adjustment",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Repair",
"link_to": "Asset Repair",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "sheet",
"indent": 1,
"is_default_module": 0,
"keep_closed": 1,
"label": "Reports",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Fixed Asset Register",
"link_to": "Fixed Asset Register",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Depreciation Ledger",
"link_to": "Asset Depreciation Ledger",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Depreciations and Balances",
"link_to": "Asset Depreciations and Balances",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Maintenance",
"link_to": "Asset Maintenance",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Activity",
"link_to": "Asset Activity",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "database",
"indent": 1,
"is_default_module": 0,
"keep_closed": 1,
"label": "Setup",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Item",
"link_to": "Item",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Asset Category",
"link_to": "Asset Category",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Location",
"link_to": "Location",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "settings",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Settings",
"link_to": "Accounts Settings",
"link_type": "DocType",
"navigate_to_tab": "assets_tab",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
}
],
"modified": "2026-08-16 00:00:00.000000",
"modified_by": "Administrator",
"module": "Assets",
"name": "Assets",
"owner": "Administrator",
"sequence_id": 0.0,
"standard": 1,
"title": "Assets"
}

View File

@@ -6,7 +6,7 @@
"label": "Asset Value Analytics"
}
],
"content": "[{\"id\": \"91d49bd2aa\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Asset Value Analytics\", \"col\": 12}}, {\"id\": \"6e2f5c71fd\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Total Assets\", \"col\": 4}}, {\"id\": \"812fb9cc2a\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Asset Value\", \"col\": 4}}, {\"id\": \"f8f0df5c57\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"New Assets (This Year)\", \"col\": 4}}]",
"content": "[{\"id\":\"Q-Cl7bMXDm\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Asset Value Analytics\",\"col\":12}},{\"id\":\"gsSQjvl0Tx\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"xRYRq1sW1O\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"Kx2j5N9BKZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Assets\",\"col\":4}},{\"id\":\"jeNsxtLaH3\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}},{\"id\":\"EX5e3NvL51\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:43:27.634865",
"custom_blocks": [],
"docstatus": 0,
@@ -17,26 +17,194 @@
"idx": 0,
"is_hidden": 0,
"label": "Assets",
"links": [],
"modified": "2026-08-26 18:34:03.000000",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Assets",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Asset",
"link_count": 0,
"link_to": "Asset",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Location",
"link_count": 0,
"link_to": "Location",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Category",
"link_count": 0,
"link_to": "Asset Category",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Movement",
"link_count": 0,
"link_to": "Asset Movement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Maintenance",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Maintenance Team",
"link_count": 0,
"link_to": "Asset Maintenance Team",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Asset Maintenance Team",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Maintenance",
"link_count": 0,
"link_to": "Asset Maintenance",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Asset Maintenance",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Maintenance Log",
"link_count": 0,
"link_to": "Asset Maintenance Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Asset",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Value Adjustment",
"link_count": 0,
"link_to": "Asset Value Adjustment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Asset",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Repair",
"link_count": 0,
"link_to": "Asset Repair",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Asset",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Capitalization",
"link_count": 0,
"link_to": "Asset Capitalization",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Asset",
"hidden": 0,
"is_query_report": 1,
"label": "Asset Depreciation Ledger",
"link_count": 0,
"link_to": "Asset Depreciation Ledger",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Asset",
"hidden": 0,
"is_query_report": 1,
"label": "Asset Depreciations and Balances",
"link_count": 0,
"link_to": "Asset Depreciations and Balances",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Asset Maintenance",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Maintenance",
"link_count": 0,
"link_to": "Asset Maintenance",
"link_type": "Report",
"onboard": 0,
"report_ref_doctype": "Asset Maintenance",
"type": "Link"
},
{
"dependencies": "Asset Activity",
"hidden": 0,
"is_query_report": 0,
"label": "Asset Activity",
"link_count": 0,
"link_to": "Asset Activity",
"link_type": "Report",
"onboard": 0,
"report_ref_doctype": "Asset Activity",
"type": "Link"
}
],
"modified": "2026-07-03 13:44:08.417956",
"modified_by": "Administrator",
"module": "Assets",
"module_onboarding": "Asset Onboarding",
"name": "Assets",
"number_cards": [
{
"label": "Total Assets",
"number_card_name": "Total Assets"
},
{
"label": "Asset Value",
"number_card_name": "Asset Value"
},
{
"label": "New Assets (This Year)",
"number_card_name": "New Assets (This Year)"
}
],
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,

View File

@@ -10,7 +10,6 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt, get_link_to_form
from erpnext.accounts.party import get_party_account
from erpnext.controllers.mapper import get_qty_already_mapped
from erpnext.controllers.status_updater import get_allowance_for
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults
@@ -35,14 +34,12 @@ def make_purchase_receipt(
def is_unit_price_row(source):
return has_unit_price_items and source.qty == 0
mapped_qty_by_item = get_qty_already_mapped(target_doc, "purchase_order_item")
def get_max_receivable_qty(source):
tolerance = flt(get_allowance_for(source.item_code, qty_or_amount="qty")[0])
return flt(source.qty) * (100 + tolerance) / 100
def update_item(obj, target, source_parent):
received_qty = flt(obj.received_qty) + flt(mapped_qty_by_item.get(obj.name, 0))
received_qty = flt(obj.received_qty)
qty = flt(obj.qty)
pending_qty = qty - received_qty
@@ -87,13 +84,11 @@ def make_purchase_receipt(
},
"postprocess": update_item,
"condition": lambda doc: (
doc.name not in mapped_qty_by_item
True
if is_unit_price_row(doc)
else abs(doc.received_qty) + abs(mapped_qty_by_item.get(doc.name, 0))
< abs(get_max_receivable_qty(doc))
else abs(doc.received_qty) < abs(get_max_receivable_qty(doc))
)
and doc.delivered_by_supplier != 1
and not doc.closed
and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
@@ -153,13 +148,9 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
)
return query.run(pluck="qty")[0] or 0
mapped_qty_by_item = get_qty_already_mapped(target_doc, "po_detail")
def get_billed_and_mapped_qty(po_item_name):
return flt(get_billed_qty(po_item_name)) + flt(mapped_qty_by_item.get(po_item_name, 0))
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - get_billed_and_mapped_qty(obj.name)
billed_qty = flt(get_billed_qty(obj.name))
target.qty = flt(obj.qty) - billed_qty
item = get_item_defaults(target.item_code, source_parent.company)
item_group = get_item_group_defaults(target.item_code, source_parent.company)
@@ -202,8 +193,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
or abs(doc.billed_amt) < abs(doc.amount)
or doc.qty > flt(get_billed_qty(doc.name))
)
and (doc.name not in mapped_qty_by_item or doc.qty > get_billed_and_mapped_qty(doc.name))
and not doc.closed
and select_item(doc),
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},

View File

@@ -14,9 +14,7 @@ frappe.ui.form.on("Purchase Order", {
setup: function (frm) {
frm.set_indicator_formatter("item_code", function (doc) {
let color;
if (doc.closed) {
color = "gray";
} else if (!doc.qty && frm.doc.has_unit_price_items) {
if (!doc.qty && frm.doc.has_unit_price_items) {
color = "yellow";
} else if (doc.qty <= doc.received_qty) {
color = "green";
@@ -342,7 +340,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
}
} else if (["Closed", "Delivered"].includes(doc.status)) {
if (this.frm.has_perm("submit") && !doc.items.every((item) => item.closed)) {
if (this.frm.has_perm("submit")) {
this.frm.add_custom_button(
__("Re-open"),
() => this.unclose_purchase_order(),
@@ -354,7 +352,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
if (doc.status != "On Hold") {
if (
(doc.items
.filter((item) => !item.delivered_by_supplier && !item.closed)
.filter((item) => !item.delivered_by_supplier)
.some((item) => item.received_qty < item.qty) ||
doc.__onload?.has_pending_receivable_qty) &&
allow_receipt
@@ -367,11 +365,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (doc.is_subcontracted) {
if (
!doc.items
.filter((item) => !item.closed)
.every((item) => item.qty == item.subcontracted_qty)
) {
if (!doc.items.every((item) => item.qty == item.subcontracted_qty)) {
this.frm.add_custom_button(
__("Subcontracting Order"),
() => {
@@ -439,8 +433,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
} else if (doc.docstatus === 0) {
this.frm.cscript.add_from_mappers();
}
this.set_item_close_buttons();
}
validate() {
@@ -535,7 +527,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
},
get_query_filters: {
docstatus: 1,
status: ["not in", ["Stopped", "Ordered", "Expired"]],
status: ["not in", ["Stopped", "Expired"]],
},
allow_child_item_selection: true,
child_fieldname: "items",
@@ -705,19 +697,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
this.frm.cscript.update_status("Close", "Closed");
}
set_item_close_buttons() {
erpnext.item_close.add_buttons(
this.frm,
erpnext.item_close.fulfilment_config({
qty_field: "received_qty",
qty_label: __("Received Qty"),
help: __(
"Closed rows stop being expected. Their pending quantity is written off and they are skipped when creating a Purchase Receipt or Purchase Invoice."
),
})
);
}
update_dropship_delivered_qty() {
const data = this.frm.doc.items
.filter((item) => item.delivered_by_supplier == 1)

View File

@@ -178,15 +178,7 @@ class PurchaseOrder(BuyingController):
"global_allowance_field": "over_order_allowance",
"global_allowance_doctype": "Buying Settings",
"item_allowance_field": "over_order_allowance",
},
{
"source_dt": "Purchase Order Item",
"target_dt": "Supplier Quotation Item",
"join_field": "supplier_quotation_item",
"target_field": "ordered_qty",
"target_ref_field": "stock_qty",
"source_field": "stock_qty",
},
}
]
def onload(self):
@@ -258,7 +250,6 @@ class PurchaseOrder(BuyingController):
["conversion_factor", "="],
],
"is_child_table": True,
"allow_duplicate_prev_row_id": True,
},
"Material Request": {
"ref_dn_field": "material_request",
@@ -411,25 +402,6 @@ class PurchaseOrder(BuyingController):
def update_status(self, status):
StatusService(self).update_status(status)
def on_item_close_status_change(self):
StatusService(self).recalculate_after_item_close()
def is_item_closable(self, item):
return flt(item.received_qty) < flt(item.qty) or super().is_item_closable(item)
def update_prevdoc_status(self):
super().update_prevdoc_status()
for supplier_quotation in {item.supplier_quotation for item in self.items}:
if not supplier_quotation:
continue
doc = frappe.get_doc("Supplier Quotation", supplier_quotation)
if doc.docstatus.is_cancelled():
frappe.throw(_("Supplier Quotation {0} is cancelled").format(supplier_quotation))
doc.set_status(update=True)
def on_submit(self):
super().on_submit()
@@ -559,7 +531,7 @@ class PurchaseOrder(BuyingController):
considering the configured over_delivery_receipt_allowance.
"""
for item in self.get("items", []):
if item.delivered_by_supplier or item.closed:
if item.delivered_by_supplier:
continue
tolerance = flt(get_allowance_for(item.item_code, qty_or_amount="qty")[0])
max_receivable_qty = flt(item.qty) * (100 + tolerance) / 100

View File

@@ -9,7 +9,6 @@ from frappe.desk.notifications import clear_doctype_notifications
from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.services.subcontracting import SubcontractingService
from erpnext.controllers.item_close import validate_parent_reopen
class StatusService:
@@ -19,10 +18,6 @@ class StatusService:
def update_status(self, status: str) -> None:
doc = self.doc
self.check_modified_date()
if status != "Closed" and doc.status == "Closed":
validate_parent_reopen(doc)
doc.set_status(update=True, status=status)
doc.update_requested_qty()
doc.update_ordered_qty()
@@ -31,17 +26,6 @@ class StatusService:
doc.notify_update()
clear_doctype_notifications(doc)
def recalculate_after_item_close(self) -> None:
"""Refresh progress after row flags changed.
`update_billing_percentage` runs last because it reloads the parent and
writes the final status from both percentages.
"""
doc = self.doc
self.update_receiving_percentage()
doc.update_ordered_qty()
doc.update_billing_percentage()
def check_modified_date(self) -> None:
doc = self.doc
modified_in_db = frappe.db.get_value("Purchase Order", doc.name, "modified")
@@ -55,9 +39,10 @@ class StatusService:
def update_receiving_percentage(self) -> None:
doc = self.doc
total_qty, received_qty = 0.0, 0.0
for item in [item for item in doc.items if not item.closed] or doc.items:
for item in doc.items:
received_qty += min(item.received_qty, item.qty)
total_qty += item.qty
per_received = flt(received_qty / total_qty) * 100 if total_qty else 0
doc.db_set("per_received", per_received, update_modified=False)
if total_qty and received_qty:
doc.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False)
else:
doc.db_set("per_received", 0, update_modified=False)

View File

@@ -333,38 +333,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
def test_update_child_qty_with_conversion_factor_after_receipt(self):
item = make_item(uoms=[{"uom": "Box", "conversion_factor": 5}])
purchase_order = create_purchase_order(item_code=item.item_code, qty=6, do_not_save=True)
purchase_order.items[0].uom = "Box"
purchase_order.items[0].conversion_factor = 5
purchase_order.save()
purchase_order.submit()
create_pr_against_po(purchase_order.name, 2)
row = purchase_order.items[0]
trans_items = json.dumps(
[
{
"item_code": row.item_code,
"rate": row.rate,
"qty": 4,
"uom": row.uom,
"conversion_factor": 2,
"docname": row.name,
}
]
)
self.assertRaisesRegex(
frappe.ValidationError,
"Cannot set quantity less than received quantity",
update_child_qty_rate,
"Purchase Order",
trans_items,
purchase_order.name,
)
def test_update_child_adding_new_item(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
@@ -410,7 +378,9 @@ class TestPurchaseOrder(ERPNextTestSuite):
po.submit()
first_item_of_po = po.get("items")[0]
company_default = frappe.db.get_value("Company", po.company, "default_warehouse")
frappe.db.set_value("Company", po.company, "default_warehouse", None)
self.addCleanup(frappe.db.set_value, "Company", po.company, "default_warehouse", company_default)
def get_trans_items(item_code):
return json.dumps(
@@ -824,9 +794,14 @@ class TestPurchaseOrder(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, below_minimum.insert)
def test_marginal_min_order_qty_overage_toast(self):
original_precision = frappe.db.get_default("float_precision")
frappe.db.set_default("float_precision", "3")
self.addCleanup(frappe.db.set_default, "float_precision", original_precision)
item_doc = make_item(properties={"min_order_qty": 50000, "stock_uom": "_Test UOM 1"})
if not frappe.db.exists("UOM", "Gram"):
frappe.get_doc({"doctype": "UOM", "uom_name": "Gram"}).insert()
item_doc = make_item(properties={"min_order_qty": 50000, "stock_uom": "Gram"})
item_doc.append("uoms", {"uom": "Pound", "conversion_factor": 453.592292197})
item_doc.save()
item = item_doc.name
@@ -1824,11 +1799,25 @@ def create_po_for_sc_testing():
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
company = "_Test Company with perpetual inventory"
create_internal_customer(
"_Test Internal Customer 2",
company,
company,
)
create_internal_supplier(
"_Test Internal Supplier 2",
company,
company,
)
warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company)
create_warehouse("_Test Internal Warehouse GIT", company=company)

View File

@@ -86,7 +86,6 @@
"returned_qty",
"column_break_60",
"billed_amt",
"closed",
"accounting_details",
"expense_account",
"column_break_fyqr",
@@ -558,7 +557,6 @@
"fieldname": "blanket_order_rate",
"fieldtype": "Currency",
"label": "Blanket Order Rate",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -647,15 +645,6 @@
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "closed",
"fieldtype": "Check",
"label": "Closed",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges",
"fieldname": "item_tax_rate",
@@ -955,7 +944,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-08-27 11:55:37.000000",
"modified": "2026-08-07 17:31:31.732720",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -29,7 +29,6 @@ class PurchaseOrderItem(Document):
blanket_order_rate: DF.Currency
bom: DF.Link | None
brand: DF.Link | None
closed: DF.Check
company_total_stock: DF.Float
conversion_factor: DF.Float
cost_center: DF.Link | None

View File

@@ -8,8 +8,6 @@ from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.controllers.mapper import get_qty_already_mapped
@frappe.whitelist()
def make_purchase_order(
@@ -18,9 +16,6 @@ def make_purchase_order(
if args is None:
args = {}
args = frappe.parse_json(args)
ordered_items = get_ordered_items(source_name)
mapped_items = get_qty_already_mapped(target_doc, "supplier_quotation_item")
def set_missing_values(source, target):
target.run_method("set_missing_values")
@@ -28,9 +23,7 @@ def make_purchase_order(
target.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent):
balance_stock_qty = obj.stock_qty - ordered_items.get(obj.name, 0.0)
target.stock_qty = balance_stock_qty if balance_stock_qty > 0 else 0
target.qty = flt(target.stock_qty) / flt(obj.conversion_factor)
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
def select_item(d):
filtered_items = args.get("filtered_children", [])
@@ -58,9 +51,7 @@ def make_purchase_order(
["sales_order", "sales_order"],
],
"postprocess": update_item,
"condition": lambda item: item.name not in mapped_items
and (item.stock_qty > ordered_items.get(item.name, 0.0) or item.qty == 0)
and select_item(item),
"condition": select_item,
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
@@ -116,14 +107,3 @@ def make_quotation(source_name: str, target_doc: str | dict | Document | None =
)
return doclist
def get_ordered_items(supplier_quotation: str) -> frappe._dict:
return frappe._dict(
frappe.get_all(
"Supplier Quotation Item",
{"docstatus": 1, "parent": supplier_quotation, "ordered_qty": (">", 0)},
["name", "ordered_qty"],
as_list=True,
)
)

View File

@@ -34,10 +34,7 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
if (this.frm.doc.__islocal && !this.frm.doc.valid_till) {
this.frm.set_value("valid_till", frappe.datetime.add_months(this.frm.doc.transaction_date, 1));
}
if (
this.frm.doc.docstatus === 1 &&
!["Stopped", "Ordered", "Expired"].includes(this.frm.doc.status)
) {
if (this.frm.doc.docstatus === 1) {
this.frm.add_custom_button(
__("Purchase Order"),
this.make_purchase_order.bind(this),

View File

@@ -767,7 +767,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nPartially Ordered\nOrdered\nStopped\nCancelled\nExpired",
"options": "\nDraft\nSubmitted\nStopped\nCancelled\nExpired",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
@@ -948,7 +948,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-08-30 16:30:00.000000",
"modified": "2026-08-21 23:11:50.274992",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@@ -6,13 +6,10 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
from pypika.terms import ExistsCriterion
from erpnext.buying.utils import validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from .mapper import get_ordered_items
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -89,9 +86,7 @@ class SupplierQuotation(BuyingController):
shipping_address: DF.Link | None
shipping_address_display: DF.TextEditor | None
shipping_rule: DF.Link | None
status: DF.Literal[
"", "Draft", "Submitted", "Partially Ordered", "Ordered", "Stopped", "Cancelled", "Expired"
]
status: DF.Literal["", "Draft", "Submitted", "Stopped", "Cancelled", "Expired"]
supplier: DF.Link
supplier_address: DF.Link | None
supplier_name: DF.Data | None
@@ -117,17 +112,13 @@ class SupplierQuotation(BuyingController):
def validate(self):
super().validate()
self.set_status()
if not self.status:
self.status = "Draft"
from erpnext.controllers.status_updater import validate_status
validate_status(
self.status,
["Draft", "Submitted", "Partially Ordered", "Ordered", "Stopped", "Cancelled", "Expired"],
)
validate_status(self.status, ["Draft", "Submitted", "Stopped", "Cancelled"])
validate_for_items(self)
self.validate_with_previous_doc()
@@ -135,11 +126,11 @@ class SupplierQuotation(BuyingController):
self.validate_valid_till()
def on_submit(self):
self.set_status(update=True)
self.db_set("status", "Submitted")
self.update_rfq_supplier_status(1)
def on_cancel(self):
self.set_status(update=True)
self.db_set("status", "Cancelled")
self.update_rfq_supplier_status(0)
def on_trash(self):
@@ -175,24 +166,6 @@ class SupplierQuotation(BuyingController):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till Date cannot be before Transaction Date"))
def get_ordered_status(self):
ordered_items = get_ordered_items(self.name)
if not ordered_items:
return "Submitted"
for row in self.items:
if row.name not in ordered_items or row.stock_qty > ordered_items[row.name]:
return "Partially Ordered"
return "Ordered"
def is_fully_ordered(self):
return self.get_ordered_status() == "Ordered"
def is_partially_ordered(self):
return self.get_ordered_status() == "Partially Ordered"
def update_rfq_supplier_status(self, include_me):
from frappe.query_builder.functions import Count
@@ -270,29 +243,27 @@ def get_list_context(context=None):
def set_expired_status():
supplier_quotation = frappe.qb.DocType("Supplier Quotation")
purchase_order = frappe.qb.DocType("Purchase Order")
purchase_order_item = frappe.qb.DocType("Purchase Order Item")
purchase_order_against_quotation = (
frappe.qb.from_(purchase_order)
.from_(purchase_order_item)
.select(purchase_order.name)
.where(
(purchase_order_item.docstatus == 1)
& (purchase_order.docstatus == 1)
& (purchase_order_item.parent == purchase_order.name)
& (purchase_order_item.supplier_quotation == supplier_quotation.name)
)
# Only submitted quotations past their validity should be expired
frappe.db.set_value(
"Supplier Quotation",
{
"docstatus": 1,
"status": ["not in", ["Cancelled", "Stopped"]],
"valid_till": ["<", nowdate()],
},
"status",
"Expired",
update_modified=True,
)
(
frappe.qb.update(supplier_quotation)
.set(supplier_quotation.status, "Expired")
.where(
(supplier_quotation.docstatus == 1)
& (supplier_quotation.status.notin(["Expired", "Stopped"]))
& (supplier_quotation.valid_till < nowdate())
& ExistsCriterion(purchase_order_against_quotation).negate()
def get_purchased_items(supplier_quotation: str):
return frappe._dict(
frappe.get_all(
"Purchase Order Item",
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
fields=["supplier_quotation_item", {"SUM": "qty"}],
group_by="supplier_quotation_item",
as_list=1,
)
).run()
)

View File

@@ -1,12 +1,10 @@
frappe.listview_settings["Supplier Quotation"] = {
add_fields: ["supplier", "base_grand_total", "status", "company", "currency"],
get_indicator: function (doc) {
if (doc.status === "Submitted") {
return [__("Submitted"), "blue", "status,=,Submitted"];
} else if (doc.status === "Partially Ordered") {
return [__("Partially Ordered"), "yellow", "status,=,Partially Ordered"];
} else if (doc.status === "Ordered") {
if (doc.status === "Ordered") {
return [__("Ordered"), "green", "status,=,Ordered"];
} else if (doc.status === "Rejected") {
return [__("Lost"), "gray", "status,=,Lost"];
} else if (doc.status === "Expired") {
return [__("Expired"), "gray", "status,=,Expired"];
}

View File

@@ -15,8 +15,6 @@ from erpnext.buying.doctype.request_for_quotation.test_request_for_quotation imp
from erpnext.buying.doctype.supplier_quotation.mapper import make_purchase_order
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import set_expired_status
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.patches.v16_0.set_supplier_quotation_order_status import execute as set_order_status
from erpnext.tests.assertions import assert_raises_with_savepoint
from erpnext.tests.utils import ERPNextTestSuite
@@ -24,23 +22,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
def setUp(self):
self.load_test_records("Supplier Quotation")
def make_order(self, supplier_quotation, qty):
purchase_order = make_purchase_order(supplier_quotation.name)
purchase_order.naming_series = "_T-Purchase Order-"
purchase_order.items[0].qty = qty
purchase_order.items[0].schedule_date = add_days(today(), 1)
purchase_order.insert()
purchase_order.submit()
return purchase_order
def update_order_qty(self, purchase_order, qty):
item = purchase_order.items[0]
items = json.dumps(
[{"item_code": item.item_code, "rate": item.rate, "qty": qty, "docname": item.name}]
)
update_child_qty_rate("Purchase Order", items, purchase_order.name)
purchase_order.reload()
def test_valid_till_before_transaction_date_rejected(self):
rfq = make_request_for_quotation()
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.suppliers[0].supplier)
@@ -62,14 +43,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
valid.insert()
valid.submit()
partially_ordered = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.suppliers[1].supplier)
partially_ordered.valid_till = add_days(today(), 10)
partially_ordered.items[0].qty = 10
partially_ordered.insert()
partially_ordered.submit()
partial_order = self.make_order(partially_ordered, 4)
partially_ordered.db_set("valid_till", add_days(today(), -2))
# A past-validity draft must not be expired - "Expired" applies to submitted quotations only
draft = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.suppliers[0].supplier)
draft.transaction_date = add_days(today(), -10)
@@ -80,24 +53,8 @@ class TestPurchaseOrder(ERPNextTestSuite):
self.assertEqual(frappe.db.get_value("Supplier Quotation", expired.name, "status"), "Expired")
self.assertEqual(frappe.db.get_value("Supplier Quotation", valid.name, "status"), "Submitted")
self.assertEqual(
frappe.db.get_value("Supplier Quotation", partially_ordered.name, "status"),
"Partially Ordered",
)
self.assertEqual(frappe.db.get_value("Supplier Quotation", draft.name, "status"), "Draft")
partial_order.cancel()
self.assertEqual(
frappe.db.get_value("Supplier Quotation", partially_ordered.name, "status"),
"Submitted",
)
set_expired_status()
self.assertEqual(
frappe.db.get_value("Supplier Quotation", partially_ordered.name, "status"),
"Expired",
)
def test_submit_and_cancel_updates_rfq_quote_status(self):
rfq = make_request_for_quotation()
supplier_row = rfq.suppliers[0]
@@ -115,150 +72,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
"Pending",
)
def test_purchase_order_updates_order_status(self):
supplier_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
supplier_quotation.submit()
self.assertEqual(supplier_quotation.status, "Submitted")
partial_order = self.make_order(supplier_quotation, 4)
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Partially Ordered")
self.assertEqual(supplier_quotation.items[0].ordered_qty, 4)
self.update_order_qty(partial_order, 10)
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Ordered")
self.assertEqual(supplier_quotation.items[0].ordered_qty, 10)
self.update_order_qty(partial_order, 4)
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Partially Ordered")
complete_order = self.make_order(supplier_quotation, 6)
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Ordered")
complete_order.cancel()
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Partially Ordered")
partial_order.cancel()
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Submitted")
self.assertEqual(supplier_quotation.items[0].ordered_qty, 0)
def test_purchase_order_maps_remaining_quotation_qty(self):
supplier_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
supplier_quotation.submit()
self.make_order(supplier_quotation, 4)
purchase_order = make_purchase_order(supplier_quotation.name)
self.assertEqual(purchase_order.items[0].qty, 6)
purchase_order.items[0].schedule_date = add_days(today(), 1)
purchase_order.submit()
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Ordered")
self.assertEqual(make_purchase_order(supplier_quotation.name).items, [])
def test_purchase_order_skips_fully_ordered_quotation_rows(self):
supplier_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
supplier_quotation.append("items", {"item_code": "_Test Item 2", "qty": 3, "rate": 100})
supplier_quotation.submit()
first_order = make_purchase_order(
supplier_quotation.name,
args={"filtered_children": [supplier_quotation.items[0].name]},
)
first_order.items[0].schedule_date = add_days(today(), 1)
first_order.submit()
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Partially Ordered")
second_order = make_purchase_order(supplier_quotation.name)
self.assertEqual(len(second_order.items), 1)
self.assertEqual(second_order.items[0].item_code, "_Test Item 2")
self.assertEqual(second_order.items[0].qty, 3)
second_order.items[0].schedule_date = add_days(today(), 1)
second_order.submit()
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Ordered")
def test_purchase_order_cannot_exceed_supplier_quotation_qty(self):
supplier_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
supplier_quotation.items[0].qty = 5
supplier_quotation.submit()
first_order = make_purchase_order(supplier_quotation.name)
second_order = make_purchase_order(supplier_quotation.name)
for purchase_order in (first_order, second_order):
purchase_order.items[0].schedule_date = add_days(today(), 1)
first_order.submit()
self.assertRaises(frappe.ValidationError, second_order.submit)
def test_removing_purchase_order_item_updates_quotation_status(self):
first_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
first_quotation.submit()
second_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
second_item = second_quotation.items[0]
second_item.item_code = second_item.item_name = "_Test Item 2"
second_item.stock_uom = second_item.uom = frappe.db.get_value("Item", "_Test Item 2", "stock_uom")
second_item.conversion_factor = 1
second_quotation.submit()
purchase_order = make_purchase_order(first_quotation.name)
purchase_order = make_purchase_order(second_quotation.name, purchase_order)
purchase_order.naming_series = "_T-Purchase Order-"
for item in purchase_order.items:
item.schedule_date = add_days(today(), 1)
purchase_order.insert()
purchase_order.submit()
first_quotation.reload()
second_quotation.reload()
self.assertEqual(first_quotation.status, "Ordered")
self.assertEqual(second_quotation.status, "Ordered")
remaining_item = next(
item for item in purchase_order.items if item.supplier_quotation == second_quotation.name
)
update_child_qty_rate(
"Purchase Order",
json.dumps(
[
{
"item_code": remaining_item.item_code,
"rate": remaining_item.rate,
"qty": remaining_item.qty,
"docname": remaining_item.name,
}
]
),
purchase_order.name,
)
first_quotation.reload()
second_quotation.reload()
self.assertEqual(first_quotation.status, "Submitted")
self.assertEqual(second_quotation.status, "Ordered")
def test_order_status_patch_updates_existing_quotation(self):
supplier_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
supplier_quotation.submit()
self.make_order(supplier_quotation, 4)
supplier_quotation.db_set("status", "Submitted")
frappe.db.set_value("Supplier Quotation Item", supplier_quotation.items[0].name, "ordered_qty", 0)
set_order_status()
supplier_quotation.reload()
self.assertEqual(supplier_quotation.status, "Partially Ordered")
self.assertEqual(supplier_quotation.items[0].ordered_qty, 4)
def test_update_child_supplier_quotation_add_item(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.submit()
@@ -313,52 +126,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)
def test_update_child_qty_with_conversion_factor_after_purchase(self):
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(uoms=[{"uom": "Box", "conversion_factor": 5}])
supplier_quotation = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
supplier_quotation.items[0].item_code = item.item_code
supplier_quotation.items[0].qty = 6
supplier_quotation.items[0].uom = "Box"
supplier_quotation.items[0].conversion_factor = 5
supplier_quotation.insert()
supplier_quotation.submit()
purchase_order = make_purchase_order(supplier_quotation.name)
purchase_order.schedule_date = add_days(today(), 1)
purchase_order.items[0].qty = 2
purchase_order.save()
purchase_order.submit()
def update_qty(qty):
row = supplier_quotation.items[0]
trans_items = json.dumps(
[
{
"item_code": row.item_code,
"rate": row.rate,
"qty": qty,
"uom": row.uom,
"conversion_factor": 2,
"docname": row.name,
}
]
)
update_child_qty_rate("Supplier Quotation", trans_items, supplier_quotation.name)
update_qty(5)
supplier_quotation.reload()
self.assertEqual(supplier_quotation.items[0].conversion_factor, 2)
self.assertEqual(supplier_quotation.items[0].stock_qty, 10)
self.assertRaisesRegex(
frappe.ValidationError,
"Cannot reduce quantity than ordered or purchased quantity",
update_qty,
4,
)
def test_update_supplier_quotation_child_remove_item(self):
sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0])
sq.submit()
@@ -394,9 +161,12 @@ class TestPurchaseOrder(ERPNextTestSuite):
]
)
frappe.db.savepoint("before_cancel")
# check if item having purchase order can be removed
with assert_raises_with_savepoint(self, frappe.LinkExistsError):
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
self.assertRaises(
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
)
frappe.db.rollback(save_point="before_cancel")
trans_item = json.dumps(
[

View File

@@ -230,17 +230,6 @@
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "ordered_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Ordered Qty",
"no_copy": 1,
"non_negative": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "base_price_list_rate",
"fieldtype": "Currency",
@@ -626,7 +615,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-08-31 12:00:00.000000",
"modified": "2026-08-07 17:31:31.732720",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation Item",

View File

@@ -44,7 +44,6 @@ class SupplierQuotationItem(Document):
material_request_item: DF.Data | None
net_amount: DF.Currency
net_rate: DF.Currency
ordered_qty: DF.Float
page_break: DF.Check
parent: DF.Data
parentfield: DF.Data

View File

@@ -96,17 +96,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
],
default: "Submitted",
},
{
fieldname: "order_status",
label: __("Order Status"),
fieldtype: "Select",
options: [
{ label: "", value: "" },
{ label: __("Not Ordered"), value: "Not Ordered" },
{ label: __("Partially Ordered"), value: "Partially Ordered" },
{ label: __("Ordered"), value: "Ordered" },
],
},
{
fieldtype: "Check",
label: __("Include Expired"),

View File

@@ -52,7 +52,6 @@ def get_data(filters):
sq_item.request_for_quotation,
sq_item.lead_time_days,
sq.supplier.as_("supplier_name"),
sq.status.as_("supplier_quotation_status"),
sq.valid_till,
)
.where(
@@ -83,11 +82,6 @@ def get_data(filters):
if filters.get("supplier"):
query = query.where(sq.supplier.isin(filters.get("supplier")))
if filters.get("order_status") == "Not Ordered":
query = query.where(sq.status.notin(["Partially Ordered", "Ordered"]))
elif filters.get("order_status"):
query = query.where(sq.status == filters.get("order_status"))
if not filters.get("include_expired"):
query = query.where(sq.status != "Expired")
@@ -115,7 +109,6 @@ def prepare_data(supplier_quotation_data, filters):
else data.get("item_code"), # leave blank if group by field
"supplier_name": "" if group_by_field == "supplier_name" else data.get("supplier_name"),
"quotation": data.get("parent"),
"order_status": get_order_status(data.get("supplier_quotation_status")),
"qty": data.get("qty"),
"price": flt(data.get("amount"), float_precision),
"uom": data.get("uom"),
@@ -171,10 +164,6 @@ def prepare_data(supplier_quotation_data, filters):
return out, chart_data
def get_order_status(status):
return status if status in ("Partially Ordered", "Ordered") else "Not Ordered"
def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
data_points_map = {}
qty_list.sort()
@@ -276,12 +265,6 @@ def get_columns(filters):
"options": "Supplier Quotation",
"width": 200,
},
{
"fieldname": "order_status",
"label": _("Order Status"),
"fieldtype": "Data",
"width": 130,
},
{"fieldname": "valid_till", "label": _("Valid Till"), "fieldtype": "Date", "width": 100},
{
"fieldname": "lead_time_days",

View File

@@ -2,9 +2,7 @@
# See license.txt
import frappe
from frappe.utils import add_days, today
from erpnext.buying.doctype.supplier_quotation.mapper import make_purchase_order
from erpnext.buying.report.supplier_quotation_comparison.supplier_quotation_comparison import execute
from erpnext.tests.utils import ERPNextTestSuite
@@ -40,15 +38,6 @@ class TestSupplierQuotationComparison(ERPNextTestSuite):
filters.update(extra)
return execute(filters)[1]
def make_order(self, supplier_quotation, qty):
purchase_order = make_purchase_order(supplier_quotation.name)
purchase_order.naming_series = "_T-Purchase Order-"
purchase_order.items[0].qty = qty
purchase_order.items[0].schedule_date = add_days(today(), 1)
purchase_order.insert()
purchase_order.submit()
return purchase_order
def test_no_filters_returns_empty(self):
self.assertEqual(execute(None)[1], [])
@@ -94,34 +83,3 @@ class TestSupplierQuotationComparison(ERPNextTestSuite):
both = names(status="")
self.assertIn(draft.name, both)
self.assertIn(submitted.name, both)
def test_order_status_and_filter(self):
supplier_quotation = self.make_quotation("_Test Supplier", qty=10, rate=100)
def get_order_status():
return next(
row["order_status"]
for row in self.run_report(item_code=ITEM)
if row["quotation"] == supplier_quotation.name
)
def quotations_with_status(order_status):
return {row["quotation"] for row in self.run_report(item_code=ITEM, order_status=order_status)}
self.assertEqual(get_order_status(), "Not Ordered")
self.assertIn(supplier_quotation.name, quotations_with_status("Not Ordered"))
partial_order = self.make_order(supplier_quotation, qty=4)
self.assertEqual(get_order_status(), "Partially Ordered")
self.assertIn(supplier_quotation.name, quotations_with_status("Partially Ordered"))
self.assertNotIn(supplier_quotation.name, quotations_with_status("Ordered"))
complete_order = self.make_order(supplier_quotation, qty=6)
self.assertEqual(get_order_status(), "Ordered")
self.assertIn(supplier_quotation.name, quotations_with_status("Ordered"))
complete_order.cancel()
self.assertEqual(get_order_status(), "Partially Ordered")
partial_order.cancel()
self.assertEqual(get_order_status(), "Not Ordered")

View File

@@ -1,550 +0,0 @@
{
"app": "erpnext",
"creation": "2026-08-16 00:00:00",
"docstatus": 0,
"doctype": "Sidebar",
"header_icon": "shopping-cart",
"idx": 0,
"items": [
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "house",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Home",
"link_to": "Buying",
"link_type": "Workspace",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "chart-column",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Dashboard",
"link_to": "Buying",
"link_type": "Dashboard",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "notepad-text",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Material Request",
"link_to": "Material Request",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "git-pull-request-arrow",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Request for Quotation",
"link_to": "Request for Quotation",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "book-open-text",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Quotation",
"link_to": "Supplier Quotation",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "receipt-text",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Order",
"link_to": "Purchase Order",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "scale",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Invoice",
"link_to": "Purchase Invoice",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "database",
"indent": 1,
"is_default_module": 0,
"keep_closed": 1,
"label": "Setup",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier",
"link_to": "Supplier",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Group",
"link_to": "Supplier Group",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Item",
"link_to": "Item",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Price List",
"link_to": "Price List",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Address",
"link_to": "Address",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Contacts",
"link_to": "Contact",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Scorecard",
"link_to": "Supplier Scorecard",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Scorecard Criteria",
"link_to": "Supplier Scorecard Criteria",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Scorecard Variable",
"link_to": "Supplier Scorecard Variable",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Scorecard Standing",
"link_to": "Supplier Scorecard Standing",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "sheet",
"indent": 1,
"is_default_module": 0,
"keep_closed": 1,
"label": "Reports",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Section Break"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Analytics",
"link_to": "Purchase Analytics",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Order Analysis",
"link_to": "Purchase Order Analysis",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Requested Items to Order and Receive",
"link_to": "Requested Items to Order and Receive",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Items To Be Requested",
"link_to": "Items To Be Requested",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Item-wise Purchase History",
"link_to": "Item-wise Purchase History",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Receipt Trends ",
"link_to": "Purchase Receipt Trends",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Invoice Trends",
"link_to": "Purchase Invoice Trends",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Purchase Order Trends",
"link_to": "Purchase Order Trends",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Procurement Tracker",
"link_to": "Procurement Tracker",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Item Wise Consumption",
"link_to": "Item Wise Consumption",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Quotation Comparison",
"link_to": "Supplier Quotation Comparison",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Supplier Addresses And Contacts",
"link_to": "Address And Contacts",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Subcontract Order Summary",
"link_to": "Subcontract Order Summary",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Materials To Be Transferred",
"link_to": "Subcontracted Raw Materials To Be Transferred",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 1,
"collapsible": 1,
"hidden": 0,
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Items To Be Received",
"link_to": "Subcontracted Item To Be Received",
"link_type": "Report",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
},
{
"added": 0,
"child": 0,
"collapsible": 1,
"hidden": 0,
"icon": "settings",
"indent": 0,
"is_default_module": 0,
"keep_closed": 0,
"label": "Settings",
"link_to": "Buying Settings",
"link_type": "DocType",
"open_in_new_tab": 0,
"show_arrow": 0,
"type": "Link"
}
],
"modified": "2026-09-01 17:37:29.971719",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
"owner": "Administrator",
"standard": 1,
"title": "Buying"
}

View File

@@ -2,11 +2,11 @@
"app": "erpnext",
"charts": [
{
"chart_name": "Purchase Order Analysis",
"label": "Purchase Order Analysis"
"chart_name": "Purchase Order Trends",
"label": "Purchase Order Trends"
}
],
"content": "[{\"id\": \"983cd8ea63\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Purchase Order Analysis\", \"col\": 12}}, {\"id\": \"d88a483751\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Purchase Orders to Receive\", \"col\": 4}}, {\"id\": \"22a7c36674\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Purchase Orders to Bill\", \"col\": 4}}, {\"id\": \"5fe5b5c369\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Annual Purchase\", \"col\": 4}}]",
"content": "[{\"id\":\"j3dJGo8Ok6\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Purchase Order Trends\",\"col\":12}},{\"id\":\"k75jSq2D6Z\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Purchase Orders Count\",\"col\":4}},{\"id\":\"UPXys0lQLj\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Purchase Amount\",\"col\":4}},{\"id\":\"yQGK3eb2hg\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Average Order Values\",\"col\":4}},{\"id\":\"oN7lXSwQji\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"Xe2GVLOq8J\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"QwqyG6XuUt\",\"type\":\"card\",\"data\":{\"card_name\":\"Buying\",\"col\":4}},{\"id\":\"bTPjOxC_N_\",\"type\":\"card\",\"data\":{\"card_name\":\"Items & Pricing\",\"col\":4}},{\"id\":\"87ht0HIneb\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"EDOsBOmwgw\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier\",\"col\":4}},{\"id\":\"oWNNIiNb2i\",\"type\":\"card\",\"data\":{\"card_name\":\"Supplier Scorecard\",\"col\":4}},{\"id\":\"7F_13-ihHB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"pfwiLvionl\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}},{\"id\":\"8ySDy6s4qn\",\"type\":\"card\",\"data\":{\"card_name\":\"Regional\",\"col\":4}}]",
"creation": "2020-01-28 11:50:26.195467",
"custom_blocks": [],
"docstatus": 0,
@@ -17,24 +17,507 @@
"idx": 0,
"is_hidden": 0,
"label": "Buying",
"links": [],
"modified": "2026-08-26 18:34:03.000000",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Buying",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Item",
"hidden": 0,
"is_query_report": 0,
"label": "Material Request",
"link_count": 0,
"link_to": "Material Request",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Item, Supplier",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Order",
"link_count": 0,
"link_to": "Purchase Order",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Item, Supplier",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Invoice",
"link_count": 0,
"link_to": "Purchase Invoice",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Item, Supplier",
"hidden": 0,
"is_query_report": 0,
"label": "Request for Quotation",
"link_count": 0,
"link_to": "Request for Quotation",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Item, Supplier",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Quotation",
"link_count": 0,
"link_to": "Supplier Quotation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Items & Pricing",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Item",
"link_count": 0,
"link_to": "Item",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Item Price",
"link_count": 0,
"link_to": "Item Price",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Price List",
"link_count": 0,
"link_to": "Price List",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Product Bundle",
"link_count": 0,
"link_to": "Product Bundle",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Item Group",
"link_count": 0,
"link_to": "Item Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Promotional Scheme",
"link_count": 0,
"link_to": "Promotional Scheme",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Pricing Rule",
"link_count": 0,
"link_to": "Pricing Rule",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Buying Settings",
"link_count": 0,
"link_to": "Buying Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Taxes and Charges Template",
"link_count": 0,
"link_to": "Purchase Taxes and Charges Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Terms and Conditions Template",
"link_count": 0,
"link_to": "Terms and Conditions",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"link_to": "Supplier",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Group",
"link_count": 0,
"link_to": "Supplier Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Contact",
"link_count": 0,
"link_to": "Contact",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Address",
"link_count": 0,
"link_to": "Address",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Scorecard",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Scorecard",
"link_count": 0,
"link_to": "Supplier Scorecard",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Scorecard Variable",
"link_count": 0,
"link_to": "Supplier Scorecard Variable",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Scorecard Criteria",
"link_count": 0,
"link_to": "Supplier Scorecard Criteria",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Supplier Scorecard Standing",
"link_count": 0,
"link_to": "Supplier Scorecard Standing",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Analytics",
"link_count": 0,
"link_to": "Purchase Analytics",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Order Analysis",
"link_count": 0,
"link_to": "Purchase Order Analysis",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Items to Order and Receive",
"link_count": 0,
"link_to": "Requested Items to Order and Receive",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Order Trends",
"link_count": 0,
"link_to": "Purchase Order Trends",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Procurement Tracker",
"link_count": 0,
"link_to": "Procurement Tracker",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Other Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Items To Be Requested",
"link_count": 0,
"link_to": "Items To Be Requested",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Item-wise Purchase History",
"link_count": 0,
"link_to": "Item-wise Purchase History",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Receipt Trends",
"link_count": 0,
"link_to": "Purchase Receipt Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Invoice Trends",
"link_count": 0,
"link_to": "Purchase Invoice Trends",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Subcontracted Raw Materials To Be Transferred",
"link_count": 0,
"link_to": "Subcontracted Raw Materials To Be Transferred",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Subcontracted Item To Be Received",
"link_count": 0,
"link_to": "Subcontracted Item To Be Received",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Supplier Quotation Comparison",
"link_count": 0,
"link_to": "Supplier Quotation Comparison",
"link_type": "Report",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Material Requests for which Supplier Quotations are not created",
"link_count": 0,
"link_to": "Material Requests for which Supplier Quotations are not created",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Supplier Addresses And Contacts",
"link_count": 0,
"link_to": "Address And Contacts",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Regional",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Import Supplier Invoice",
"link_count": 0,
"link_to": "Import Supplier Invoice",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2026-07-14 12:00:00.000000",
"modified_by": "Administrator",
"module": "Buying",
"module_onboarding": "Buying Onboarding",
"name": "Buying",
"number_cards": [
{
"label": "Purchase Orders to Receive",
"number_card_name": "Purchase Orders to Receive"
"label": "Purchase Orders Count",
"number_card_name": "Purchase Orders Count"
},
{
"label": "Purchase Orders to Bill",
"number_card_name": "Purchase Orders to Bill"
"label": "Total Purchase Amount",
"number_card_name": "Total Purchase Amount"
},
{
"label": "Annual Purchase",
"number_card_name": "Annual Purchase"
"label": "Average Order Values",
"number_card_name": "Average Order Values"
}
],
"owner": "Administrator",

View File

@@ -39,7 +39,6 @@ from erpnext.accounts.utils import (
get_advance_payment_doctypes as _get_advance_payment_doctypes,
)
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
from erpnext.controllers.item_close import clear_closed_rows_on_amend
from erpnext.controllers.print_settings import (
set_print_templates_for_item_table,
set_print_templates_for_taxes,
@@ -228,23 +227,7 @@ class AccountsController(TransactionBase):
return False
def is_item_closable(self, item):
"""A row can be closed while anything is still pending on it.
Billing is the axis every closable document shares; the order doctypes
extend this with their own fulfilment axis.
Amounts are compared as magnitudes so that return rows stay closable.
That is deliberate: writing off a credit note that will never be issued
is a real decision, and closing a whole return document is already
allowed. Leaving it to the sign of the amount would decide it by
accident.
"""
return abs(flt(item.billed_amt)) < abs(flt(item.amount))
def validate(self):
clear_closed_rows_on_amend(self)
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()

View File

@@ -1,145 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""Row level close and reopen for transaction items.
`REOPEN_STATUS` holds, per closable parent, the status its own Re-open button
passes to `update_status`. `set_status` recomputes from `status_map` anyway, so
the value is mostly a sentinel for "clear the Closed override" -- but not
always: Sales Order re-checks the credit limit only on the literal "Draft".
Reusing each doctype's own value keeps reopening a row indistinguishable from
reopening the document by hand.
"""
import frappe
from frappe import _
from frappe.utils import cint
REOPEN_STATUS = {
"Purchase Order": "Submitted",
"Sales Order": "Draft",
"Delivery Note": "Submitted",
"Purchase Receipt": "Submitted",
}
SETTLED_BY_CLOSE = ("per_ordered", "per_received", "per_delivered", "per_billed")
def has_closable_items(doctype: str | None) -> bool:
return doctype in REOPEN_STATUS
def closed_rows_settle(parent_doctype: str, item_doctype: str, percentage_field: str) -> bool:
"""Whether closed rows count as fully settled for this progress field.
Returns are excluded: closing a row writes off what is still pending on it,
it does not turn the row into a return.
"""
return (
percentage_field in SETTLED_BY_CLOSE
and has_closable_items(parent_doctype)
and frappe.get_meta(item_doctype).has_field("closed")
)
@frappe.whitelist()
def update_closed_status(doctype: str, name: str, item_names: str | list[str], closed: int) -> None:
if not has_closable_items(doctype):
frappe.throw(_("Rows of {0} cannot be closed individually").format(_(doctype)))
closed = 1 if cint(closed) else 0
item_names = set(frappe.parse_json(item_names) or [])
if not item_names:
frappe.throw(_("Select at least one row"))
doc = frappe.get_lazy_doc(doctype, name, check_permission="submit")
if doc.docstatus != 1:
frappe.throw(_("{0} {1} is not submitted").format(_(doctype), name))
changed = [row for row in doc.items if row.name in item_names and cint(row.closed) != closed]
if not changed:
return
if closed:
settled = [row for row in changed if not doc.is_item_closable(row)]
if settled:
frappe.throw(
_("Row #{0}: {1} is already completed in full, so there is nothing to close").format(
settled[0].idx, frappe.bold(settled[0].item_code)
)
)
validate_rows = getattr(doc, "validate_item_close", None)
if validate_rows:
validate_rows(changed)
for row in changed:
row.db_set("closed", closed)
doc.on_item_close_status_change()
doc.reload()
if closed:
close_parent_if_fully_closed(doc)
else:
reopen_parent_if_closed(doc)
doc.notify_update()
def close_parent_if_fully_closed(doc) -> None:
"""Close the parent once every row has been closed."""
if doc.status == "Closed":
return
if all(cint(row.closed) for row in doc.items):
doc.update_status("Closed")
def reopen_parent_if_closed(doc) -> None:
"""Reopen the parent so the row that was just reopened can be acted on.
A closed parent suppresses its rows everywhere, so leaving it closed would
make reopening a row look like it did nothing.
"""
if doc.status == "Closed":
doc.update_status(REOPEN_STATUS[doc.doctype])
def is_bundle_of_closed_row(packed_item) -> bool:
"""A packed item follows the row of its parent document that bundles it."""
if not packed_item.parent_detail_docname or not packed_item.parenttype:
return False
item_doctype = f"{packed_item.parenttype} Item"
return bool(frappe.db.get_value(item_doctype, packed_item.parent_detail_docname, "closed"))
def clear_closed_rows_on_amend(doc) -> None:
"""An amended document starts with nothing written off.
Frappe copies `no_copy` fields when amending so a cancelled document can be
corrected and resubmitted, which would otherwise carry a write-off decision
that was made against the cancelled document onto the new one.
"""
if not doc.is_new() or not doc.get("amended_from") or not has_closable_items(doc.doctype):
return
for row in doc.get("items") or []:
row.closed = 0
def validate_parent_reopen(doc) -> None:
"""Block reopening a parent whose rows are all closed.
It would read as open while every row stayed suppressed. Reopening the rows
is the way back, and that reopens the parent on its own.
"""
rows = doc.get("items") or []
if rows and all(cint(row.get("closed")) for row in rows):
frappe.throw(
_("Every row of {0} is closed. Reopen the rows you need instead, using {1}.").format(
frappe.bold(doc.name), frappe.bold(_("Reopen Items"))
)
)

View File

@@ -1,25 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.utils import flt
def get_qty_already_mapped(target_doc, ref_field: str, qty_field: str = "qty") -> frappe._dict:
"""Return a map: {source row name: qty} of rows already mapped into the target document.
"Get Items From" passes the in-progress (unsaved) document back as `target_doc`. Its rows
are invisible to the pending-qty queries in the mappers, which only count submitted
documents -- so without this, selecting the same source document twice maps every row
again. Rows are keyed by `ref_field` (dn_detail, so_detail, ...), and a row is present in
the map even when its qty is 0, so mappers without qty tracking can dedupe on presence.
"""
if isinstance(target_doc, str):
target_doc = frappe.parse_json(target_doc)
qty_map = frappe._dict()
for row in (target_doc and target_doc.get("items")) or []:
if ref := row.get(ref_field):
qty_map[ref] = qty_map.get(ref, 0) + flt(row.get(qty_field))
return qty_map

View File

@@ -801,7 +801,7 @@ def get_blanket_orders(doctype: str, txt: str, searchfield: str, start: int, pag
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
query = (
blanket_orders = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo.name)
@@ -814,12 +814,10 @@ def get_blanket_orders(doctype: str, txt: str, searchfield: str, start: int, pag
& (bo.company == filters.get("company"))
& (bo.docstatus == 1)
)
.run()
)
if currency := filters.get("currency"):
query = query.where(bo.currency == currency)
return query.run()
return blanket_orders
@frappe.whitelist()

View File

@@ -8,8 +8,6 @@ from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div
from erpnext.controllers.item_close import closed_rows_settle, has_closable_items
class OverAllowanceError(frappe.ValidationError):
pass
@@ -42,14 +40,6 @@ status_map = {
["Ordered", "is_fully_ordered"],
["Cancelled", "eval:self.docstatus==2"],
],
"Supplier Quotation": [
["Draft", None],
["Submitted", "eval:self.docstatus==1"],
["Stopped", "eval:self.status=='Stopped'"],
["Partially Ordered", "is_partially_ordered"],
["Ordered", "is_fully_ordered"],
["Cancelled", "eval:self.docstatus==2"],
],
"Sales Order": [
["Draft", None],
[
@@ -202,60 +192,9 @@ class StatusUpdater(Document):
self.db_set("status", "Cancelled")
def update_prevdoc_status(self):
self.validate_closed_source_items()
self.update_qty()
self.validate_qty()
def get_closed_source_links(self):
"""Row links that must not point at a closed source row.
`status_updater` covers documents whose progress it already tracks.
Delivery Note and Purchase Receipt are billed through their own services
instead, so their invoices declare the link in `closed_source_links`.
"""
links = [
(args["source_dt"], args["join_field"], args["target_dt"], args["target_parent_dt"])
for args in self.status_updater
if args.get("target_dt")
and args.get("target_parent_dt")
and has_closable_items(args["target_parent_dt"])
]
return links + list(getattr(self, "closed_source_links", []))
def validate_closed_source_items(self):
"""Block submitting against rows that were closed on the source document."""
if self.docstatus != 1:
return
for source_dt, join_field, target_dt, target_parent_dt in self.get_closed_source_links():
if not frappe.get_meta(target_dt).has_field("closed"):
continue
row_idx = {}
for d in self.get_all_children(source_dt):
if d.get(join_field):
row_idx[d.get(join_field)] = d.idx
if not row_idx:
continue
closed_rows = frappe.get_all(
target_dt,
filters={"name": ("in", list(row_idx)), "closed": 1},
fields=["name", "item_code", "parent"],
)
for row in closed_rows:
frappe.throw(
_("Row #{0}: Item {1} is closed in {2} {3} and cannot be processed further").format(
row_idx[row.name],
frappe.bold(row.item_code),
_(target_parent_dt),
frappe.bold(row.parent),
)
)
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get("amended_from"):
@@ -545,7 +484,6 @@ class StatusUpdater(Document):
if args["source_dt"] != "Pick List Item" and args["target_dt"] not in [
"Quotation Item",
"Supplier Quotation Item",
"Packed Item",
]:
if args.get("target_dt") == "Material Request Item":
@@ -667,28 +605,16 @@ class StatusUpdater(Document):
@staticmethod
def _calculate_target_parent_percentage(
name,
target_parent_dt,
target_dt,
target_ref_field,
target_field,
target_parent_field=None,
exclude_field=None,
name, target_parent_dt, target_dt, target_ref_field, target_field, exclude_field=None
):
filters = {"parent": name, "parenttype": target_parent_dt}
if exclude_field:
filters[exclude_field] = 0
tracks_closed_rows = closed_rows_settle(target_parent_dt, target_dt, target_parent_field)
fields = [target_ref_field, target_field]
if tracks_closed_rows:
fields.append("closed")
child_records = frappe.get_all(
target_dt,
filters=filters,
fields=fields,
fields=[target_ref_field, target_field],
)
if exclude_field and not child_records:
@@ -697,19 +623,13 @@ class StatusUpdater(Document):
# For operator dicts, the alias is in the "as" key; for strings, use the field name directly
ref_key = target_ref_field.get("as") if isinstance(target_ref_field, dict) else target_ref_field
# A closed row is written off, so it leaves the denominator rather than
# counting as done. The percentage stays a true measure of what was
# actually received, delivered or billed against what is still expected.
# Once every row is written off there is nothing left to measure against,
# so fall back to the whole table and report what actually happened.
open_records = [r for r in child_records if not (tracks_closed_rows and r["closed"])]
basis = open_records or child_records
sum_ref = sum(abs(record[ref_key]) for record in basis)
sum_ref = sum(abs(record[ref_key]) for record in child_records)
if sum_ref > 0:
percentage = round(
sum(min(abs(record[target_field]), abs(record[ref_key])) for record in basis) / sum_ref * 100,
sum(min(abs(record[target_field]), abs(record[ref_key])) for record in child_records)
/ sum_ref
* 100,
6,
)
else:
@@ -758,7 +678,6 @@ class StatusUpdater(Document):
args["target_dt"],
args["target_ref_field"],
args["target_field"],
args["target_parent_field"],
args.get("exclude_field"),
)
# update field

View File

@@ -337,12 +337,8 @@ class StockController(AccountsController):
items = frappe.get_all(
"Delivery Note Item",
filters={"parent": self.name, "parenttype": "Delivery Note"},
fields=["name", "qty", "returned_qty", "rate", "amount", "billed_amt", "closed"],
fields=["name", "qty", "returned_qty", "rate", "amount", "billed_amt"],
)
# A written off row leaves the basis. Once every row is written off there is
# nothing left to measure against, so fall back to the whole table.
items = [item for item in items if not item.closed] or items
total_amount = sum(flt(item.amount) for item in items)
total_returned = sum(flt(item.returned_qty) * flt(item.rate) for item in items)
# Preserve the original amount basis once the entire Delivery Note is returned.

View File

@@ -151,7 +151,7 @@ class SubcontractingController(StockController):
).format(item.idx, get_link_to_form("Item", item.item_code))
)
if not item.get("secondary_item_type") and not item.get("valuation_type"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
if not is_sub_contracted_item:
frappe.throw(
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
@@ -1248,10 +1248,10 @@ class SubcontractingController(StockController):
total_amt = sum(
flt(item.amount)
for item in self.get("items")
if not item.get("secondary_item_type") and not item.get("valuation_type")
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
)
for item in self.items:
if not item.get("secondary_item_type") and not item.get("valuation_type"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = (
(item.amount * self.total_additional_costs) / total_amt
) / item.qty
@@ -1259,15 +1259,15 @@ class SubcontractingController(StockController):
total_qty = sum(
flt(item.qty)
for item in self.get("items")
if not item.get("secondary_item_type") and not item.get("valuation_type")
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
)
additional_cost_per_qty = self.total_additional_costs / total_qty
for item in self.items:
if not item.get("secondary_item_type") and not item.get("valuation_type"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = additional_cost_per_qty
else:
for item in self.items:
if not item.get("secondary_item_type") and not item.get("valuation_type"):
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
item.additional_cost_per_qty = 0
@frappe.whitelist()

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