mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-31 23:33:43 +00:00
Merge branch 'develop' into party-import-tool-integrated-with-data-import-tool
This commit is contained in:
51
.github/workflows/crowdin-actions-download-translations.yml
vendored
Normal file
51
.github/workflows/crowdin-actions-download-translations.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Download translations from Crowdin
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: crowdin-download
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
download-translations:
|
||||
name: Download translations into ${{ matrix.branch }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: ["develop", "version-16-hotfix"]
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ matrix.branch }}
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ matrix.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download translations and open PR
|
||||
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
download_translations: true
|
||||
crowdin_branch_name: "[frappe.erpnext] ${{ matrix.branch }}"
|
||||
skip_ref_checkout: true
|
||||
localization_branch_name: l10n_crowdin_${{ matrix.branch }}
|
||||
create_pull_request: true
|
||||
pull_request_base_branch_name: ${{ matrix.branch }}
|
||||
commit_message: "fix: sync translations from crowdin"
|
||||
pull_request_title: "fix: sync translations from crowdin (${{ matrix.branch }})"
|
||||
pull_request_labels: "translation, skip-release-notes"
|
||||
pull_request_reviewers: barredterra
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
54
.github/workflows/crowdin-actions-update-main-pot.yml
vendored
Normal file
54
.github/workflows/crowdin-actions-update-main-pot.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Upload main.pot to Crowdin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- version-16-hotfix
|
||||
paths:
|
||||
- "erpnext/locale/main.pot"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: crowdin-upload-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload-sources:
|
||||
name: Upload sources from ${{ github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.ref_name }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Crowdin cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .crowdin
|
||||
key: crowdin-${{ github.ref_name }}
|
||||
restore-keys: crowdin-${{ github.ref_name }}-
|
||||
|
||||
- name: Upload main.pot to Crowdin
|
||||
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
create_pull_request: false
|
||||
crowdin_branch_name: "[frappe.erpnext] ${{ github.ref_name }}"
|
||||
upload_sources_args: "--cache"
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Save Crowdin cache
|
||||
uses: actions/cache/save@v6
|
||||
if: always()
|
||||
with:
|
||||
path: .crowdin
|
||||
key: crowdin-${{ github.ref_name }}-${{ github.run_id }}
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
- '**.po'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
1
.github/workflows/server-tests-mariadb.yml
vendored
1
.github/workflows/server-tests-mariadb.yml
vendored
@@ -13,6 +13,7 @@ on:
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
- '**.po'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
15
crowdin.yml
15
crowdin.yml
@@ -1,16 +1,5 @@
|
||||
preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
- source: /erpnext/locale/main.pot
|
||||
translation: /erpnext/locale/%two_letters_code%.po
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
- skip-release-notes
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-BR: pt_BR
|
||||
zh-CN: zh
|
||||
zh-TW: zh_TW
|
||||
|
||||
@@ -16,6 +16,8 @@ frappe.ui.form.on("Accounting Dimension", {
|
||||
return {
|
||||
filters: {
|
||||
name: ["not in", invalid_doctypes],
|
||||
istable: 0,
|
||||
issingle: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -60,6 +60,14 @@ class AccountingDimension(Document):
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
meta = frappe.get_meta(self.document_type)
|
||||
if meta.istable or meta.issingle:
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} cannot be used as an accounting dimension as it is not a standalone document type."
|
||||
).format(frappe.bold(self.document_type))
|
||||
)
|
||||
|
||||
exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"])
|
||||
|
||||
if exists and self.is_new():
|
||||
|
||||
@@ -51,6 +51,23 @@ class TestAccountingDimension(ERPNextTestSuite):
|
||||
self.assertEqual(gle.get("department"), "_Test Department - _TC")
|
||||
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
|
||||
|
||||
def test_child_table_not_allowed_as_dimension(self):
|
||||
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Sales Team"})
|
||||
self.assertRaises(frappe.ValidationError, dimension.insert)
|
||||
|
||||
def test_single_doctype_not_allowed_as_dimension(self):
|
||||
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Selling Settings"})
|
||||
self.assertRaises(frappe.ValidationError, dimension.insert)
|
||||
|
||||
def test_non_scalar_dimension_value_skipped_in_gl_dict(self):
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
|
||||
si.department = "_Test Department - _TC"
|
||||
self.assertEqual(si.get_gl_dict({}).get("department"), "_Test Department - _TC")
|
||||
|
||||
si.department = ["_Test Department - _TC"]
|
||||
self.assertNotIn("department", si.get_gl_dict({}))
|
||||
|
||||
def test_mandatory(self):
|
||||
location = frappe.get_doc("Accounting Dimension", "Location")
|
||||
location.dimension_defaults[0].mandatory_for_bs = True
|
||||
|
||||
@@ -4,29 +4,18 @@
|
||||
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()
|
||||
|
||||
@@ -139,11 +128,8 @@ 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=supplier.name,
|
||||
supplier="_Test Supplier",
|
||||
supplier_warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
uom="Nos",
|
||||
@@ -158,10 +144,6 @@ 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"}):
|
||||
@@ -170,13 +152,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=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||
si = create_sales_invoice(
|
||||
customer="_Test 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()
|
||||
|
||||
@@ -23,8 +23,6 @@ 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)
|
||||
@@ -32,6 +30,7 @@ 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)
|
||||
|
||||
@@ -47,7 +46,7 @@ class TestBankTransaction(ERPNextTestSuite):
|
||||
from_date=bank_transaction.date,
|
||||
to_date=utils.today(),
|
||||
)
|
||||
self.assertEqual(linked_payments[0]["party"], "Conrad Electronic")
|
||||
self.assertIn("Conrad Electronic", [payment["party"] for payment in linked_payments])
|
||||
|
||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||
def test_reconcile(self):
|
||||
|
||||
@@ -729,6 +729,7 @@ 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"))
|
||||
)
|
||||
|
||||
@@ -234,8 +234,10 @@ frappe.ui.form.on("Dunning", {
|
||||
dn: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
if (!r.exc) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -31,11 +31,4 @@ class TestFinanceBook(ERPNextTestSuite):
|
||||
|
||||
|
||||
def create_finance_book():
|
||||
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
|
||||
return frappe.get_doc("Finance Book", "Test Finance Book 1")
|
||||
|
||||
@@ -163,7 +163,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
label: "Company",
|
||||
label: __("Company"),
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
onchange: () => {
|
||||
@@ -176,7 +176,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
fieldname: "view_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Missing Accounts", "Filtered Accounts"],
|
||||
label: "View",
|
||||
label: __("View"),
|
||||
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
|
||||
reqd: 1,
|
||||
onchange: () => {
|
||||
@@ -192,10 +192,10 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
{
|
||||
fieldname: "tip",
|
||||
fieldtype: "HTML",
|
||||
label: "Tip",
|
||||
label: __("Tip"),
|
||||
options: `
|
||||
<div class="alert alert-success" role="alert">
|
||||
Tip: Select report lines to view their accounts
|
||||
${__("Tip: Select report lines to view their accounts")}
|
||||
</div>
|
||||
`,
|
||||
depends_on: has_selection ? "eval: false" : "eval: true",
|
||||
@@ -203,7 +203,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
{
|
||||
fieldname: "tree_area",
|
||||
fieldtype: "HTML",
|
||||
label: "Chart of Accounts",
|
||||
label: __("Chart of Accounts"),
|
||||
read_only: 1,
|
||||
depends_on: "eval: doc.company",
|
||||
},
|
||||
@@ -288,14 +288,14 @@ function update_formula_label(frm, data_source) {
|
||||
if (!field) return;
|
||||
|
||||
const labels = {
|
||||
"Account Data": "Account Filter",
|
||||
"Custom API": "API Method Path",
|
||||
"Account Data": __("Account Filter"),
|
||||
"Custom API": __("API Method Path"),
|
||||
};
|
||||
|
||||
grid.update_docfield_property(
|
||||
"calculation_formula",
|
||||
"label",
|
||||
labels[data_source] || "Calculation Formula"
|
||||
labels[data_source] || __("Calculation Formula")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,21 @@ from frappe import _
|
||||
from frappe.database.operator_map import OPERATOR_MAP
|
||||
|
||||
|
||||
def get_formula_field_label(data_source: str) -> str:
|
||||
# Must mirror the `labels` map in financial_report_template.js (update_formula_label),
|
||||
labels = {
|
||||
"Account Data": _("Account Filter"),
|
||||
"Custom API": _("API Method Path"),
|
||||
}
|
||||
return labels.get(data_source, _("Calculation Formula"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""Represents a single validation issue"""
|
||||
|
||||
message: str
|
||||
row_idx: int | None = None
|
||||
field: str | None = None
|
||||
details: dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -26,10 +34,9 @@ class ValidationIssue:
|
||||
self.details = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
|
||||
field_info = f"[{self.field}] " if self.field else ""
|
||||
message = f"{prefix}{field_info}{self.message}"
|
||||
return _(message)
|
||||
if self.row_idx:
|
||||
return _("Row {0}: {1}", context="Financial Report Template").format(self.row_idx, self.message)
|
||||
return self.message
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -131,7 +138,9 @@ class TemplateStructureValidator(Validator):
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
|
||||
message=_(
|
||||
"Invalid line reference format: '{0}'. Must start with letter and contain only letters, numbers, underscores, and hyphens"
|
||||
).format(ref_code),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -140,7 +149,7 @@ class TemplateStructureValidator(Validator):
|
||||
if ref_code in used_codes:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Duplicate line reference: '{ref_code}'",
|
||||
message=_("Duplicate line reference: '{0}'").format(ref_code),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -156,7 +165,7 @@ class TemplateStructureValidator(Validator):
|
||||
if row.data_source == "Account Data" and not row.balance_type:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Balance Type is required for Account Data",
|
||||
message=_("Balance Type is required for Account Data"),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -166,7 +175,9 @@ class TemplateStructureValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula is required for {row.data_source}",
|
||||
message=_("{0} is required for {1}").format(
|
||||
get_formula_field_label(row.data_source), row.data_source
|
||||
),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -223,7 +234,7 @@ class DependencyValidator(Validator):
|
||||
cycle = [*path[cycle_start:], node]
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Circular dependency detected: {' → '.join(cycle)}",
|
||||
message=_("Circular dependency detected: {0}").format(" → ".join(cycle)),
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -255,7 +266,7 @@ class DependencyValidator(Validator):
|
||||
row_idx = self._get_row_idx(ref_code)
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Line References undefined in Formula: {', '.join(undefined)}",
|
||||
message=_("Line References undefined in Formula: {0}").format(", ".join(undefined)),
|
||||
row_idx=row_idx,
|
||||
)
|
||||
)
|
||||
@@ -285,9 +296,10 @@ class CalculationFormulaValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Formula is required for Calculated Amount",
|
||||
message=_("{0} is required for Calculated Amount").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -299,7 +311,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if not self._are_parentheses_balanced(formula):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Formula has unbalanced parentheses",
|
||||
message=_("Formula has unbalanced parentheses"),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -311,7 +323,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if row.reference_code and row.reference_code in refs:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula references itself ('{row.reference_code}')",
|
||||
message=_("Formula references itself ('{0}')").format(row.reference_code),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -321,7 +333,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if undefined:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula references undefined codes: {', '.join(undefined)}",
|
||||
message=_("Formula references undefined codes: {0}").format(", ".join(undefined)),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -331,7 +343,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if eval_error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Formula evaluation error: {eval_error}",
|
||||
message=_("Formula evaluation error: {0}").format(eval_error),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -368,7 +380,7 @@ class CalculationFormulaValidator(Validator):
|
||||
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
|
||||
|
||||
if not isinstance(result, (int, float)): # noqa: UP038
|
||||
return f"Formula must return a numeric value, got {type(result).__name__}"
|
||||
return _("Formula must return a numeric value, got {0}").format(type(result).__name__)
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
@@ -391,9 +403,10 @@ class AccountFilterValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Account filter is required for Account Data",
|
||||
message=_("{0} is required for Account Data").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -409,18 +422,18 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=error,
|
||||
message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error),
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Invalid JSON format: {e!s}",
|
||||
message=_("{0}: Invalid JSON format: {1}").format(
|
||||
get_formula_field_label(row.data_source), str(e)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -435,38 +448,38 @@ class AccountFilterValidator(Validator):
|
||||
# simple condition: [field, operator, value]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
return "Filter must be [field, operator, value]"
|
||||
return _("Filter must be [field, operator, value]")
|
||||
|
||||
field, operator, value = filter_config
|
||||
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
return "Field and operator must be strings"
|
||||
return _("Field and operator must be strings")
|
||||
|
||||
display = (
|
||||
field if advanced_filtering else self.account_meta.get_translated_label(field)
|
||||
) or field
|
||||
|
||||
if field not in account_fields:
|
||||
return f"Field '{display}' is not a valid Account field"
|
||||
return _("Field '{0}' is not a valid Account field").format(display)
|
||||
|
||||
if operator.casefold() not in OPERATOR_MAP:
|
||||
return f"Invalid operator '{operator}'"
|
||||
return _("Invalid operator '{0}'").format(operator)
|
||||
|
||||
if operator in ["in", "not in"] and not isinstance(value, list):
|
||||
return f"Operator '{operator}' requires a list value"
|
||||
return _("Operator '{0}' requires a list value").format(operator)
|
||||
|
||||
# logical condition: {"and": [condition1, condition2]}
|
||||
elif isinstance(filter_config, dict):
|
||||
if len(filter_config) != 1:
|
||||
return "Logical condition must have exactly one operator"
|
||||
return _("Logical condition must have exactly one operator")
|
||||
|
||||
op = next(iter(filter_config.keys())).lower()
|
||||
if op not in ["and", "or"]:
|
||||
return "Logical operators must be 'and' or 'or'"
|
||||
return _("Logical operators must be 'and' or 'or'")
|
||||
|
||||
conditions = filter_config[next(iter(filter_config.keys()))]
|
||||
if not isinstance(conditions, list) or len(conditions) < 1:
|
||||
return "Logical conditions need at least 1 sub-condition"
|
||||
return _("Logical conditions need at least 1 sub-condition")
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
@@ -474,7 +487,7 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
return "Filter must be a list or dict"
|
||||
return _("Filter must be a list or dict")
|
||||
|
||||
return None
|
||||
|
||||
@@ -510,9 +523,10 @@ class FormulaValidator(Validator):
|
||||
if "." not in api_path:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message="Custom API path should be in format: app.module.method",
|
||||
message=_("{0} should be in format: app.module.method").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -525,17 +539,19 @@ class FormulaValidator(Validator):
|
||||
if not hasattr(module, method_name):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
|
||||
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,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Could not validate API path: {e!s}",
|
||||
message=_("Could not validate {0}: {1}").format(
|
||||
get_formula_field_label(row.data_source), str(e)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ 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))
|
||||
@@ -78,6 +79,8 @@ 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
|
||||
@@ -118,6 +121,8 @@ 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")})
|
||||
|
||||
|
||||
@@ -318,9 +318,8 @@ class TestJournalEntry(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
# the guard must not disclose the reversal to a user who cannot read the entry
|
||||
frappe.set_user("Guest")
|
||||
self.addCleanup(frappe.set_user, "Administrator")
|
||||
self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name)
|
||||
with self.set_user("Guest"):
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
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"))
|
||||
|
||||
@unittest.mock.patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
|
||||
@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(
|
||||
|
||||
@@ -297,6 +297,9 @@ 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:
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
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
|
||||
|
||||
@@ -126,6 +128,55 @@ 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
|
||||
|
||||
@@ -1279,8 +1279,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
await frappe.after_ajax();
|
||||
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||
let other_deductions = 0;
|
||||
if (frm.doc.payment_type === "Internal Transfer") {
|
||||
other_deductions = (frm.doc.deductions || [])
|
||||
.filter((row) => !row.is_exchange_gain_loss)
|
||||
.reduce((sum, row) => sum + flt(row.amount), 0);
|
||||
}
|
||||
const exchange_gain_loss = flt(
|
||||
base_paid_amount - base_received_amount,
|
||||
base_paid_amount - base_received_amount - other_deductions,
|
||||
get_deduction_amount_precision()
|
||||
);
|
||||
|
||||
@@ -1857,11 +1863,19 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
||||
},
|
||||
|
||||
amount: function (frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
if (frm.doc.payment_type === "Internal Transfer") {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
} else {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
|
||||
deductions_remove: function (frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
if (frm.doc.payment_type === "Internal Transfer") {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
} else {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1118,8 +1118,14 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
other_deductions = 0
|
||||
if self.payment_type == "Internal Transfer":
|
||||
other_deductions = sum(
|
||||
flt(row.amount) for row in self.get("deductions") if not row.is_exchange_gain_loss
|
||||
)
|
||||
|
||||
exchange_gain_loss = flt(
|
||||
self.base_paid_amount - self.base_received_amount,
|
||||
self.base_paid_amount - self.base_received_amount - other_deductions,
|
||||
self.precision("amount", "deductions"),
|
||||
)
|
||||
|
||||
@@ -2620,7 +2626,11 @@ 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)))
|
||||
|
||||
@@ -789,7 +789,6 @@ 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"
|
||||
@@ -834,7 +833,6 @@ 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"
|
||||
@@ -870,6 +868,64 @@ class TestPaymentEntry(ERPNextTestSuite):
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
def test_cross_currency_transfer_splits_bank_charge_and_exchange_gain_loss(self):
|
||||
exchange_gain_loss_account = frappe.db.get_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account"
|
||||
)
|
||||
bank_charges_account = create_account(
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
account_name="_Test Bank Charges",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = "Internal Transfer"
|
||||
pe.company = "_Test Company"
|
||||
pe.paid_from = "_Test Bank USD - _TC"
|
||||
pe.paid_to = "_Test Bank - _TC"
|
||||
pe.paid_amount = 100
|
||||
pe.source_exchange_rate = 50
|
||||
pe.received_amount = 4500
|
||||
pe.reference_no = "6"
|
||||
pe.reference_date = nowdate()
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": bank_charges_account,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
deductions = {d.account: d for d in pe.deductions}
|
||||
self.assertEqual(deductions[bank_charges_account].amount, 100)
|
||||
self.assertEqual(deductions[exchange_gain_loss_account].amount, 400)
|
||||
self.assertTrue(deductions[exchange_gain_loss_account].is_exchange_gain_loss)
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
for d in pe.deductions:
|
||||
d.cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
["_Test Bank USD - _TC", 0, 5000, None],
|
||||
["_Test Bank - _TC", 4500, 0, None],
|
||||
[exchange_gain_loss_account, 400.0, 0, None],
|
||||
[bank_charges_account, 100.0, 0, None],
|
||||
]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
def test_payment_against_negative_sales_invoice(self):
|
||||
si1 = create_sales_invoice()
|
||||
|
||||
@@ -1051,8 +1107,6 @@ 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",
|
||||
|
||||
@@ -201,8 +201,6 @@ 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):
|
||||
@@ -1331,15 +1329,6 @@ 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
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
|
||||
freeze: true,
|
||||
callback: function (data) {
|
||||
if (!data.exc) {
|
||||
frm.clear_table("subscription_plans");
|
||||
$.each(data.message || [], function (i, v) {
|
||||
var d = frappe.model.add_child(
|
||||
frm.doc,
|
||||
|
||||
@@ -875,6 +875,7 @@ def make_payment_request(**args):
|
||||
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
subscription_plans = get_subscription_details(ref_doc.doctype, ref_doc.name)
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -906,12 +907,24 @@ def make_payment_request(**args):
|
||||
or gateway_account.get("payment_channel", "Email") != "Email"
|
||||
),
|
||||
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
|
||||
"is_a_subscription": 1 if subscription_plans else 0,
|
||||
}
|
||||
)
|
||||
|
||||
if selected_payment_schedules:
|
||||
apply_payment_references(pr, payment_reference)
|
||||
|
||||
if subscription_plans:
|
||||
pr.set(
|
||||
"subscription_plans",
|
||||
[
|
||||
{
|
||||
"plan": row.plan,
|
||||
"qty": row.qty,
|
||||
}
|
||||
for row in subscription_plans
|
||||
],
|
||||
)
|
||||
# Dimensions
|
||||
pr.update(
|
||||
{
|
||||
@@ -1225,20 +1238,25 @@ def get_dummy_message(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_subscription_details(reference_doctype: str, reference_name: str):
|
||||
if reference_doctype == "Sales Invoice":
|
||||
subscriptions = frappe.get_all(
|
||||
"Subscription Invoice",
|
||||
filters={"invoice": reference_name},
|
||||
fields=["parent as sub_name"],
|
||||
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
|
||||
)
|
||||
subscription_plans = []
|
||||
for subscription in subscriptions:
|
||||
plans = frappe.get_doc("Subscription", subscription.sub_name).plans
|
||||
for plan in plans:
|
||||
subscription_plans.append(plan)
|
||||
return subscription_plans
|
||||
def get_subscription_details(reference_doctype: str, reference_name: str) -> list[dict]:
|
||||
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
|
||||
|
||||
if not frappe.get_meta(reference_doctype).has_field("subscription"):
|
||||
return []
|
||||
|
||||
subscription = frappe.db.get_value(reference_doctype, reference_name, "subscription")
|
||||
|
||||
if not subscription:
|
||||
return []
|
||||
|
||||
return frappe.get_all(
|
||||
"Subscription Plan Detail",
|
||||
filters={"parent": subscription, "parenttype": "Subscription", "parentfield": "plans"},
|
||||
fields=[
|
||||
"plan",
|
||||
"qty",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1341,6 +1359,7 @@ 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 []
|
||||
|
||||
@@ -11,15 +11,27 @@ from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
get_subscription_details,
|
||||
make_payment_request,
|
||||
)
|
||||
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.accounts.doctype.subscription.test_subscription import (
|
||||
create_plan,
|
||||
create_subscription,
|
||||
make_plans,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
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"},
|
||||
@@ -62,6 +74,18 @@ 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:
|
||||
@@ -80,24 +104,11 @@ class TestPaymentRequest(ERPNextTestSuite):
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
def test_payment_request_linkings(self):
|
||||
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||
@@ -2009,3 +2020,140 @@ class TestPaymentRequestV2Gateway(ERPNextTestSuite):
|
||||
call_kwargs = mock_log_error.call_args
|
||||
self.assertIn("Payment Initialization Failed", str(call_kwargs))
|
||||
self.assertIn("_Test Gateway", str(call_kwargs))
|
||||
|
||||
def test_payment_request_with_subscription(self):
|
||||
make_plans()
|
||||
|
||||
subscription_plan = frappe.get_doc("Subscription Plan", "_Test Plan Name")
|
||||
subscription_plan.payment_gateway = "_Test Gateway - INR - _TC"
|
||||
subscription_plan.save()
|
||||
|
||||
subscription = create_subscription(
|
||||
plans=[{"plan": "_Test Plan Name", "qty": 1}],
|
||||
start_date=nowdate(),
|
||||
generate_invoice_at="Prepaid (bill at period start)",
|
||||
submit_invoice=1,
|
||||
)
|
||||
invoice_name = frappe.get_value(
|
||||
"Sales Invoice",
|
||||
{
|
||||
"subscription": subscription.name,
|
||||
"docstatus": 1,
|
||||
"is_return": 0,
|
||||
},
|
||||
"name",
|
||||
order_by="from_date asc",
|
||||
)
|
||||
|
||||
payment_request = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=invoice_name,
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
|
||||
self.assertEqual(payment_request.is_a_subscription, 1)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 1)
|
||||
|
||||
subscription_plan = payment_request.subscription_plans[0]
|
||||
self.assertEqual(subscription_plan.plan, "_Test Plan Name")
|
||||
self.assertEqual(subscription_plan.qty, 1)
|
||||
self.assertEqual(payment_request.reference_doctype, "Sales Invoice")
|
||||
self.assertEqual(payment_request.reference_name, invoice_name)
|
||||
|
||||
def test_payment_request_without_subscription(self):
|
||||
si = create_sales_invoice()
|
||||
payment_request = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=si.name,
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
self.assertEqual(payment_request.is_a_subscription, 0)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 0)
|
||||
self.assertEqual(payment_request.reference_doctype, "Sales Invoice")
|
||||
self.assertEqual(payment_request.reference_name, si.name)
|
||||
|
||||
def test_payment_request_with_subscription_for_purchase_invoice(self):
|
||||
make_plans()
|
||||
|
||||
subscription_plan = frappe.get_doc("Subscription Plan", "_Test Plan Name")
|
||||
subscription_plan.payment_gateway = "_Test Gateway - INR - _TC"
|
||||
subscription_plan.save()
|
||||
|
||||
subscription = create_subscription(
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
plans=[{"plan": "_Test Plan Name", "qty": 1}],
|
||||
start_date=nowdate(),
|
||||
generate_invoice_at="Prepaid (bill at period start)",
|
||||
submit_invoice=1,
|
||||
)
|
||||
invoice_name = frappe.get_value(
|
||||
"Purchase Invoice",
|
||||
{
|
||||
"subscription": subscription.name,
|
||||
"docstatus": 1,
|
||||
"is_return": 0,
|
||||
},
|
||||
"name",
|
||||
order_by="from_date asc",
|
||||
)
|
||||
|
||||
payment_request = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=invoice_name,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
|
||||
self.assertEqual(payment_request.is_a_subscription, 1)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 1)
|
||||
|
||||
subscription_plan = payment_request.subscription_plans[0]
|
||||
self.assertEqual(subscription_plan.plan, "_Test Plan Name")
|
||||
self.assertEqual(subscription_plan.qty, 1)
|
||||
self.assertEqual(payment_request.reference_doctype, "Purchase Invoice")
|
||||
self.assertEqual(payment_request.reference_name, invoice_name)
|
||||
|
||||
def test_payment_request_without_subscription_for_purchase_invoice(self):
|
||||
pi = make_purchase_invoice()
|
||||
payment_request = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
party_type="Supplier",
|
||||
party=pi.supplier,
|
||||
recipient_id="test@example.com",
|
||||
)
|
||||
self.assertEqual(payment_request.is_a_subscription, 0)
|
||||
self.assertEqual(len(payment_request.subscription_plans), 0)
|
||||
self.assertEqual(payment_request.reference_doctype, "Purchase Invoice")
|
||||
self.assertEqual(payment_request.reference_name, pi.name)
|
||||
|
||||
def test_get_subscription_details_returns_empty_for_doctype_without_subscription_field(self):
|
||||
so = make_sales_order()
|
||||
self.assertEqual(get_subscription_details("Sales Order", so.name), [])
|
||||
|
||||
def test_get_subscription_details_requires_read_permission_on_reference(self):
|
||||
si = create_sales_invoice()
|
||||
|
||||
restricted_user = "no-roles@example.com"
|
||||
if not frappe.db.exists("User", restricted_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = restricted_user
|
||||
user.first_name = "No Roles"
|
||||
user.send_welcome_email = 0
|
||||
user.insert()
|
||||
|
||||
accounts_user = "accounts-user@example.com"
|
||||
if not frappe.db.exists("User", accounts_user):
|
||||
user = frappe.new_doc("User")
|
||||
user.email = accounts_user
|
||||
user.first_name = "Accounts"
|
||||
user.send_welcome_email = 0
|
||||
user.add_roles("Accounts User")
|
||||
|
||||
with self.set_user(restricted_user):
|
||||
self.assertRaises(frappe.PermissionError, get_subscription_details, "Sales Invoice", si.name)
|
||||
|
||||
with self.set_user(accounts_user):
|
||||
self.assertEqual(get_subscription_details("Sales Invoice", si.name), [])
|
||||
|
||||
@@ -263,12 +263,15 @@ 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
|
||||
|
||||
|
||||
@@ -21,13 +21,12 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
init_user_and_profile()
|
||||
self.test_user, self.pos_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):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.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})
|
||||
@@ -59,8 +58,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
Test if POS Closing Entry is created without item code
|
||||
"""
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.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})
|
||||
@@ -79,10 +77,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
test_item_qty = get_test_item_qty(pos_profile)
|
||||
test_item_qty = get_test_item_qty(self.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})
|
||||
@@ -104,13 +101,11 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
pcv_doc.flags.in_test = True
|
||||
pcv_doc.submit()
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
test_item_qty_after_sales = get_test_item_qty(pos_profile)
|
||||
test_item_qty_after_sales = get_test_item_qty(self.pos_profile)
|
||||
self.assertEqual(test_item_qty_after_sales, test_item_qty - 1)
|
||||
|
||||
def test_cancelling_of_pos_closing_entry(self):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.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})
|
||||
@@ -169,9 +164,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
pos_profile.insert()
|
||||
self.assertTrue(frappe.db.exists("POS Profile", pos_profile.name))
|
||||
|
||||
test_user = init_user_and_profile(do_not_create_pos_profile=1)
|
||||
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(pos_profile, self.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()
|
||||
@@ -195,9 +188,6 @@ 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(
|
||||
@@ -220,8 +210,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
)
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
item_code=item_code,
|
||||
@@ -291,18 +280,17 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
|
||||
@ERPNextTestSuite.change_settings("POS Settings", {"invoice_type": "Sales Invoice"})
|
||||
def test_closing_entries_with_sales_invoice(self):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
pos_si = create_sales_invoice(
|
||||
qty=10, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
|
||||
qty=10, is_created_using_pos=1, pos_profile=self.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=pos_profile.name, do_not_save=11
|
||||
qty=5, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=11
|
||||
)
|
||||
pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
pos_si2.save()
|
||||
@@ -332,12 +320,10 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
|
||||
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(self.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})
|
||||
@@ -357,13 +343,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(pos_profile, test_user.name)
|
||||
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.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=pos_profile.name, do_not_save=1
|
||||
qty=1, is_created_using_pos=1, pos_profile=self.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)
|
||||
@@ -394,16 +380,14 @@ class TestPOSClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
|
||||
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
|
||||
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
|
||||
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.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=pos_profile.name, do_not_save=1
|
||||
qty=1, is_created_using_pos=1, pos_profile=self.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)
|
||||
@@ -421,9 +405,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(pos_profile, test_user.name)
|
||||
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
|
||||
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
|
||||
pos_si1, pos_si2 = create_multiple_sales_invoices(self.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})
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,
|
||||
@@ -53,14 +54,14 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
w2 = frappe.get_doc(w.doctype, w.name)
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
w.save()
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
frappe.db.set_value(
|
||||
w.doctype,
|
||||
w.name,
|
||||
"modified",
|
||||
add_to_date(w.modified, seconds=1),
|
||||
update_modified=False,
|
||||
)
|
||||
self.assertRaises(frappe.TimestampMismatchError, w2.save)
|
||||
|
||||
def test_change_naming_series(self):
|
||||
@@ -902,9 +903,6 @@ 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
|
||||
|
||||
@@ -916,8 +914,6 @@ 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],
|
||||
@@ -931,13 +927,9 @@ 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
|
||||
|
||||
init_user_and_profile()
|
||||
from erpnext.stock.utils import get_stock_balance
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -959,9 +951,18 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
# 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)
|
||||
# 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,
|
||||
)
|
||||
|
||||
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
|
||||
bundle_item = "_Test Bundle"
|
||||
@@ -1010,16 +1011,19 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
pos_profile = None
|
||||
if not args.pos_profile:
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile.save()
|
||||
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_inv = frappe.new_doc("POS Invoice")
|
||||
pos_inv.update(args)
|
||||
pos_inv.update_stock = 1
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||
pos_inv.pos_profile = pos_profile_name
|
||||
|
||||
if args.posting_date:
|
||||
pos_inv.set_posting_time = 1
|
||||
|
||||
@@ -26,14 +26,10 @@ 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()
|
||||
@@ -55,14 +51,10 @@ 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(
|
||||
@@ -107,9 +99,6 @@ 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,
|
||||
)
|
||||
@@ -121,7 +110,6 @@ 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(
|
||||
|
||||
@@ -289,6 +289,11 @@ 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")
|
||||
|
||||
@@ -298,6 +303,7 @@ 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()
|
||||
@@ -314,6 +320,7 @@ 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()
|
||||
)
|
||||
|
||||
@@ -235,6 +235,9 @@ 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()
|
||||
|
||||
@@ -130,6 +130,9 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
get_purchase_document_details,
|
||||
)
|
||||
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
|
||||
get_custom_dimension_overrides,
|
||||
)
|
||||
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
@@ -270,25 +273,25 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
|
||||
# Amount added through landed-cost-voucher
|
||||
if landed_cost_entries:
|
||||
if (item.item_code, item.name) in landed_cost_entries:
|
||||
for account, base_amount in landed_cost_entries[
|
||||
(item.item_code, item.name)
|
||||
].items():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(base_amount["base_amount"]),
|
||||
"credit_in_account_currency": flt(base_amount["amount"]),
|
||||
"credit_in_transaction_currency": item.net_amount,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
for entry in landed_cost_entries.get((item.item_code, item.name), []):
|
||||
if not (entry.amount or entry.base_amount):
|
||||
continue
|
||||
|
||||
gl_dict = self.get_gl_dict(
|
||||
{
|
||||
"account": entry.expense_account,
|
||||
"against": item.expense_account,
|
||||
"cost_center": entry.dimensions.cost_center or item.cost_center,
|
||||
"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": item.net_amount,
|
||||
"project": entry.dimensions.project or item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
gl_dict.update(get_custom_dimension_overrides(entry))
|
||||
gl_entries.append(gl_dict)
|
||||
|
||||
# sub-contracting warehouse
|
||||
if flt(item.rm_supp_cost):
|
||||
|
||||
@@ -578,17 +578,7 @@ 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",
|
||||
@@ -616,16 +606,7 @@ 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"
|
||||
@@ -3545,7 +3526,6 @@ 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",
|
||||
|
||||
@@ -278,6 +278,9 @@ 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"""
|
||||
@@ -614,6 +617,7 @@ class SalesInvoice(SellingController):
|
||||
"percent_join_field": "sales_order",
|
||||
"status_field": "delivery_status",
|
||||
"keyword": "Delivered",
|
||||
"exclude_field": "skip_delivery",
|
||||
"second_source_dt": "Delivery Note Item",
|
||||
"second_source_field": "qty",
|
||||
"second_join_field": "so_detail",
|
||||
|
||||
@@ -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, cint, flt, format_date, getdate, nowdate, today
|
||||
from frappe.utils import add_days, add_to_date, 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()
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
frappe.db.set_value(
|
||||
w.doctype,
|
||||
w.name,
|
||||
"modified",
|
||||
add_to_date(w.modified, seconds=1),
|
||||
update_modified=False,
|
||||
)
|
||||
self.assertRaises(frappe.TimestampMismatchError, w2.save)
|
||||
|
||||
def test_sales_invoice_change_naming_series(self):
|
||||
@@ -3817,25 +3817,12 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# 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()
|
||||
# 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 sales invoice
|
||||
si = create_sales_invoice(
|
||||
@@ -5789,12 +5776,6 @@ 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",
|
||||
@@ -5815,12 +5796,6 @@ 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",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils.data import (
|
||||
@@ -658,23 +659,15 @@ 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")
|
||||
|
||||
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:
|
||||
# Stub transaction recovery so the test can observe the complete iteration in isolation.
|
||||
with patch.object(Subscription, "process", patched), patch.object(frappe.db, "rollback"):
|
||||
process_all([sub1.name, sub2.name])
|
||||
finally:
|
||||
Subscription.process = original_process
|
||||
frappe.db.rollback = original_rollback
|
||||
|
||||
self.assertEqual(processed, [sub1.name, sub2.name])
|
||||
|
||||
@@ -1073,12 +1066,6 @@ 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"
|
||||
|
||||
@@ -63,25 +63,6 @@ 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",
|
||||
|
||||
@@ -869,9 +869,7 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(rows_b[0].future_amount, 50.0)
|
||||
|
||||
def test_sales_person(self):
|
||||
sales_person = frappe.get_doc(
|
||||
{"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}
|
||||
).insert()
|
||||
sales_person = frappe.get_doc("Sales Person", "_Test Sales Person")
|
||||
si = self.create_sales_invoice(do_not_submit=True)
|
||||
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
|
||||
si.save().submit()
|
||||
@@ -1494,17 +1492,8 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
|
||||
def test_receivable_filtered_by_sales_partner(self):
|
||||
frappe.set_user("Administrator")
|
||||
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()
|
||||
partner_a = "_Test Sales Partner India - 1"
|
||||
partner_b = "_Test Sales Partner India - 2"
|
||||
|
||||
def _si(sales_partner):
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True, qty=2)
|
||||
|
||||
@@ -193,16 +193,7 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertEqual(len(rpt_output), 0)
|
||||
|
||||
def test_03_summary_sales_partner_column(self):
|
||||
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()
|
||||
partner = "_Test Sales Partner India - 1"
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
|
||||
@@ -21,10 +21,8 @@ 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)
|
||||
|
||||
si = create_sales_invoice(company=self.company)
|
||||
self.addCleanup(self._cancel_and_delete, "Sales Invoice", si.name)
|
||||
create_sales_invoice(company=self.company)
|
||||
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
@@ -42,15 +40,6 @@ 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",
|
||||
|
||||
@@ -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 = make_sales_person("_Test Sales Person")
|
||||
sales_person = frappe.get_doc("Sales Person", "_Test Sales Person")
|
||||
|
||||
posting_date = get_first_day(nowdate())
|
||||
qty = 10
|
||||
@@ -1194,19 +1194,3 @@ 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
|
||||
|
||||
@@ -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 = create_shareholder("_Test Share Balance Holder", COMPANY)
|
||||
self.shareholder = get_shareholder("Iron Man", 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 = create_shareholder("_Test Share Balance Holder 2", COMPANY)
|
||||
other_holder = get_shareholder("Thor", COMPANY)
|
||||
create_share_transfer(
|
||||
transfer_type="Issue",
|
||||
to_shareholder=self.shareholder,
|
||||
@@ -187,9 +187,8 @@ def create_share_type(title):
|
||||
return title
|
||||
|
||||
|
||||
def create_shareholder(title, company):
|
||||
shareholder = frappe.get_doc({"doctype": "Shareholder", "title": title, "company": company}).insert()
|
||||
return shareholder.name
|
||||
def get_shareholder(title, company):
|
||||
return frappe.db.get_value("Shareholder", {"title": title, "company": company}, "name")
|
||||
|
||||
|
||||
def create_share_transfer(**kwargs):
|
||||
|
||||
@@ -23,7 +23,7 @@ COL_SHARE_TRANSFER = 8
|
||||
|
||||
class TestShareLedger(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.shareholder = self.create_shareholder("_Test Share Ledger Holder")
|
||||
self.shareholder = self.get_shareholder("Iron Man")
|
||||
# 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.create_shareholder("_Test Share Ledger Buyer")
|
||||
buyer = self.get_shareholder("Thor")
|
||||
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.create_shareholder("_Test Share Ledger Seller")
|
||||
seller = self.get_shareholder("Hulk")
|
||||
# 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,15 +119,8 @@ class TestShareLedger(ERPNextTestSuite):
|
||||
self.assertIsNotNone(row, f"Share Transfer {transfer_name} missing from ledger")
|
||||
return row
|
||||
|
||||
def create_shareholder(self, title):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Shareholder",
|
||||
"title": title,
|
||||
"company": COMPANY,
|
||||
}
|
||||
).insert()
|
||||
return doc.name
|
||||
def get_shareholder(self, title):
|
||||
return frappe.db.get_value("Shareholder", {"title": title, "company": COMPANY}, "name")
|
||||
|
||||
def issue_shares(self, date, from_no, to_no, rate, shareholder=None):
|
||||
doc = frappe.get_doc(
|
||||
|
||||
@@ -67,9 +67,12 @@ def get_gl_dict(doc, args: dict, account_currency: str | None = None, item=None)
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
dimension_dict = frappe._dict()
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_dict[dimension] = doc.get(dimension)
|
||||
value = doc.get(dimension)
|
||||
if item and item.get(dimension):
|
||||
dimension_dict[dimension] = item.get(dimension)
|
||||
value = item.get(dimension)
|
||||
if isinstance(value, list | dict):
|
||||
continue
|
||||
dimension_dict[dimension] = value
|
||||
|
||||
gl_dict.update(dimension_dict)
|
||||
gl_dict.update(args)
|
||||
@@ -139,8 +142,13 @@ def add_gl_entry(
|
||||
voucher_detail_no: str | None = None,
|
||||
item=None,
|
||||
posting_date=None,
|
||||
dimensions: dict | None = None,
|
||||
) -> None:
|
||||
"""Build a GL entry via get_gl_dict and append it to gl_entries."""
|
||||
"""Build a GL entry via get_gl_dict and append it to gl_entries.
|
||||
|
||||
`dimensions` sets accounting dimensions explicitly, overriding the values `get_gl_dict`
|
||||
would otherwise derive from `item` and the parent document.
|
||||
"""
|
||||
gl_entry = {
|
||||
"account": account,
|
||||
"cost_center": cost_center,
|
||||
@@ -165,6 +173,9 @@ def add_gl_entry(
|
||||
if posting_date:
|
||||
gl_entry["posting_date"] = posting_date
|
||||
|
||||
if dimensions:
|
||||
gl_entry.update(dimensions)
|
||||
|
||||
gl_entries.append(get_gl_dict(doc, gl_entry, account_currency, item=item))
|
||||
|
||||
|
||||
@@ -252,6 +263,7 @@ class BaseGLComposer:
|
||||
voucher_detail_no: str | None = None,
|
||||
item=None,
|
||||
posting_date=None,
|
||||
dimensions: dict | None = None,
|
||||
) -> None:
|
||||
add_gl_entry(
|
||||
self.doc,
|
||||
@@ -269,4 +281,5 @@ class BaseGLComposer:
|
||||
voucher_detail_no,
|
||||
item,
|
||||
posting_date,
|
||||
dimensions,
|
||||
)
|
||||
|
||||
@@ -32,8 +32,7 @@ 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._ordered_items: dict | None = None
|
||||
self._purchased_items: dict | None = None
|
||||
self._transacted_stock_qty: dict | None = None
|
||||
|
||||
def update(self, trans_items: str | list) -> None:
|
||||
"""Process item additions, edits, and deletions from trans_items JSON."""
|
||||
@@ -48,11 +47,15 @@ class ChildItemUpdater:
|
||||
self._check_permissions("write")
|
||||
|
||||
if self.parent_doctype == "Quotation":
|
||||
self._ordered_items = get_ordered_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items)
|
||||
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
|
||||
)
|
||||
elif self.parent_doctype == "Supplier Quotation":
|
||||
self._purchased_items = get_purchased_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items)
|
||||
self._transacted_stock_qty = get_purchased_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(
|
||||
self.parent, data, self._transacted_stock_qty
|
||||
)
|
||||
else:
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data)
|
||||
|
||||
@@ -71,6 +74,7 @@ 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
|
||||
@@ -78,6 +82,13 @@ 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")):
|
||||
@@ -144,6 +155,7 @@ class ChildItemUpdater:
|
||||
if parent.is_against_so():
|
||||
parent.update_status_updater()
|
||||
elif self.parent_doctype == "Sales Order":
|
||||
parent.set_skip_delivery()
|
||||
parent.check_credit_limit()
|
||||
|
||||
for idx, row in enumerate(parent.get(self.child_docname), start=1):
|
||||
@@ -250,6 +262,22 @@ 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(
|
||||
@@ -263,24 +291,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]
|
||||
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
|
||||
old_stock_qty = flt(child_item.get(qty_field)) * old_conversion_factor
|
||||
if new_stock_qty < old_stock_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}:").format(new_data.get("idx")) + error_message,
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if self.parent_doctype not in ("Quotation", "Supplier Quotation"):
|
||||
if not self._transacted_stock_qty:
|
||||
return
|
||||
|
||||
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:
|
||||
old_stock_qty = self._transacted_stock_qty.get(child_item.name)
|
||||
if not old_stock_qty:
|
||||
return
|
||||
|
||||
if not rate_unchanged:
|
||||
@@ -290,7 +318,7 @@ class ChildItemUpdater:
|
||||
).format(frappe.bold(new_data.get("item_code")))
|
||||
)
|
||||
|
||||
if flt(new_data.get("qty")) < qty_to_check:
|
||||
if new_stock_qty < old_stock_qty:
|
||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||
|
||||
def _validate_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None:
|
||||
@@ -457,7 +485,11 @@ 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]
|
||||
deleted_children = [item for item in parent.items if item.name not in updated_item_names]
|
||||
# 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")
|
||||
]
|
||||
|
||||
for d in deleted_children:
|
||||
validate_child_on_delete(d, parent, ordered_item)
|
||||
@@ -580,22 +612,18 @@ 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"), conv_fac_precision)
|
||||
child_item.conversion_factor = flt(new_data.get("conversion_factor"))
|
||||
|
||||
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"), conv_fac_precision) or conversion_factor
|
||||
)
|
||||
child_item.conversion_factor = flt(new_data.get("conversion_factor")) or conversion_factor
|
||||
|
||||
if child_item.get("weight_per_unit"):
|
||||
child_item.total_weight = flt(
|
||||
|
||||
@@ -53,6 +53,11 @@ 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"):
|
||||
|
||||
1887
erpnext/accounts/sidebar/accounts/accounts.json
Normal file
1887
erpnext/accounts/sidebar/accounts/accounts.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,25 +4,9 @@
|
||||
{
|
||||
"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\":\"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}}]",
|
||||
"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}}]",
|
||||
"creation": "2026-07-14 12:00:00",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -36,27 +20,23 @@
|
||||
"label": "Accounting",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-14 14:28:55.763394",
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Accounting",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Outgoing Bills",
|
||||
"label": "Total Outgoing Bills",
|
||||
"number_card_name": "Total Outgoing Bills"
|
||||
},
|
||||
{
|
||||
"label": "Incoming Bills",
|
||||
"label": "Total Incoming Bills",
|
||||
"number_card_name": "Total Incoming Bills"
|
||||
},
|
||||
{
|
||||
"label": "Incoming Payment",
|
||||
"label": "Total Incoming Payment",
|
||||
"number_card_name": "Total Incoming Payment"
|
||||
},
|
||||
{
|
||||
"label": "Outgoing Payment",
|
||||
"number_card_name": "Total Outgoing Payment"
|
||||
}
|
||||
],
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"app": "erpnext",
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Profit and Loss",
|
||||
"label": "Profit and Loss"
|
||||
"chart_name": "Budget Variance",
|
||||
"label": "Budget Variance"
|
||||
}
|
||||
],
|
||||
"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}}]",
|
||||
"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}}]",
|
||||
"creation": "2024-01-05 16:09:16.766939",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -18,260 +18,26 @@
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
"label": "Financial Reports",
|
||||
"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",
|
||||
"links": [],
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Financial Reports",
|
||||
"number_cards": [],
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"app": "erpnext",
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Profit and Loss",
|
||||
"label": "Profit and Loss"
|
||||
"chart_name": "Outgoing Bills (Sales Invoice)",
|
||||
"label": "Outgoing Bills (Sales Invoice)"
|
||||
}
|
||||
],
|
||||
"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 & 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}}]",
|
||||
"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}}]",
|
||||
"creation": "2020-03-02 15:41:59.515192",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -18,595 +18,23 @@
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
"label": "Invoicing",
|
||||
"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",
|
||||
"links": [],
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Invoicing",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Outgoing Bills",
|
||||
"label": "Total Outgoing Bills",
|
||||
"number_card_name": "Total Outgoing Bills"
|
||||
},
|
||||
{
|
||||
"label": "Incoming Bills",
|
||||
"label": "Total Incoming Bills",
|
||||
"number_card_name": "Total Incoming Bills"
|
||||
},
|
||||
{
|
||||
"label": "Incoming Payment",
|
||||
"number_card_name": "Total Incoming Payment"
|
||||
},
|
||||
{
|
||||
"label": "Outgoing Payment",
|
||||
"label": "Total Outgoing Payment",
|
||||
"number_card_name": "Total Outgoing Payment"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"charts": [],
|
||||
"content": "[]",
|
||||
"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}}]",
|
||||
"creation": "2026-06-11 11:51:21.886461",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -15,12 +20,25 @@
|
||||
"label": "Payments",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-14 12:00:00.000000",
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
"name": "Payments",
|
||||
"number_cards": [],
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"owner": "Administrator",
|
||||
"public": 1,
|
||||
"quick_lists": [],
|
||||
|
||||
@@ -2109,13 +2109,17 @@ 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 or "Macbook Pro",
|
||||
"item_code": item_code,
|
||||
"item_name": "Macbook Pro",
|
||||
"description": "Macbook Pro Retina Display",
|
||||
"asset_category": asset_category or "Computers",
|
||||
|
||||
@@ -54,14 +54,16 @@ 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(
|
||||
@@ -74,7 +76,7 @@ class AssetCapitalizationGLComposer(BaseStockGLComposer):
|
||||
"remarks": doc.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": -1 * stock_value_difference,
|
||||
},
|
||||
_inv_dict["account_currency"],
|
||||
account_currency,
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -440,7 +440,11 @@ 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 create_warehouse("_Test Warehouse", company=company)
|
||||
warehouse = args.warehouse or (
|
||||
"_Test Warehouse - _TC"
|
||||
if company == "_Test Company"
|
||||
else create_warehouse("_Test Warehouse", company=company)
|
||||
)
|
||||
source_warehouse = args.source_warehouse or warehouse
|
||||
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
|
||||
@@ -20,7 +20,6 @@ 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,
|
||||
@@ -32,7 +31,6 @@ 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()
|
||||
|
||||
371
erpnext/assets/sidebar/assets/assets.json
Normal file
371
erpnext/assets/sidebar/assets/assets.json
Normal file
@@ -0,0 +1,371 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"label": "Asset Value Analytics"
|
||||
}
|
||||
],
|
||||
"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 & 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}}]",
|
||||
"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}}]",
|
||||
"creation": "2020-03-02 15:43:27.634865",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -17,194 +17,26 @@
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "Assets",
|
||||
"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",
|
||||
"links": [],
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"module_onboarding": "Asset Onboarding",
|
||||
"name": "Assets",
|
||||
"number_cards": [],
|
||||
"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)"
|
||||
}
|
||||
],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-16 00:00:00.000000",
|
||||
"docstatus": 0,
|
||||
"doctype": "Sidebar",
|
||||
"header_icon": "layers",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bulk Transaction Log Detail",
|
||||
"link_to": "Bulk Transaction Log Detail",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bulk Transaction Log",
|
||||
"link_to": "Bulk Transaction Log",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-16 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Bulk Transaction",
|
||||
"name": "Bulk Transaction",
|
||||
"owner": "Administrator",
|
||||
"sequence_id": 0.0,
|
||||
"standard": 1,
|
||||
"title": "Bulk Transaction"
|
||||
}
|
||||
@@ -89,6 +89,7 @@ def make_purchase_receipt(
|
||||
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},
|
||||
@@ -193,6 +194,7 @@ 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 not doc.closed
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
|
||||
@@ -14,7 +14,9 @@ frappe.ui.form.on("Purchase Order", {
|
||||
setup: function (frm) {
|
||||
frm.set_indicator_formatter("item_code", function (doc) {
|
||||
let color;
|
||||
if (!doc.qty && frm.doc.has_unit_price_items) {
|
||||
if (doc.closed) {
|
||||
color = "gray";
|
||||
} else if (!doc.qty && frm.doc.has_unit_price_items) {
|
||||
color = "yellow";
|
||||
} else if (doc.qty <= doc.received_qty) {
|
||||
color = "green";
|
||||
@@ -340,7 +342,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")) {
|
||||
if (this.frm.has_perm("submit") && !doc.items.every((item) => item.closed)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Re-open"),
|
||||
() => this.unclose_purchase_order(),
|
||||
@@ -352,7 +354,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
if (doc.status != "On Hold") {
|
||||
if (
|
||||
(doc.items
|
||||
.filter((item) => !item.delivered_by_supplier)
|
||||
.filter((item) => !item.delivered_by_supplier && !item.closed)
|
||||
.some((item) => item.received_qty < item.qty) ||
|
||||
doc.__onload?.has_pending_receivable_qty) &&
|
||||
allow_receipt
|
||||
@@ -365,7 +367,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Create")
|
||||
);
|
||||
if (doc.is_subcontracted) {
|
||||
if (!doc.items.every((item) => item.qty == item.subcontracted_qty)) {
|
||||
if (
|
||||
!doc.items
|
||||
.filter((item) => !item.closed)
|
||||
.every((item) => item.qty == item.subcontracted_qty)
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
@@ -433,6 +439,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
} else if (doc.docstatus === 0) {
|
||||
this.frm.cscript.add_from_mappers();
|
||||
}
|
||||
|
||||
this.set_item_close_buttons();
|
||||
}
|
||||
|
||||
validate() {
|
||||
@@ -697,6 +705,19 @@ 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)
|
||||
|
||||
@@ -402,6 +402,12 @@ 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 on_submit(self):
|
||||
super().on_submit()
|
||||
|
||||
@@ -531,7 +537,7 @@ class PurchaseOrder(BuyingController):
|
||||
considering the configured over_delivery_receipt_allowance.
|
||||
"""
|
||||
for item in self.get("items", []):
|
||||
if item.delivered_by_supplier:
|
||||
if item.delivered_by_supplier or item.closed:
|
||||
continue
|
||||
tolerance = flt(get_allowance_for(item.item_code, qty_or_amount="qty")[0])
|
||||
max_receivable_qty = flt(item.qty) * (100 + tolerance) / 100
|
||||
|
||||
@@ -9,6 +9,7 @@ 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:
|
||||
@@ -18,6 +19,10 @@ 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()
|
||||
@@ -26,6 +31,17 @@ 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")
|
||||
@@ -39,10 +55,9 @@ class StatusService:
|
||||
def update_receiving_percentage(self) -> None:
|
||||
doc = self.doc
|
||||
total_qty, received_qty = 0.0, 0.0
|
||||
for item in doc.items:
|
||||
for item in [item for item in doc.items if not item.closed] or doc.items:
|
||||
received_qty += min(item.received_qty, item.qty)
|
||||
total_qty += item.qty
|
||||
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)
|
||||
|
||||
per_received = flt(received_qty / total_qty) * 100 if total_qty else 0
|
||||
doc.db_set("per_received", per_received, update_modified=False)
|
||||
|
||||
@@ -333,6 +333,38 @@ 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
|
||||
@@ -378,9 +410,7 @@ 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(
|
||||
@@ -794,14 +824,9 @@ 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)
|
||||
|
||||
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 = make_item(properties={"min_order_qty": 50000, "stock_uom": "_Test UOM 1"})
|
||||
item_doc.append("uoms", {"uom": "Pound", "conversion_factor": 453.592292197})
|
||||
item_doc.save()
|
||||
item = item_doc.name
|
||||
@@ -1799,25 +1824,11 @@ 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)
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"returned_qty",
|
||||
"column_break_60",
|
||||
"billed_amt",
|
||||
"closed",
|
||||
"accounting_details",
|
||||
"expense_account",
|
||||
"column_break_fyqr",
|
||||
@@ -557,6 +558,7 @@
|
||||
"fieldname": "blanket_order_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Blanket Order Rate",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -645,6 +647,15 @@
|
||||
"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",
|
||||
@@ -944,7 +955,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-07 17:31:31.732720",
|
||||
"modified": "2026-08-27 11:55:37.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -29,6 +29,7 @@ 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
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.contacts.address_and_contact import (
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
from erpnext.accounts.party import (
|
||||
get_dashboard_info,
|
||||
@@ -184,10 +185,15 @@ class Supplier(TransactionBase):
|
||||
)
|
||||
|
||||
if internal_supplier:
|
||||
internal_supplier_link = get_link_to_form("Supplier", internal_supplier)
|
||||
frappe.throw(
|
||||
_("Internal Supplier for company {0} already exists").format(
|
||||
frappe.bold(self.represents_company)
|
||||
)
|
||||
_(
|
||||
"Internal Supplier {0} already exists for {1}. Disable it to make this Supplier internal."
|
||||
).format(
|
||||
internal_supplier_link,
|
||||
frappe.bold(self.represents_company),
|
||||
),
|
||||
title=_("Internal Supplier Already Exists"),
|
||||
)
|
||||
|
||||
def create_primary_contact(self):
|
||||
|
||||
@@ -262,7 +262,7 @@ def get_purchased_items(supplier_quotation: str):
|
||||
frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
|
||||
fields=["supplier_quotation_item", {"SUM": "qty"}],
|
||||
fields=["supplier_quotation_item", {"SUM": "stock_qty"}],
|
||||
group_by="supplier_quotation_item",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ 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.tests.assertions import assert_raises_with_savepoint
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -126,6 +127,52 @@ 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()
|
||||
@@ -161,12 +208,9 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
]
|
||||
)
|
||||
|
||||
frappe.db.savepoint("before_cancel")
|
||||
# check if item having purchase order can be removed
|
||||
self.assertRaises(
|
||||
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||
)
|
||||
frappe.db.rollback(save_point="before_cancel")
|
||||
with assert_raises_with_savepoint(self, frappe.LinkExistsError):
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
|
||||
@@ -10,6 +10,26 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
options: ["Supplier Group", "Supplier", "Item Group", "Item"],
|
||||
default: "Supplier",
|
||||
reqd: 1,
|
||||
on_change: function () {
|
||||
const entity_filter = frappe.query_report.get_filter("entity");
|
||||
if (entity_filter) {
|
||||
entity_filter.df.label = __(frappe.query_report.get_filter_value("tree_type"));
|
||||
entity_filter.set_value([]);
|
||||
entity_filter.refresh();
|
||||
}
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "entity",
|
||||
label: __("Entity"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
const tree_type = frappe.query_report.get_filter_value("tree_type");
|
||||
if (!tree_type || tree_type === "Order Type") return [];
|
||||
return frappe.db.get_link_options(tree_type, txt);
|
||||
},
|
||||
depends_on: "eval:doc.tree_type != 'Order Type'",
|
||||
},
|
||||
{
|
||||
fieldname: "doc_type",
|
||||
@@ -65,6 +85,19 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
default: "Monthly",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "curves",
|
||||
label: __("Curves"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "select", label: __("Select") },
|
||||
{ value: "all", label: __("All") },
|
||||
{ value: "non-zeros", label: __("Non-Zeros") },
|
||||
{ value: "total", label: __("Total Only") },
|
||||
],
|
||||
default: "select",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_aggregate_value_from_subsidiary_companies",
|
||||
label: __("Show Aggregate Value from Subsidiary Companies"),
|
||||
|
||||
@@ -43,6 +43,44 @@ class TestPurchaseAnalytics(ERPNextTestSuite):
|
||||
company=COMPANY, supplier=SUPPLIER, qty=qty, rate=rate, transaction_date="2019-04-10"
|
||||
)
|
||||
|
||||
def test_supplier_entity_filter(self):
|
||||
filters = self._filters(tree_type="Supplier", entity=[SUPPLIER], curves="all")
|
||||
base_total = flt(self._rows(filters).get(SUPPLIER, {}).get("total", 0.0))
|
||||
|
||||
po = self.make_po()
|
||||
columns, data, _message, chart, *_rest = execute(filters)
|
||||
|
||||
self.assertTrue(columns)
|
||||
self.assertEqual({row["entity"] for row in data}, {SUPPLIER})
|
||||
self.assertAlmostEqual(data[0]["total"] - base_total, flt(po.base_net_total), places=2)
|
||||
|
||||
supplier_name = frappe.db.get_value("Supplier", SUPPLIER, "supplier_name")
|
||||
self.assertEqual({dataset["name"] for dataset in chart["data"]["datasets"]}, {supplier_name})
|
||||
|
||||
def test_parent_supplier_group_filter_preserves_rollup(self):
|
||||
self.make_po()
|
||||
filters = self._filters(tree_type="Supplier Group")
|
||||
unfiltered = self._rows(filters)
|
||||
filtered = self._rows(self._filters(tree_type="Supplier Group", entity=["All Supplier Groups"]))
|
||||
|
||||
self.assertEqual(set(filtered), {"All Supplier Groups"})
|
||||
self.assertAlmostEqual(
|
||||
filtered["All Supplier Groups"]["total"],
|
||||
unfiltered["All Supplier Groups"]["total"],
|
||||
places=2,
|
||||
)
|
||||
|
||||
def test_supplier_group_entity_filter(self):
|
||||
self.make_po()
|
||||
unfiltered = self._rows(self._filters(tree_type="Supplier Group"))
|
||||
filtered = self._rows(self._filters(tree_type="Supplier Group", entity=[SUPPLIER_GROUP]))
|
||||
|
||||
self.assertEqual(set(filtered), {SUPPLIER_GROUP})
|
||||
self.assertEqual(filtered[SUPPLIER_GROUP]["indent"], 0)
|
||||
self.assertAlmostEqual(
|
||||
filtered[SUPPLIER_GROUP]["total"], unfiltered[SUPPLIER_GROUP]["total"], places=2
|
||||
)
|
||||
|
||||
def test_supplier_group_tree_rolls_up_to_root(self):
|
||||
filters = self._filters(tree_type="Supplier Group")
|
||||
base = self._rows(filters)
|
||||
|
||||
642
erpnext/buying/sidebar/buying/buying.json
Normal file
642
erpnext/buying/sidebar/buying/buying.json
Normal file
@@ -0,0 +1,642 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-16 00:00:00.000000",
|
||||
"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": "rocket",
|
||||
"indent": 1,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 1,
|
||||
"label": "Subcontracting",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Section Break"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "folder-tree",
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Subcontracting BOM",
|
||||
"link_to": "Subcontracting BOM",
|
||||
"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": "Subcontracting Inward Order",
|
||||
"link_to": "Subcontracting Inward Order",
|
||||
"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": "Subcontracting Delivery",
|
||||
"link_to": "Stock Entry",
|
||||
"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": "Subcontracting Order",
|
||||
"link_to": "Subcontracting Order",
|
||||
"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": "Subcontracting Receipt",
|
||||
"link_to": "Subcontracting Receipt",
|
||||
"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-08-16 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying",
|
||||
"owner": "Administrator",
|
||||
"sequence_id": 0.0,
|
||||
"standard": 1,
|
||||
"title": "Buying"
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
"app": "erpnext",
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Purchase Order Trends",
|
||||
"label": "Purchase Order Trends"
|
||||
"chart_name": "Purchase Order Analysis",
|
||||
"label": "Purchase Order Analysis"
|
||||
}
|
||||
],
|
||||
"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 & 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}}]",
|
||||
"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}}]",
|
||||
"creation": "2020-01-28 11:50:26.195467",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -17,507 +17,24 @@
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "Buying",
|
||||
"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",
|
||||
"links": [],
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"module_onboarding": "Buying Onboarding",
|
||||
"name": "Buying",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Purchase Orders Count",
|
||||
"number_card_name": "Purchase Orders Count"
|
||||
"label": "Purchase Orders to Receive",
|
||||
"number_card_name": "Purchase Orders to Receive"
|
||||
},
|
||||
{
|
||||
"label": "Total Purchase Amount",
|
||||
"number_card_name": "Total Purchase Amount"
|
||||
"label": "Purchase Orders to Bill",
|
||||
"number_card_name": "Purchase Orders to Bill"
|
||||
},
|
||||
{
|
||||
"label": "Average Order Values",
|
||||
"number_card_name": "Average Order Values"
|
||||
"label": "Annual Purchase",
|
||||
"number_card_name": "Annual Purchase"
|
||||
}
|
||||
],
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-16 00:00:00.000000",
|
||||
"docstatus": 0,
|
||||
"doctype": "Sidebar",
|
||||
"header_icon": "messages-square",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Communication Medium",
|
||||
"link_to": "Communication Medium",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-16 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Communication",
|
||||
"name": "Communication",
|
||||
"owner": "Administrator",
|
||||
"sequence_id": 0.0,
|
||||
"standard": 1,
|
||||
"title": "Communication"
|
||||
}
|
||||
@@ -39,6 +39,7 @@ 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,
|
||||
@@ -227,7 +228,23 @@ 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()
|
||||
|
||||
|
||||
145
erpnext/controllers/item_close.py
Normal file
145
erpnext/controllers/item_close.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# 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"))
|
||||
)
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
blanket_orders = (
|
||||
query = (
|
||||
frappe.qb.from_(bo)
|
||||
.from_(bo_item)
|
||||
.select(bo.name)
|
||||
@@ -814,10 +814,12 @@ def get_blanket_orders(doctype: str, txt: str, searchfield: str, start: int, pag
|
||||
& (bo.company == filters.get("company"))
|
||||
& (bo.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
return blanket_orders
|
||||
if currency := filters.get("currency"):
|
||||
query = query.where(bo.currency == currency)
|
||||
|
||||
return query.run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -8,6 +8,8 @@ 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
|
||||
@@ -192,9 +194,60 @@ 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"):
|
||||
@@ -605,24 +658,49 @@ class StatusUpdater(Document):
|
||||
|
||||
@staticmethod
|
||||
def _calculate_target_parent_percentage(
|
||||
name, target_parent_dt, target_dt, target_ref_field, target_field
|
||||
name,
|
||||
target_parent_dt,
|
||||
target_dt,
|
||||
target_ref_field,
|
||||
target_field,
|
||||
target_parent_field=None,
|
||||
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={"parent": name, "parenttype": target_parent_dt},
|
||||
fields=[target_ref_field, target_field],
|
||||
filters=filters,
|
||||
fields=fields,
|
||||
)
|
||||
|
||||
if exclude_field and not child_records:
|
||||
return 100
|
||||
|
||||
# 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
|
||||
|
||||
sum_ref = sum(abs(record[ref_key]) for record in child_records)
|
||||
# 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)
|
||||
|
||||
if sum_ref > 0:
|
||||
percentage = round(
|
||||
sum(min(abs(record[target_field]), abs(record[ref_key])) for record in child_records)
|
||||
/ sum_ref
|
||||
* 100,
|
||||
sum(min(abs(record[target_field]), abs(record[ref_key])) for record in basis) / sum_ref * 100,
|
||||
6,
|
||||
)
|
||||
else:
|
||||
@@ -671,6 +749,8 @@ class StatusUpdater(Document):
|
||||
args["target_dt"],
|
||||
args["target_ref_field"],
|
||||
args["target_field"],
|
||||
args["target_parent_field"],
|
||||
args.get("exclude_field"),
|
||||
)
|
||||
# update field
|
||||
if args.get("status_field"):
|
||||
|
||||
@@ -337,8 +337,12 @@ 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"],
|
||||
fields=["name", "qty", "returned_qty", "rate", "amount", "billed_amt", "closed"],
|
||||
)
|
||||
# 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.
|
||||
@@ -434,6 +438,7 @@ class StockController(AccountsController):
|
||||
voucher_detail_no=None,
|
||||
item=None,
|
||||
posting_date=None,
|
||||
dimensions=None,
|
||||
):
|
||||
from erpnext.accounts.services.base_gl_composer import add_gl_entry
|
||||
|
||||
@@ -453,6 +458,7 @@ class StockController(AccountsController):
|
||||
voucher_detail_no,
|
||||
item,
|
||||
posting_date,
|
||||
dimensions,
|
||||
)
|
||||
|
||||
def update_stock_reservation_entries(self):
|
||||
|
||||
@@ -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("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
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("is_legacy_scrap_item")
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type")
|
||||
)
|
||||
for item in self.items:
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
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("is_legacy_scrap_item")
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type")
|
||||
)
|
||||
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("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
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("is_legacy_scrap_item"):
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
item.additional_cost_per_qty = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -243,7 +243,7 @@ class SubcontractingInwardController:
|
||||
for item in self.get("items")
|
||||
if not item.is_finished_item
|
||||
and not item.secondary_item_type
|
||||
and not item.is_legacy_scrap_item
|
||||
and not item.valuation_type
|
||||
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
||||
]
|
||||
|
||||
@@ -380,7 +380,7 @@ class SubcontractingInwardController:
|
||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||
for item in self.items:
|
||||
if (
|
||||
item.is_finished_item or item.secondary_item_type or item.is_legacy_scrap_item
|
||||
item.is_finished_item or item.secondary_item_type or item.valuation_type
|
||||
) and item.valuation_rate == 0:
|
||||
item.allow_zero_valuation_rate = 1
|
||||
|
||||
@@ -480,7 +480,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery_on_save()
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item:
|
||||
if not item.secondary_item_type and not item.valuation_type:
|
||||
delivered_qty, returned_qty = frappe.get_value(
|
||||
"Subcontracting Inward Order Item",
|
||||
item.scio_detail,
|
||||
@@ -550,7 +550,7 @@ class SubcontractingInwardController:
|
||||
bold(
|
||||
frappe.get_cached_value(
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
if not item.secondary_item_type and not item.valuation_type
|
||||
else "Subcontracting Inward Order Secondary Item",
|
||||
item.scio_detail,
|
||||
"stock_uom",
|
||||
@@ -602,7 +602,7 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
for item in [item for item in self.items if not item.is_finished_item]:
|
||||
if item.secondary_item_type or item.is_legacy_scrap_item:
|
||||
if item.secondary_item_type or item.valuation_type:
|
||||
scio_secondary_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
{
|
||||
@@ -661,7 +661,7 @@ class SubcontractingInwardController:
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
if not item.secondary_item_type and not item.valuation_type
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
qty_map[doctype][item.scio_detail] += (
|
||||
@@ -802,7 +802,7 @@ class SubcontractingInwardController:
|
||||
items = [
|
||||
item
|
||||
for item in self.items
|
||||
if not item.is_finished_item and not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
if not item.is_finished_item and not item.secondary_item_type and not item.valuation_type
|
||||
]
|
||||
if not items:
|
||||
return
|
||||
@@ -913,7 +913,7 @@ class SubcontractingInwardController:
|
||||
def update_inward_order_secondary_items(self):
|
||||
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
||||
secondary_items_list = [
|
||||
item for item in self.items if item.secondary_item_type or item.is_legacy_scrap_item
|
||||
item for item in self.items if item.secondary_item_type or item.valuation_type
|
||||
]
|
||||
|
||||
secondary_items = defaultdict(float)
|
||||
|
||||
@@ -53,7 +53,6 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
self.item = "_Test Item"
|
||||
self.customer = "_Test Customer USD"
|
||||
self.supplier = "_Test Supplier USD"
|
||||
self.create_account()
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
|
||||
def create_account(self):
|
||||
@@ -99,6 +98,7 @@ class TestAccountsController(ERPNextTestSuite):
|
||||
setattr(self, x.attribute_name, acc.name)
|
||||
|
||||
def setup_advance_accounts_in_party_master(self):
|
||||
self.create_account()
|
||||
company = frappe.get_doc("Company", self.company)
|
||||
company.book_advance_payments_in_separate_party_account = 1
|
||||
company.save()
|
||||
|
||||
231
erpnext/controllers/tests/test_item_close.py
Normal file
231
erpnext/controllers/tests/test_item_close.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.mapper import (
|
||||
get_mapped_purchase_invoice,
|
||||
make_purchase_receipt,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.controllers.item_close import update_closed_status
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
|
||||
def get_ordered_qty(item_code):
|
||||
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": WAREHOUSE}, "ordered_qty"))
|
||||
|
||||
|
||||
class TestPurchaseOrderItemClose(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
||||
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
def make_purchase_order(self):
|
||||
po = create_purchase_order(item_code=self.first_item, qty=10, rate=100, do_not_save=True)
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": self.second_item,
|
||||
"warehouse": WAREHOUSE,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
},
|
||||
)
|
||||
po.set_missing_values()
|
||||
po.insert()
|
||||
po.submit()
|
||||
return po
|
||||
|
||||
def close_items(self, po, rows, closed=1):
|
||||
update_closed_status("Purchase Order", po.name, [row.name for row in rows], closed)
|
||||
po.reload()
|
||||
|
||||
def test_closing_row_releases_ordered_qty(self):
|
||||
po = self.make_purchase_order()
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 10)
|
||||
|
||||
self.close_items(po, [po.items[1]])
|
||||
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 0)
|
||||
self.assertEqual(get_ordered_qty(self.first_item), 10)
|
||||
|
||||
def test_closing_row_settles_receiving_percentage(self):
|
||||
po = self.make_purchase_order()
|
||||
|
||||
receipt = make_purchase_receipt(po.name)
|
||||
receipt.items = [item for item in receipt.items if item.item_code == self.first_item]
|
||||
receipt.insert()
|
||||
receipt.submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.per_received, 50)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
self.close_items(po, [po.items[1]])
|
||||
|
||||
self.assertEqual(po.per_received, 100)
|
||||
self.assertEqual(po.status, "To Bill")
|
||||
|
||||
def test_closing_every_row_closes_the_order(self):
|
||||
po = self.make_purchase_order()
|
||||
|
||||
self.close_items(po, po.items)
|
||||
|
||||
self.assertEqual(po.status, "Closed")
|
||||
self.assertEqual(get_ordered_qty(self.first_item), 0)
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 0)
|
||||
|
||||
def test_parent_reopen_is_blocked_when_all_rows_are_closed(self):
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, po.items)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, po.update_status, "Submitted")
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.status, "Closed")
|
||||
self.assertTrue(all(row.closed for row in po.items))
|
||||
|
||||
def test_reopening_all_rows_restores_the_order(self):
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, po.items)
|
||||
self.assertEqual(po.status, "Closed")
|
||||
|
||||
self.close_items(po, po.items, closed=0)
|
||||
|
||||
self.assertFalse(any(row.closed for row in po.items))
|
||||
self.assertEqual(po.per_received, 0)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
self.assertEqual(get_ordered_qty(self.first_item), 10)
|
||||
|
||||
def test_reopening_one_row_reopens_the_parent(self):
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, po.items)
|
||||
|
||||
self.close_items(po, [po.items[1]], closed=0)
|
||||
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
self.assertTrue(po.items[0].closed)
|
||||
self.assertFalse(po.items[1].closed)
|
||||
# nothing received, and the closed row is written off rather than counted
|
||||
self.assertEqual(po.per_received, 0)
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 10)
|
||||
self.assertEqual(get_ordered_qty(self.first_item), 0)
|
||||
|
||||
def test_settled_row_cannot_be_closed(self):
|
||||
po = self.make_purchase_order()
|
||||
|
||||
receipt = make_purchase_receipt(po.name)
|
||||
receipt.insert()
|
||||
receipt.submit()
|
||||
invoice = get_mapped_purchase_invoice(po.name)
|
||||
invoice.insert()
|
||||
invoice.submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.status, "Completed")
|
||||
self.assertRaises(frappe.ValidationError, self.close_items, po, [po.items[0]])
|
||||
|
||||
def test_received_but_unbilled_row_can_be_closed(self):
|
||||
po = self.make_purchase_order()
|
||||
|
||||
receipt = make_purchase_receipt(po.name)
|
||||
receipt.insert()
|
||||
receipt.submit()
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.status, "To Bill")
|
||||
|
||||
self.close_items(po, po.items)
|
||||
|
||||
# billing written off, but the goods really did arrive
|
||||
self.assertEqual(po.per_billed, 0)
|
||||
self.assertEqual(po.per_received, 100)
|
||||
self.assertEqual(po.status, "Closed")
|
||||
|
||||
def test_receipt_is_not_offered_when_the_rest_is_closed(self):
|
||||
po = self.make_purchase_order()
|
||||
|
||||
receipt = make_purchase_receipt(po.name)
|
||||
receipt.items = [item for item in receipt.items if item.item_code == self.first_item]
|
||||
receipt.insert()
|
||||
receipt.submit()
|
||||
|
||||
po.reload()
|
||||
self.close_items(po, [po.items[1]])
|
||||
|
||||
self.assertEqual(po.status, "To Bill")
|
||||
self.assertFalse(po.has_pending_receivable_qty())
|
||||
self.assertFalse(make_purchase_receipt(po.name).get("items"))
|
||||
|
||||
def test_reopening_partly_closed_order_keeps_row_flags(self):
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, [po.items[1]])
|
||||
|
||||
po.update_status("Closed")
|
||||
po.reload()
|
||||
self.assertEqual(po.status, "Closed")
|
||||
|
||||
po.update_status("Submitted")
|
||||
po.reload()
|
||||
|
||||
self.assertFalse(po.items[0].closed)
|
||||
self.assertTrue(po.items[1].closed)
|
||||
self.assertEqual(get_ordered_qty(self.first_item), 10)
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 0)
|
||||
|
||||
def test_closed_row_is_not_mapped_to_purchase_receipt(self):
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, [po.items[1]])
|
||||
|
||||
receipt = make_purchase_receipt(po.name)
|
||||
|
||||
self.assertEqual([item.item_code for item in receipt.items], [self.first_item])
|
||||
|
||||
def test_receiving_a_closed_row_is_blocked(self):
|
||||
po = self.make_purchase_order()
|
||||
receipt = make_purchase_receipt(po.name)
|
||||
|
||||
self.close_items(po, [po.items[1]])
|
||||
|
||||
receipt.insert()
|
||||
self.assertRaises(frappe.ValidationError, receipt.submit)
|
||||
|
||||
def test_reopening_a_row_restores_pending_qty(self):
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, [po.items[1]])
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 0)
|
||||
|
||||
self.close_items(po, [po.items[1]], closed=0)
|
||||
|
||||
self.assertEqual(get_ordered_qty(self.second_item), 10)
|
||||
self.assertEqual(po.per_received, 0)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
def test_closing_is_rejected_for_unsupported_doctype(self):
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
update_closed_status,
|
||||
"Material Request",
|
||||
"any-name",
|
||||
["any-row"],
|
||||
1,
|
||||
)
|
||||
|
||||
def test_amending_clears_closed_rows(self):
|
||||
"""Frappe keeps no_copy fields when amending, so the flag must be cleared."""
|
||||
po = self.make_purchase_order()
|
||||
self.close_items(po, [po.items[1]])
|
||||
po.cancel()
|
||||
|
||||
amended = frappe.copy_doc(po, ignore_no_copy=True)
|
||||
amended.docstatus = 0
|
||||
amended.amended_from = po.name
|
||||
amended.insert()
|
||||
|
||||
self.assertFalse(any(row.closed for row in amended.items))
|
||||
264
erpnext/controllers/tests/test_item_close_billing.py
Normal file
264
erpnext/controllers/tests/test_item_close_billing.py
Normal file
@@ -0,0 +1,264 @@
|
||||
# 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
|
||||
|
||||
from erpnext.controllers.item_close import update_closed_status
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
|
||||
class TestPurchaseReceiptItemClose(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
||||
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
def make_purchase_receipt(self):
|
||||
receipt = make_purchase_receipt(
|
||||
item_code=self.first_item, qty=10, rate=100, warehouse=WAREHOUSE, do_not_submit=True
|
||||
)
|
||||
receipt.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": self.second_item,
|
||||
"warehouse": WAREHOUSE,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
receipt.save()
|
||||
receipt.submit()
|
||||
return receipt
|
||||
|
||||
def close_items(self, doc, rows, closed=1):
|
||||
update_closed_status(doc.doctype, doc.name, [row.name for row in rows], closed)
|
||||
doc.reload()
|
||||
|
||||
def test_closing_a_row_does_not_inflate_billing_percentage(self):
|
||||
receipt = self.make_purchase_receipt()
|
||||
self.assertEqual(receipt.per_billed, 0)
|
||||
|
||||
self.close_items(receipt, [receipt.items[1]])
|
||||
|
||||
# nothing was billed, so the receipt must not read as partly billed
|
||||
self.assertEqual(receipt.per_billed, 0)
|
||||
self.assertEqual(receipt.status, "To Bill")
|
||||
|
||||
def test_closing_every_row_closes_the_receipt(self):
|
||||
receipt = self.make_purchase_receipt()
|
||||
|
||||
self.close_items(receipt, receipt.items)
|
||||
|
||||
# nothing was billed, and writing every row off must not claim otherwise
|
||||
self.assertEqual(receipt.per_billed, 0)
|
||||
self.assertEqual(receipt.status, "Closed")
|
||||
|
||||
def test_closed_row_is_not_mapped_to_purchase_invoice(self):
|
||||
receipt = self.make_purchase_receipt()
|
||||
self.close_items(receipt, [receipt.items[1]])
|
||||
|
||||
invoice = make_purchase_invoice(receipt.name)
|
||||
|
||||
self.assertEqual([item.item_code for item in invoice.items], [self.first_item])
|
||||
|
||||
def test_billing_a_closed_row_is_blocked(self):
|
||||
receipt = self.make_purchase_receipt()
|
||||
invoice = make_purchase_invoice(receipt.name)
|
||||
|
||||
self.close_items(receipt, [receipt.items[1]])
|
||||
|
||||
invoice.insert()
|
||||
self.assertRaises(frappe.ValidationError, invoice.submit)
|
||||
|
||||
def test_parent_reopen_is_blocked_when_all_rows_are_closed(self):
|
||||
receipt = self.make_purchase_receipt()
|
||||
self.close_items(receipt, receipt.items)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, receipt.update_status, "Submitted")
|
||||
|
||||
def test_reopening_one_row_reopens_the_receipt(self):
|
||||
receipt = self.make_purchase_receipt()
|
||||
self.close_items(receipt, receipt.items)
|
||||
|
||||
self.close_items(receipt, [receipt.items[1]], closed=0)
|
||||
|
||||
self.assertNotEqual(receipt.status, "Closed")
|
||||
self.assertEqual(receipt.per_billed, 0)
|
||||
|
||||
def test_unbilled_return_row_can_be_closed(self):
|
||||
"""Return rows are closable by design, not by an accident of sign."""
|
||||
receipt = self.make_purchase_receipt()
|
||||
return_receipt = make_return_doc("Purchase Receipt", receipt.name)
|
||||
return_receipt.insert()
|
||||
return_receipt.submit()
|
||||
|
||||
row = return_receipt.items[0]
|
||||
self.assertLess(row.amount, 0)
|
||||
self.assertTrue(return_receipt.is_item_closable(row))
|
||||
|
||||
self.close_items(return_receipt, [row])
|
||||
self.assertTrue(return_receipt.items[0].closed)
|
||||
|
||||
|
||||
class TestDeliveryNoteItemClose(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
||||
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
||||
for item_code in (self.first_item, self.second_item):
|
||||
make_stock_entry(item_code=item_code, target=WAREHOUSE, qty=100, basic_rate=50)
|
||||
|
||||
def make_delivery_note(self):
|
||||
note = create_delivery_note(
|
||||
item_code=self.first_item, qty=10, rate=100, warehouse=WAREHOUSE, do_not_save=True
|
||||
)
|
||||
note.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": self.second_item,
|
||||
"warehouse": WAREHOUSE,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
note.insert()
|
||||
note.submit()
|
||||
return note
|
||||
|
||||
def close_items(self, doc, rows, closed=1):
|
||||
update_closed_status(doc.doctype, doc.name, [row.name for row in rows], closed)
|
||||
doc.reload()
|
||||
|
||||
def test_closing_a_row_does_not_inflate_billing_percentage(self):
|
||||
note = self.make_delivery_note()
|
||||
self.assertEqual(note.per_billed, 0)
|
||||
|
||||
self.close_items(note, [note.items[1]])
|
||||
|
||||
# nothing was billed, so the note must not read as partially billed
|
||||
self.assertEqual(note.per_billed, 0)
|
||||
self.assertEqual(note.status, "To Bill")
|
||||
|
||||
def test_closing_every_row_closes_the_note(self):
|
||||
note = self.make_delivery_note()
|
||||
|
||||
self.close_items(note, note.items)
|
||||
|
||||
# nothing was billed, and writing every row off must not claim otherwise
|
||||
self.assertEqual(note.per_billed, 0)
|
||||
self.assertEqual(note.status, "Closed")
|
||||
|
||||
def test_closed_row_is_not_mapped_to_sales_invoice(self):
|
||||
note = self.make_delivery_note()
|
||||
self.close_items(note, [note.items[1]])
|
||||
|
||||
invoice = make_sales_invoice(note.name)
|
||||
|
||||
self.assertEqual([item.item_code for item in invoice.items], [self.first_item])
|
||||
|
||||
def test_billing_a_closed_row_is_blocked(self):
|
||||
note = self.make_delivery_note()
|
||||
invoice = make_sales_invoice(note.name)
|
||||
|
||||
self.close_items(note, [note.items[1]])
|
||||
|
||||
invoice.insert()
|
||||
self.assertRaises(frappe.ValidationError, invoice.submit)
|
||||
|
||||
def test_closing_a_row_does_not_mark_it_returned(self):
|
||||
note = self.make_delivery_note()
|
||||
|
||||
self.close_items(note, note.items)
|
||||
|
||||
self.assertEqual(note.per_returned, 0)
|
||||
self.assertEqual(note.status, "Closed")
|
||||
|
||||
def test_amending_clears_closed_rows(self):
|
||||
"""Frappe keeps no_copy fields when amending, so the flag must be cleared."""
|
||||
note = self.make_delivery_note()
|
||||
self.close_items(note, [note.items[1]])
|
||||
note.cancel()
|
||||
|
||||
amended = frappe.copy_doc(note, ignore_no_copy=True)
|
||||
amended.docstatus = 0
|
||||
amended.amended_from = note.name
|
||||
amended.insert()
|
||||
|
||||
self.assertFalse(any(row.closed for row in amended.items))
|
||||
|
||||
def test_noncanonical_closed_value_is_normalised(self):
|
||||
"""A truthy non-1 value must not slip past the exact-match submission guard."""
|
||||
note = self.make_delivery_note()
|
||||
|
||||
update_closed_status("Delivery Note", note.name, [note.items[1].name], 2)
|
||||
|
||||
note.reload()
|
||||
self.assertEqual(note.items[1].closed, 1)
|
||||
|
||||
def test_unbilled_return_row_can_be_closed(self):
|
||||
"""Return rows carry negative amounts and must still be closable."""
|
||||
note = self.make_delivery_note()
|
||||
return_note = make_return_doc("Delivery Note", note.name)
|
||||
return_note.insert()
|
||||
return_note.submit()
|
||||
|
||||
row = return_note.items[0]
|
||||
self.assertLess(row.amount, 0)
|
||||
self.assertTrue(return_note.is_item_closable(row))
|
||||
|
||||
self.close_items(return_note, [row])
|
||||
self.assertTrue(return_note.items[0].closed)
|
||||
|
||||
def test_return_row_pending_amount_is_a_magnitude(self):
|
||||
"""The dialog shows what is outstanding, so a return row must not read as zero."""
|
||||
note = self.make_delivery_note()
|
||||
return_note = make_return_doc("Delivery Note", note.name)
|
||||
return_note.insert()
|
||||
return_note.submit()
|
||||
|
||||
row = return_note.items[0]
|
||||
self.assertLess(row.amount, 0)
|
||||
pending = abs(flt(row.amount)) - abs(flt(row.billed_amt))
|
||||
self.assertEqual(pending, abs(flt(note.items[0].amount)))
|
||||
self.assertGreater(pending, 0)
|
||||
|
||||
def test_closing_a_return_row_leaves_the_original_untouched(self):
|
||||
"""Writing off a credit note must not disturb what was returned."""
|
||||
note = self.make_delivery_note()
|
||||
return_note = make_return_doc("Delivery Note", note.name)
|
||||
return_note.insert()
|
||||
return_note.submit()
|
||||
|
||||
note.reload()
|
||||
before = [(row.returned_qty, row.closed) for row in note.items]
|
||||
per_returned_before = note.per_returned
|
||||
|
||||
self.close_items(return_note, [return_note.items[0]])
|
||||
|
||||
note.reload()
|
||||
self.assertEqual([(row.returned_qty, row.closed) for row in note.items], before)
|
||||
self.assertEqual(note.per_returned, per_returned_before)
|
||||
|
||||
def test_closing_the_unbilled_row_completes_the_note(self):
|
||||
"""The point of the feature: a written off row stops holding billing open."""
|
||||
note = self.make_delivery_note()
|
||||
invoice = make_sales_invoice(note.name)
|
||||
invoice.items = [item for item in invoice.items if item.item_code == self.first_item]
|
||||
invoice.insert()
|
||||
invoice.submit()
|
||||
|
||||
note.reload()
|
||||
self.assertEqual(note.per_billed, 50)
|
||||
|
||||
self.close_items(note, [note.items[1]])
|
||||
|
||||
self.assertEqual(note.per_billed, 100)
|
||||
self.assertEqual(note.status, "Completed")
|
||||
141
erpnext/controllers/tests/test_item_close_sales_order.py
Normal file
141
erpnext/controllers/tests/test_item_close_sales_order.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.controllers.item_close import update_closed_status
|
||||
from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
|
||||
def get_reserved_qty(item_code):
|
||||
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": WAREHOUSE}, "reserved_qty"))
|
||||
|
||||
|
||||
class TestSalesOrderItemClose(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
||||
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
||||
for item_code in (self.first_item, self.second_item):
|
||||
make_stock_entry(item_code=item_code, target=WAREHOUSE, qty=100, basic_rate=50)
|
||||
|
||||
def make_sales_order(self):
|
||||
so = make_sales_order(
|
||||
item_code=self.first_item, qty=10, rate=100, warehouse=WAREHOUSE, do_not_submit=True
|
||||
)
|
||||
so.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": self.second_item,
|
||||
"warehouse": WAREHOUSE,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"delivery_date": add_days(nowdate(), 1),
|
||||
},
|
||||
)
|
||||
so.save()
|
||||
so.submit()
|
||||
return so
|
||||
|
||||
def close_items(self, so, rows, closed=1):
|
||||
update_closed_status("Sales Order", so.name, [row.name for row in rows], closed)
|
||||
so.reload()
|
||||
|
||||
def test_closing_row_releases_reserved_qty(self):
|
||||
so = self.make_sales_order()
|
||||
self.assertEqual(get_reserved_qty(self.second_item), 10)
|
||||
|
||||
self.close_items(so, [so.items[1]])
|
||||
|
||||
self.assertEqual(get_reserved_qty(self.second_item), 0)
|
||||
self.assertEqual(get_reserved_qty(self.first_item), 10)
|
||||
|
||||
def test_closing_row_settles_delivery_percentage(self):
|
||||
so = self.make_sales_order()
|
||||
|
||||
note = make_delivery_note(so.name)
|
||||
note.items = [item for item in note.items if item.item_code == self.first_item]
|
||||
note.insert()
|
||||
note.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.per_delivered, 50)
|
||||
|
||||
self.close_items(so, [so.items[1]])
|
||||
|
||||
self.assertEqual(so.per_delivered, 100)
|
||||
self.assertEqual(so.delivery_status, "Fully Delivered")
|
||||
|
||||
def test_closing_every_row_closes_the_order(self):
|
||||
so = self.make_sales_order()
|
||||
|
||||
self.close_items(so, so.items)
|
||||
|
||||
self.assertEqual(so.status, "Closed")
|
||||
self.assertEqual(get_reserved_qty(self.first_item), 0)
|
||||
self.assertEqual(get_reserved_qty(self.second_item), 0)
|
||||
|
||||
def test_reopening_one_row_reopens_the_parent(self):
|
||||
so = self.make_sales_order()
|
||||
self.close_items(so, so.items)
|
||||
|
||||
self.close_items(so, [so.items[1]], closed=0)
|
||||
|
||||
self.assertNotEqual(so.status, "Closed")
|
||||
self.assertTrue(so.items[0].closed)
|
||||
self.assertFalse(so.items[1].closed)
|
||||
self.assertEqual(get_reserved_qty(self.second_item), 10)
|
||||
self.assertEqual(get_reserved_qty(self.first_item), 0)
|
||||
|
||||
def test_parent_reopen_is_blocked_when_all_rows_are_closed(self):
|
||||
so = self.make_sales_order()
|
||||
self.close_items(so, so.items)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, so.update_status, "Draft")
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.status, "Closed")
|
||||
|
||||
def test_closed_row_is_not_mapped_to_delivery_note(self):
|
||||
so = self.make_sales_order()
|
||||
self.close_items(so, [so.items[1]])
|
||||
|
||||
note = make_delivery_note(so.name)
|
||||
|
||||
self.assertEqual([item.item_code for item in note.items], [self.first_item])
|
||||
|
||||
def test_closed_row_is_not_mapped_to_sales_invoice(self):
|
||||
so = self.make_sales_order()
|
||||
self.close_items(so, [so.items[1]])
|
||||
|
||||
invoice = make_sales_invoice(so.name)
|
||||
|
||||
self.assertEqual([item.item_code for item in invoice.items], [self.first_item])
|
||||
|
||||
def test_delivering_a_closed_row_is_blocked(self):
|
||||
so = self.make_sales_order()
|
||||
note = make_delivery_note(so.name)
|
||||
|
||||
self.close_items(so, [so.items[1]])
|
||||
|
||||
note.insert()
|
||||
self.assertRaises(frappe.ValidationError, note.submit)
|
||||
|
||||
def test_settled_row_cannot_be_closed(self):
|
||||
so = self.make_sales_order()
|
||||
|
||||
note = make_delivery_note(so.name)
|
||||
note.insert()
|
||||
note.submit()
|
||||
invoice = make_sales_invoice(so.name)
|
||||
invoice.insert()
|
||||
invoice.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertRaises(frappe.ValidationError, self.close_items, so, [so.items[0]])
|
||||
96
erpnext/controllers/tests/test_item_close_update_items.py
Normal file
96
erpnext/controllers/tests/test_item_close_update_items.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.services.child_item_update import update_child_qty_rate
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.controllers.item_close import update_closed_status
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
|
||||
class TestUpdateItemsWithClosedRows(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
||||
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
def make_purchase_order(self):
|
||||
po = create_purchase_order(item_code=self.first_item, qty=10, rate=100, do_not_save=True)
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": self.second_item,
|
||||
"warehouse": WAREHOUSE,
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
},
|
||||
)
|
||||
po.set_missing_values()
|
||||
po.insert()
|
||||
po.submit()
|
||||
update_closed_status("Purchase Order", po.name, [po.items[1].name], 1)
|
||||
po.reload()
|
||||
return po
|
||||
|
||||
def as_payload(self, rows, **overrides):
|
||||
return json.dumps(
|
||||
[
|
||||
{
|
||||
"docname": row.name,
|
||||
"item_code": row.item_code,
|
||||
"qty": overrides.get(row.name, row.qty),
|
||||
"rate": row.rate,
|
||||
"uom": row.uom,
|
||||
"conversion_factor": row.conversion_factor,
|
||||
"description": row.description,
|
||||
"schedule_date": str(row.schedule_date),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
)
|
||||
|
||||
def test_payload_without_the_closed_row_does_not_delete_it(self):
|
||||
"""The dialog omits closed rows, and absence must not read as removal."""
|
||||
po = self.make_purchase_order()
|
||||
open_row, closed_row = po.items[0], po.items[1]
|
||||
|
||||
update_child_qty_rate("Purchase Order", self.as_payload([open_row], **{open_row.name: 15}), po.name)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(len(po.items), 2)
|
||||
self.assertEqual(po.items[0].qty, 15)
|
||||
self.assertTrue(po.items[1].closed)
|
||||
self.assertEqual(po.items[1].name, closed_row.name)
|
||||
|
||||
def test_closed_row_cannot_be_changed_through_the_api(self):
|
||||
"""The dialog hides closed rows, but the whitelisted call is the real gate."""
|
||||
po = self.make_purchase_order()
|
||||
closed_row = po.items[1]
|
||||
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
update_child_qty_rate,
|
||||
"Purchase Order",
|
||||
self.as_payload(po.items, **{closed_row.name: 99}),
|
||||
po.name,
|
||||
)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.items[1].qty, 10)
|
||||
|
||||
def test_unchanged_closed_row_in_the_payload_is_tolerated(self):
|
||||
"""A caller sending the whole table untouched should not be rejected."""
|
||||
po = self.make_purchase_order()
|
||||
|
||||
update_child_qty_rate("Purchase Order", self.as_payload(po.items), po.name)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(len(po.items), 2)
|
||||
self.assertTrue(po.items[1].closed)
|
||||
@@ -7,15 +7,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
@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 test_sales_return_validates_against_original(self):
|
||||
# Submitting a return Delivery Note runs validate_returned_items (Item / Packed Item lookups
|
||||
# via frappe.get_all) and get_already_returned_items (qb GROUP BY of the returned qty) -- both
|
||||
@@ -24,16 +15,13 @@ class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name)
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
|
||||
dn = create_delivery_note(qty=5)
|
||||
self.addCleanup(self._cancel_and_delete, "Delivery Note", dn.name)
|
||||
|
||||
return_dn = make_sales_return(dn.name)
|
||||
return_dn.insert()
|
||||
return_dn.submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Delivery Note", return_dn.name)
|
||||
|
||||
self.assertEqual(return_dn.is_return, 1)
|
||||
self.assertEqual(return_dn.items[0].qty, -5)
|
||||
@@ -44,7 +32,6 @@ class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
pi = make_purchase_invoice(qty=10)
|
||||
self.addCleanup(self._cancel_and_delete, "Purchase Invoice", pi.name)
|
||||
|
||||
return_pi = make_purchase_invoice(
|
||||
is_return=1,
|
||||
@@ -66,7 +53,6 @@ class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
pi.items[0].item_code = ""
|
||||
pi.save()
|
||||
pi.submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Purchase Invoice", pi.name)
|
||||
|
||||
return_pi = make_purchase_invoice(
|
||||
item_name="_Test Item",
|
||||
@@ -86,11 +72,9 @@ class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name)
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
|
||||
dn = create_delivery_note(qty=5)
|
||||
self.addCleanup(self._cancel_and_delete, "Delivery Note", dn.name)
|
||||
|
||||
return_dn = make_sales_return(dn.name)
|
||||
return_dn.items[0].qty = 0
|
||||
@@ -104,7 +88,6 @@ class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
si = create_sales_invoice(qty=10)
|
||||
self.addCleanup(self._cancel_and_delete, "Sales Invoice", si.name)
|
||||
|
||||
return_si = make_return_doc(si.doctype, si.name)
|
||||
return_si.items[0].qty = 0
|
||||
@@ -131,14 +114,11 @@ class TestSalesAndPurchaseReturn(ERPNextTestSuite):
|
||||
si.items[0].stock_uom = "Kg"
|
||||
si.items[0].conversion_factor = 0.013888889
|
||||
si.save().submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Sales Invoice", si.name)
|
||||
|
||||
first_return = make_return_doc(si.doctype, si.name)
|
||||
first_return.items[0].qty = -24
|
||||
first_return.save().submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Sales Invoice", first_return.name)
|
||||
|
||||
second_return = make_return_doc(si.doctype, si.name)
|
||||
self.assertEqual(second_return.items[0].qty, -24)
|
||||
second_return.save().submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Sales Invoice", second_return.name)
|
||||
|
||||
@@ -7,15 +7,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestSellingControllerConversions(ERPNextTestSuite):
|
||||
@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 test_partial_delivery_updates_sales_order_status(self):
|
||||
# Submitting a Delivery Note against a Sales Order calls
|
||||
# SellingController.get_already_delivered_qty / get_so_qty_and_warehouse and StatusUpdater
|
||||
@@ -24,8 +15,7 @@ class TestSellingControllerConversions(ERPNextTestSuite):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
se = make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name)
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=100)
|
||||
|
||||
so = make_sales_order(qty=10)
|
||||
|
||||
@@ -33,7 +23,6 @@ class TestSellingControllerConversions(ERPNextTestSuite):
|
||||
dn.items[0].qty = 4
|
||||
dn.insert()
|
||||
dn.submit()
|
||||
self.addCleanup(self._cancel_and_delete, "Delivery Note", dn.name)
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.per_delivered, 40.0)
|
||||
|
||||
@@ -8,15 +8,6 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestStockControllerConversions(ERPNextTestSuite):
|
||||
@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 test_future_sle_exists_detects_later_entries(self):
|
||||
# future_sle_exists / get_conditions_to_validate_future_sle were converted to query builder
|
||||
# (Count + Criterion.any). A later SLE for the same item+warehouse must be detected, which
|
||||
@@ -26,8 +17,7 @@ class TestStockControllerConversions(ERPNextTestSuite):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
item = make_item("_Test Future SLE Item", {"is_stock_item": 1}).name
|
||||
se = make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", se.name)
|
||||
make_stock_entry(item_code=item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||
|
||||
# Pretend a different voucher posts a day earlier for the same item/warehouse: the existing
|
||||
# (later) SLE must be reported as a future entry.
|
||||
@@ -52,7 +42,6 @@ class TestStockControllerConversions(ERPNextTestSuite):
|
||||
posting_date=add_days(today(), -5),
|
||||
posting_time="01:00:00",
|
||||
)
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", opening.name)
|
||||
|
||||
return opening
|
||||
|
||||
@@ -106,7 +95,6 @@ class TestStockControllerConversions(ERPNextTestSuite):
|
||||
finally:
|
||||
stock_ledger.make_entry = original_make_entry
|
||||
|
||||
self.addCleanup(self._cancel_and_delete, "Stock Entry", entry.name)
|
||||
if inject is not None:
|
||||
self.assertTrue(injected, "the later SL Entry was not written during the submit")
|
||||
|
||||
@@ -126,9 +114,6 @@ class TestStockControllerConversions(ERPNextTestSuite):
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
for name in names:
|
||||
self.addCleanup(frappe.delete_doc, "Repost Item Valuation", name, force=1)
|
||||
|
||||
return names
|
||||
|
||||
def test_repost_queued_for_entry_backdated_while_its_sl_entries_were_written(self):
|
||||
|
||||
607
erpnext/crm/sidebar/crm/crm.json
Normal file
607
erpnext/crm/sidebar/crm/crm.json
Normal file
@@ -0,0 +1,607 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-16 00:00:00.000000",
|
||||
"docstatus": 0,
|
||||
"doctype": "Sidebar",
|
||||
"header_icon": "handshake",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "chart-column",
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
"link_to": "CRM",
|
||||
"link_type": "Dashboard",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "users-round",
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Lead",
|
||||
"link_to": "Lead",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "lightbulb",
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Opportunity",
|
||||
"link_to": "Opportunity",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "user",
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer",
|
||||
"link_to": "Customer",
|
||||
"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": "Sales Analytics",
|
||||
"link_to": "Sales 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": "Lead Details",
|
||||
"link_to": "Lead Details",
|
||||
"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": "Sales Pipeline Analytics",
|
||||
"link_to": "Sales Pipeline 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": "Opportunity Summary by Sales Stage",
|
||||
"link_to": "Opportunity Summary by Sales Stage",
|
||||
"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": "Sales Funnel",
|
||||
"link_to": "sales-funnel",
|
||||
"link_type": "Page",
|
||||
"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": "Prospects Engaged But Not Converted",
|
||||
"link_to": "Prospects Engaged But Not Converted",
|
||||
"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": "First Response Time for Opportunity",
|
||||
"link_to": "First Response Time for Opportunity",
|
||||
"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": "Campaign Efficiency",
|
||||
"link_to": "Campaign Efficiency",
|
||||
"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": "Lead Owner Efficiency",
|
||||
"link_to": "Lead Owner Efficiency",
|
||||
"link_type": "Report",
|
||||
"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": "Maintenance Schedule",
|
||||
"link_to": "Maintenance Schedule",
|
||||
"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": "Maintenance Visit",
|
||||
"link_to": "Maintenance Visit",
|
||||
"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": "Warranty Claim",
|
||||
"link_to": "Warranty Claim",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "funnel",
|
||||
"indent": 1,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 1,
|
||||
"label": "Sales Pipeline",
|
||||
"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": "Contract",
|
||||
"link_to": "Contract",
|
||||
"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": "Appointment",
|
||||
"link_to": "Appointment",
|
||||
"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": "Communication",
|
||||
"link_to": "Communication",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "store",
|
||||
"indent": 1,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 1,
|
||||
"label": "Campaign",
|
||||
"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": "Campaign",
|
||||
"link_to": "Campaign",
|
||||
"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": "Email Campaign",
|
||||
"link_to": "Email Campaign",
|
||||
"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": "SMS Center",
|
||||
"link_to": "SMS Center",
|
||||
"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": "SMS Log",
|
||||
"link_to": "SMS 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": "Email Group",
|
||||
"link_to": "Email Group",
|
||||
"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": "Territory",
|
||||
"link_to": "Territory",
|
||||
"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": "Customer Group",
|
||||
"link_to": "Customer 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": "Contact",
|
||||
"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": "Prospect",
|
||||
"link_to": "Prospect",
|
||||
"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": "Sales Person",
|
||||
"link_to": "Sales Person",
|
||||
"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": "Sales Stage",
|
||||
"link_to": "Sales Stage",
|
||||
"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": "Lead Source",
|
||||
"link_to": "UTM Source",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "settings",
|
||||
"indent": 1,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 1,
|
||||
"label": "Settings",
|
||||
"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": "CRM Settings",
|
||||
"link_to": "CRM Settings",
|
||||
"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": "SMS Settings",
|
||||
"link_to": "SMS Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 0,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-16 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM",
|
||||
"owner": "Administrator",
|
||||
"sequence_id": 0.0,
|
||||
"standard": 1,
|
||||
"title": "CRM"
|
||||
}
|
||||
@@ -256,6 +256,9 @@ class CRMNote(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_note(self, note: str, row_id: str):
|
||||
# db_update() skips the write check that save() does in add_note/delete_note
|
||||
self.check_permission("write")
|
||||
|
||||
for d in self.notes:
|
||||
if cstr(d.name) == row_id:
|
||||
d.note = note
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"app": "erpnext",
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Territory Wise Sales",
|
||||
"label": "Territory Wise Sales"
|
||||
"chart_name": "Incoming Leads",
|
||||
"label": "Incoming Leads"
|
||||
}
|
||||
],
|
||||
"content": "[{\"id\":\"4jhDsfZ7EP\",\"type\":\"header\",\"data\":{\"text\":\"This module is scheduled for deprecation and will be completely removed in version 17, please use <a href=\\\"http://frappe.io/crm\\\">Frappe CRM</a> instead.\",\"col\":12}},{\"id\":\"Cj2TyhgiWy\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Territory Wise Sales\",\"col\":12}},{\"id\":\"LAKRmpYMRA\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"69RN0XsiJK\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Lead\",\"col\":3}},{\"id\":\"t6PQ0vY-Iw\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Opportunity\",\"col\":3}},{\"id\":\"VOFE0hqXRD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":3}},{\"id\":\"0ik53fuemG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Analytics\",\"col\":3}},{\"id\":\"wdROEmB_XG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"-I9HhcgUKE\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"ttpROKW9vk\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"-76QPdbBHy\",\"type\":\"card\",\"data\":{\"card_name\":\"Sales Pipeline\",\"col\":4}},{\"id\":\"_YmGwzVWRr\",\"type\":\"card\",\"data\":{\"card_name\":\"Masters\",\"col\":4}},{\"id\":\"Bma1PxoXk3\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"80viA0R83a\",\"type\":\"card\",\"data\":{\"card_name\":\"Campaign\",\"col\":4}},{\"id\":\"Buo5HtKRFN\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"sLS_x4FMK2\",\"type\":\"card\",\"data\":{\"card_name\":\"Maintenance\",\"col\":4}}]",
|
||||
"content": "[{\"id\": \"5df9733229\", \"type\": \"chart\", \"data\": {\"chart_name\": \"Incoming Leads\", \"col\": 12}}, {\"id\": \"1f47c95d4e\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Open Opportunity\", \"col\": 4}}, {\"id\": \"9a0668d938\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"New Lead (Last 1 Month)\", \"col\": 4}}, {\"id\": \"096cbb7b12\", \"type\": \"number_card\", \"data\": {\"number_card_name\": \"Won Opportunity (Last 1 Month)\", \"col\": 4}}]",
|
||||
"creation": "2020-01-23 14:48:30.183272",
|
||||
"custom_blocks": [],
|
||||
"docstatus": 0,
|
||||
@@ -17,415 +17,25 @@
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "CRM",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Reports",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "Lead",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Lead Details",
|
||||
"link_count": 0,
|
||||
"link_to": "Lead Details",
|
||||
"link_type": "Report",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Sales Pipeline Analytics",
|
||||
"link_count": 0,
|
||||
"link_to": "Sales Pipeline Analytics",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Opportunity Summary by Sales Stage",
|
||||
"link_count": 0,
|
||||
"link_to": "Opportunity Summary by Sales Stage",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Sales Funnel",
|
||||
"link_count": 0,
|
||||
"link_to": "sales-funnel",
|
||||
"link_type": "Page",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Lead",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Prospects Engaged But Not Converted",
|
||||
"link_count": 0,
|
||||
"link_to": "Prospects Engaged But Not Converted",
|
||||
"link_type": "Report",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Opportunity",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "First Response Time for Opportunity",
|
||||
"link_count": 0,
|
||||
"link_to": "First Response Time for Opportunity",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Sales Order",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Inactive Customers",
|
||||
"link_count": 0,
|
||||
"link_to": "Inactive Customers",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Lead",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Campaign Efficiency",
|
||||
"link_count": 0,
|
||||
"link_to": "Campaign Efficiency",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Lead",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Lead Owner Efficiency",
|
||||
"link_count": 0,
|
||||
"link_to": "Lead Owner Efficiency",
|
||||
"link_type": "Report",
|
||||
"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": "Maintenance Schedule",
|
||||
"link_count": 0,
|
||||
"link_to": "Maintenance Schedule",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Maintenance Visit",
|
||||
"link_count": 0,
|
||||
"link_to": "Maintenance Visit",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Warranty Claim",
|
||||
"link_count": 0,
|
||||
"link_to": "Warranty Claim",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Sales Pipeline",
|
||||
"link_count": 7,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Lead",
|
||||
"link_count": 0,
|
||||
"link_to": "Lead",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Opportunity",
|
||||
"link_count": 0,
|
||||
"link_to": "Opportunity",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Customer",
|
||||
"link_count": 0,
|
||||
"link_to": "Customer",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Contract",
|
||||
"link_count": 0,
|
||||
"link_to": "Contract",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Appointment",
|
||||
"link_count": 0,
|
||||
"link_to": "Appointment",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Newsletter",
|
||||
"link_count": 0,
|
||||
"link_to": "Newsletter",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Communication",
|
||||
"link_count": 0,
|
||||
"link_to": "Communication",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Settings",
|
||||
"link_count": 2,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "CRM Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "CRM Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "SMS Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "SMS Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Campaign",
|
||||
"link_count": 5,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Campaign",
|
||||
"link_count": 0,
|
||||
"link_to": "Campaign",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Campaign",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Campaign",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "SMS Center",
|
||||
"link_count": 0,
|
||||
"link_to": "SMS Center",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "SMS Log",
|
||||
"link_count": 0,
|
||||
"link_to": "SMS Log",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Email Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Email Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Masters",
|
||||
"link_count": 7,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Territory",
|
||||
"link_count": 0,
|
||||
"link_to": "Territory",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Customer Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Customer Group",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Contact",
|
||||
"link_count": 0,
|
||||
"link_to": "Contact",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Prospect",
|
||||
"link_count": 0,
|
||||
"link_to": "Prospect",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Sales Person",
|
||||
"link_count": 0,
|
||||
"link_to": "Sales Person",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Sales Stage",
|
||||
"link_count": 0,
|
||||
"link_to": "Sales Stage",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Lead Source",
|
||||
"link_count": 0,
|
||||
"link_to": "UTM Source",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-07-03 13:44:08.297053",
|
||||
"links": [],
|
||||
"modified": "2026-08-26 18:34:03.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM",
|
||||
"number_cards": [],
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Open Opportunity",
|
||||
"number_card_name": "Open Opportunity"
|
||||
},
|
||||
{
|
||||
"label": "New Lead (Last 1 Month)",
|
||||
"number_card_name": "New Lead (Last 1 Month)"
|
||||
},
|
||||
{
|
||||
"label": "Won Opportunity (Last 1 Month)",
|
||||
"number_card_name": "Won Opportunity (Last 1 Month)"
|
||||
}
|
||||
],
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
@@ -433,40 +43,7 @@
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 17.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"color": "Blue",
|
||||
"format": "{} Open",
|
||||
"label": "Lead",
|
||||
"link_to": "Lead",
|
||||
"stats_filter": "{\"status\":\"Open\"}",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"color": "Blue",
|
||||
"format": "{} Assigned",
|
||||
"label": "Opportunity",
|
||||
"link_to": "Opportunity",
|
||||
"stats_filter": "{\"_assign\": [\"like\", '%' + frappe.session.user + '%']}",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Customer",
|
||||
"link_to": "Customer",
|
||||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "Sales Analytics",
|
||||
"link_to": "Sales Analytics",
|
||||
"report_ref_doctype": "Sales Order",
|
||||
"type": "Report"
|
||||
},
|
||||
{
|
||||
"label": "Dashboard",
|
||||
"link_to": "CRM",
|
||||
"type": "Dashboard"
|
||||
}
|
||||
],
|
||||
"shortcuts": [],
|
||||
"sidebar_items": [
|
||||
{
|
||||
"child": 0,
|
||||
|
||||
183
erpnext/dock/erpnext/erpnext.json
Normal file
183
erpnext/dock/erpnext/erpnext.json
Normal file
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-26 23:30:00",
|
||||
"docstatus": 0,
|
||||
"doctype": "Dock",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "landmark",
|
||||
"link_to": "Accounts",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Accounts"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "handshake",
|
||||
"link_to": "CRM",
|
||||
"link_type": "Sidebar",
|
||||
"title": "CRM"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "shopping-cart",
|
||||
"link_to": "Buying",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Buying"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "folder-kanban",
|
||||
"link_to": "Projects",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Projects"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "store",
|
||||
"link_to": "Selling",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Selling"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "sliders-horizontal",
|
||||
"link_to": "Setup",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Setup"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "building-2",
|
||||
"link_to": "Manufacturing",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Manufacturing"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "package",
|
||||
"link_to": "Stock",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Stock"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "headset",
|
||||
"link_to": "Support",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Support"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "pocket-knife",
|
||||
"link_to": "Utilities",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Utilities"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "archive",
|
||||
"link_to": "Assets",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Assets"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "panels-top-left",
|
||||
"link_to": "Portal",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Portal"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "wrench",
|
||||
"link_to": "Maintenance",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Maintenance"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "globe",
|
||||
"link_to": "Regional",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Regional"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "plug",
|
||||
"link_to": "ERPNext Integrations",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Integrations"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "shield-check",
|
||||
"link_to": "Quality",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Quality"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "messages-square",
|
||||
"link_to": "Communication",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Communication"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "phone",
|
||||
"link_to": "Telephony",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Telephony"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "layers",
|
||||
"link_to": "Bulk Transaction",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Bulk Transaction"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "factory",
|
||||
"link_to": "Subcontracting",
|
||||
"link_type": "Sidebar",
|
||||
"title": "Subcontracting"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"hidden": 0,
|
||||
"icon": "file-code",
|
||||
"link_to": "EDI",
|
||||
"link_type": "Sidebar",
|
||||
"title": "EDI"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-28 12:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"name": "erpnext",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"user": ""
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import frappe
|
||||
@@ -51,12 +52,9 @@ SAMPLE_GENERICODE = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
class TestCodeListImport(ERPNextTestSuite):
|
||||
def test_import_genericode_rejects_remote_file_url(self):
|
||||
self.set_upload_context(
|
||||
file_name="trusted.xml",
|
||||
file_url="https://example.com/codelists/trusted.xml",
|
||||
)
|
||||
|
||||
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
|
||||
with self.upload_context(
|
||||
file_name="trusted.xml", file_url="https://example.com/codelists/trusted.xml"
|
||||
), patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
|
||||
):
|
||||
@@ -65,12 +63,9 @@ class TestCodeListImport(ERPNextTestSuite):
|
||||
mock_get.assert_not_called()
|
||||
|
||||
def test_import_genericode_rejects_file_scheme_url(self):
|
||||
self.set_upload_context(
|
||||
file_name="trusted.xml",
|
||||
file_url="file:///tmp/trusted.xml",
|
||||
)
|
||||
|
||||
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
|
||||
with self.upload_context(file_name="trusted.xml", file_url="file:///tmp/trusted.xml"), patch(
|
||||
"erpnext.edi.doctype.code_list.code_list_import.requests.get"
|
||||
) as mock_get:
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
|
||||
):
|
||||
@@ -110,36 +105,34 @@ class TestCodeListImport(ERPNextTestSuite):
|
||||
code_list_import.import_genericode_from_url("https://example.com/codelists/trusted.xml")
|
||||
|
||||
def test_import_genericode_from_uploaded_file_returns_metadata(self):
|
||||
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
|
||||
with self.upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml"):
|
||||
import_result = code_list_import.import_genericode()
|
||||
|
||||
import_result = code_list_import.import_genericode()
|
||||
self.assert_import_response(import_result)
|
||||
|
||||
self.assert_import_response(import_result)
|
||||
|
||||
file_doc = frappe.get_doc("File", import_result["file"])
|
||||
self.assertEqual(file_doc.get_content(encodings=()), SAMPLE_GENERICODE)
|
||||
file_doc = frappe.get_doc("File", import_result["file"])
|
||||
self.assertEqual(file_doc.get_content(encodings=()), SAMPLE_GENERICODE)
|
||||
|
||||
def test_process_genericode_import_reads_file_doc_content(self):
|
||||
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
|
||||
with self.upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml"):
|
||||
import_result = code_list_import.import_genericode()
|
||||
count = code_list_import.process_genericode_import(
|
||||
code_list_name=import_result["code_list"],
|
||||
file_name=import_result["file"],
|
||||
code_column="code",
|
||||
title_column="name",
|
||||
)
|
||||
|
||||
import_result = code_list_import.import_genericode()
|
||||
count = code_list_import.process_genericode_import(
|
||||
code_list_name=import_result["code_list"],
|
||||
file_name=import_result["file"],
|
||||
code_column="code",
|
||||
title_column="name",
|
||||
)
|
||||
|
||||
self.assertEqual(count, 3)
|
||||
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Common Code",
|
||||
{"code_list": import_result["code_list"], "common_code": "A"},
|
||||
"title",
|
||||
),
|
||||
"Alpha",
|
||||
)
|
||||
self.assertEqual(count, 3)
|
||||
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Common Code",
|
||||
{"code_list": import_result["code_list"], "common_code": "A"},
|
||||
"title",
|
||||
),
|
||||
"Alpha",
|
||||
)
|
||||
|
||||
def test_import_genericode_from_local_file_url(self):
|
||||
source_file = frappe.get_doc(
|
||||
@@ -150,32 +143,38 @@ class TestCodeListImport(ERPNextTestSuite):
|
||||
"is_private": 1,
|
||||
}
|
||||
).insert()
|
||||
self.set_upload_context(file_name=source_file.file_name, file_url=source_file.file_url)
|
||||
with self.upload_context(file_name=source_file.file_name, file_url=source_file.file_url):
|
||||
import_result = code_list_import.import_genericode()
|
||||
|
||||
import_result = code_list_import.import_genericode()
|
||||
self.assert_import_response(import_result)
|
||||
|
||||
self.assert_import_response(import_result)
|
||||
|
||||
def set_upload_context(
|
||||
self,
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def upload_context(
|
||||
content: bytes | None = None,
|
||||
file_name: str = "genericode.xml",
|
||||
file_url: str | None = None,
|
||||
docname: str | None = None,
|
||||
):
|
||||
attrs = ("form_dict", "uploaded_file", "uploaded_file_url", "uploaded_filename")
|
||||
originals = {attr: getattr(frappe.local, attr, None) for attr in attrs}
|
||||
missing = object()
|
||||
attrs = {
|
||||
"form_dict": frappe._dict(doctype="Code List", docname=docname),
|
||||
"uploaded_file": content,
|
||||
"uploaded_file_url": file_url,
|
||||
"uploaded_filename": file_name,
|
||||
}
|
||||
originals = {key: getattr(frappe.local, key, missing) for key in attrs}
|
||||
for key, value in attrs.items():
|
||||
setattr(frappe.local, key, value)
|
||||
|
||||
frappe.local.form_dict = frappe._dict(doctype="Code List", docname=docname)
|
||||
frappe.local.uploaded_file = content
|
||||
frappe.local.uploaded_file_url = file_url
|
||||
frappe.local.uploaded_filename = file_name
|
||||
|
||||
def restore():
|
||||
for attr, value in originals.items():
|
||||
setattr(frappe.local, attr, value)
|
||||
|
||||
self.addCleanup(restore)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for key, value in originals.items():
|
||||
if value is missing:
|
||||
delattr(frappe.local, key)
|
||||
else:
|
||||
setattr(frappe.local, key, value)
|
||||
|
||||
def assert_import_response(self, import_result):
|
||||
self.assertEqual(
|
||||
|
||||
48
erpnext/edi/sidebar/edi/edi.json
Normal file
48
erpnext/edi/sidebar/edi/edi.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-16 00:00:00.000000",
|
||||
"docstatus": 0,
|
||||
"doctype": "Sidebar",
|
||||
"header_icon": "file-code",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Code List",
|
||||
"link_to": "Code List",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Common Code",
|
||||
"link_to": "Common Code",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-16 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "EDI",
|
||||
"owner": "Administrator",
|
||||
"sequence_id": 0.0,
|
||||
"standard": 1,
|
||||
"title": "EDI"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"app": "erpnext",
|
||||
"creation": "2026-08-16 00:00:00.000000",
|
||||
"docstatus": 0,
|
||||
"doctype": "Sidebar",
|
||||
"header_icon": "plug",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"added": 0,
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"hidden": 0,
|
||||
"icon": "settings",
|
||||
"indent": 0,
|
||||
"is_default_module": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Plaid Settings",
|
||||
"link_to": "Plaid Settings",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-08-16 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "ERPNext Integrations",
|
||||
"owner": "Administrator",
|
||||
"sequence_id": 0.0,
|
||||
"standard": 1,
|
||||
"title": "ERPNext Integrations"
|
||||
}
|
||||
@@ -595,6 +595,7 @@ accounting_dimension_doctypes = [
|
||||
"Purchase Taxes and Charges",
|
||||
"Shipping Rule",
|
||||
"Landed Cost Item",
|
||||
"Landed Cost Taxes and Charges",
|
||||
"Asset Value Adjustment",
|
||||
"Asset Repair",
|
||||
"Asset Capitalization",
|
||||
|
||||
1441
erpnext/locale/ar.po
1441
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1441
erpnext/locale/bg.po
1441
erpnext/locale/bg.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user