mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-04 14:10:52 +00:00
Compare commits
3 Commits
chore/test
...
l10n_devel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abff82a4b2 | ||
|
|
3a63c74382 | ||
|
|
6bd2f29ab5 |
@@ -1,59 +1,10 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
aggregate_with_last_account_closing_balance,
|
||||
generate_key,
|
||||
)
|
||||
# import frappe
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
def entry(**overrides):
|
||||
row = {"debit": 0, "credit": 0, "debit_in_account_currency": 0, "credit_in_account_currency": 0}
|
||||
row.update(overrides)
|
||||
return row
|
||||
|
||||
|
||||
class TestAccountClosingBalance(ERPNextTestSuite):
|
||||
"""The closing-balance snapshot is built by merging this period's entries with the
|
||||
previous period's. These lock the merge/key logic that drives that carry-forward."""
|
||||
|
||||
def test_matching_entries_are_summed(self):
|
||||
# this is how a prior-period balance carries forward into the current one
|
||||
merged = aggregate_with_last_account_closing_balance(
|
||||
[
|
||||
entry(account="Cash - _TC", debit=100, debit_in_account_currency=100),
|
||||
entry(
|
||||
account="Cash - _TC",
|
||||
debit=50,
|
||||
credit=20,
|
||||
debit_in_account_currency=50,
|
||||
credit_in_account_currency=20,
|
||||
),
|
||||
],
|
||||
[],
|
||||
)
|
||||
self.assertEqual(len(merged), 1)
|
||||
row = next(iter(merged.values()))
|
||||
self.assertEqual(row["debit"], 150)
|
||||
self.assertEqual(row["credit"], 20)
|
||||
# the account-currency columns are accumulated in the same pass
|
||||
self.assertEqual(row["debit_in_account_currency"], 150)
|
||||
self.assertEqual(row["credit_in_account_currency"], 20)
|
||||
|
||||
def test_entries_are_kept_separate_per_dimension(self):
|
||||
merged = aggregate_with_last_account_closing_balance(
|
||||
[
|
||||
entry(account="Cash - _TC", cost_center="CC1", debit=100, debit_in_account_currency=100),
|
||||
entry(account="Cash - _TC", cost_center="CC2", debit=40, debit_in_account_currency=40),
|
||||
],
|
||||
[],
|
||||
)
|
||||
self.assertEqual(len(merged), 2)
|
||||
|
||||
def test_period_closing_flag_is_part_of_the_key(self):
|
||||
# a P&L reversal (flag 0) and a closing-account entry (flag 1) for the same
|
||||
# account must not merge, so the flag has to distinguish their keys
|
||||
key_reversal, _ = generate_key(entry(account="Sales - _TC", is_period_closing_voucher_entry=0), [])
|
||||
key_closing, _ = generate_key(entry(account="Sales - _TC", is_period_closing_voucher_entry=1), [])
|
||||
self.assertNotEqual(key_reversal, key_closing)
|
||||
pass
|
||||
|
||||
@@ -6,7 +6,7 @@ frappe.ui.form.on("Accounting Dimension Filter", {
|
||||
let help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
<tr><td>
|
||||
<p>
|
||||
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
{{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
|
||||
</p>
|
||||
</td></tr>
|
||||
|
||||
@@ -188,7 +188,7 @@ def get_closing_balance_as_per_statement(bank_account: str, date: str):
|
||||
return {"balance": 0, "date": None}
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def set_closing_balance_as_per_statement(bank_account: str, date: str | datetime.date, balance: float):
|
||||
"""
|
||||
Set the closing balance as per statement for a bank account and date
|
||||
|
||||
@@ -1,76 +1,8 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.bank_guarantee.bank_guarantee import get_voucher_details
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
BANK = "_Test BG Bank"
|
||||
|
||||
|
||||
class TestBankGuarantee(ERPNextTestSuite):
|
||||
"""Bank Guarantee records a guarantee issued/received against a customer or
|
||||
supplier. validate() needs a party; on_submit() needs the bank details filled in."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
if not frappe.db.exists("Bank", BANK):
|
||||
frappe.get_doc({"doctype": "Bank", "bank_name": BANK}).insert()
|
||||
|
||||
def make_bg(self, **args):
|
||||
args = frappe._dict(args)
|
||||
doc = frappe.new_doc("Bank Guarantee")
|
||||
doc.bg_type = args.bg_type or "Receiving"
|
||||
doc.amount = args.amount if args.amount is not None else 1000
|
||||
doc.start_date = args.start_date or "2026-06-01"
|
||||
if args.end_date:
|
||||
doc.end_date = args.end_date
|
||||
doc.customer = args.get("customer", "_Test Customer")
|
||||
doc.supplier = args.get("supplier")
|
||||
# fields on_submit requires — present by default, cleared per-test to assert the guard
|
||||
doc.bank_guarantee_number = args.get("bank_guarantee_number", "BG-001")
|
||||
doc.name_of_beneficiary = args.get("name_of_beneficiary", "Test Beneficiary")
|
||||
doc.bank = args.get("bank", BANK)
|
||||
return doc
|
||||
|
||||
def test_validate_requires_customer_or_supplier(self):
|
||||
doc = self.make_bg(customer=None)
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_submit_requires_guarantee_number(self):
|
||||
doc = self.make_bg(bank_guarantee_number="")
|
||||
doc.insert()
|
||||
self.assertRaises(frappe.ValidationError, doc.submit)
|
||||
|
||||
def test_submit_requires_beneficiary_name(self):
|
||||
doc = self.make_bg(name_of_beneficiary="")
|
||||
doc.insert()
|
||||
self.assertRaises(frappe.ValidationError, doc.submit)
|
||||
|
||||
def test_submit_requires_bank(self):
|
||||
doc = self.make_bg(bank="")
|
||||
doc.insert()
|
||||
self.assertRaises(frappe.ValidationError, doc.submit)
|
||||
|
||||
def test_valid_guarantee_submits(self):
|
||||
doc = self.make_bg()
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
self.assertEqual(frappe.db.get_value("Bank Guarantee", doc.name, "docstatus"), 1)
|
||||
|
||||
def test_get_voucher_details_for_receiving(self):
|
||||
so = make_sales_order()
|
||||
details = get_voucher_details("Receiving", so.name)
|
||||
self.assertEqual(details.customer, so.customer)
|
||||
self.assertEqual(flt(details.grand_total), flt(so.grand_total))
|
||||
|
||||
def test_end_date_before_start_date_is_not_validated(self):
|
||||
# SUSPECTED BUG: validate() never checks that end_date >= start_date, so a
|
||||
# guarantee that expires before it starts saves cleanly. Locking the current
|
||||
# (wrong) behaviour so a future fix that adds the check trips this test.
|
||||
doc = self.make_bg(start_date="2026-06-30", end_date="2026-06-01")
|
||||
doc.insert()
|
||||
self.assertTrue(frappe.db.exists("Bank Guarantee", doc.name))
|
||||
pass
|
||||
|
||||
@@ -116,7 +116,7 @@ def get_account_balance(bank_account: str, till_date: str | date, company: str):
|
||||
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def update_bank_transaction(
|
||||
bank_transaction_name: str, reference_number: str, party_type: str | None = None, party: str | None = None
|
||||
):
|
||||
@@ -146,7 +146,7 @@ def update_bank_transaction(
|
||||
)[0]
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def create_journal_entry_bts(
|
||||
bank_transaction_name: str,
|
||||
reference_number: str | None = None,
|
||||
@@ -305,7 +305,7 @@ def create_journal_entry_bts(
|
||||
return reconcile_vouchers(bank_transaction_name, vouchers, is_new_voucher=True)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def create_payment_entry_bts(
|
||||
bank_transaction_name: str,
|
||||
reference_number: str | None = None,
|
||||
@@ -500,7 +500,7 @@ def create_bulk_internal_transfer(bank_transaction_names: list[str | int], bank_
|
||||
return output
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def create_internal_transfer(
|
||||
bank_transaction_name: str | int,
|
||||
posting_date: str | date,
|
||||
@@ -1057,7 +1057,7 @@ def get_auto_reconcile_message(partially_reconciled, reconciled):
|
||||
return alert_message, indicator
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str | list, is_new_voucher: bool = False):
|
||||
# updated clear date of all the vouchers based on the bank transaction
|
||||
vouchers = frappe.parse_json(vouchers)
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
auto_reconcile_vouchers,
|
||||
get_auto_reconcile_message,
|
||||
get_bank_transactions,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
@@ -98,40 +97,3 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
# assert API output post reconciliation
|
||||
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||
self.assertEqual(len(transactions), 0)
|
||||
|
||||
def make_bank_transaction(self, date, deposit=100):
|
||||
return (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"date": date,
|
||||
"deposit": deposit,
|
||||
"bank_account": self.bank_account,
|
||||
"currency": "INR",
|
||||
}
|
||||
)
|
||||
.save()
|
||||
.submit()
|
||||
)
|
||||
|
||||
def test_get_bank_transactions_excludes_dates_after_to_date(self):
|
||||
self.make_bank_transaction(date=today())
|
||||
names = [t.name for t in get_bank_transactions(self.bank_account, to_date=add_days(today(), -1))]
|
||||
self.assertEqual(names, [])
|
||||
|
||||
def test_auto_reconcile_message_for_no_matches(self):
|
||||
message, indicator = get_auto_reconcile_message([], [])
|
||||
self.assertEqual(indicator, "blue")
|
||||
self.assertIn("No matches", message)
|
||||
|
||||
def test_auto_reconcile_message_counts_and_pluralizes(self):
|
||||
# reconciled count is reported and the indicator turns green
|
||||
message, indicator = get_auto_reconcile_message([], ["t1", "t2"])
|
||||
self.assertEqual(indicator, "green")
|
||||
self.assertIn("2 Transaction(s) Reconciled", message)
|
||||
|
||||
# partially-reconciled label is singular for one, plural for many
|
||||
singular, _ = get_auto_reconcile_message(["p1"], [])
|
||||
self.assertIn("1 Transaction Partially Reconciled", singular)
|
||||
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
|
||||
self.assertIn("2 Transactions Partially Reconciled", plural)
|
||||
|
||||
@@ -397,7 +397,7 @@ def unreconcile_transaction(transaction_name: str | int):
|
||||
frappe.get_doc(voucher["doctype"], voucher["name"]).cancel()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def unreconcile_transaction_entry(bank_transaction_id: str | int, voucher_type: str, voucher_id: str | int):
|
||||
"""
|
||||
Removes a single payment entry from a bank transaction - for example only undoing one voucher instead of undoing the entire transaction
|
||||
|
||||
@@ -34,7 +34,7 @@ def upload_bank_statement():
|
||||
return {"columns": columns, "data": data}
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def create_bank_entries(columns: str, data: str | list, bank_account: str):
|
||||
header_map = get_header_mapping(columns, bank_account)
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ class BisectAccountingStatements(Document):
|
||||
self.get_report_summary()
|
||||
self.update_node()
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def bisect_left(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
@@ -198,7 +198,7 @@ class BisectAccountingStatements(Document):
|
||||
else:
|
||||
frappe.msgprint(_("No more children on Left"))
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def bisect_right(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
@@ -212,7 +212,7 @@ class BisectAccountingStatements(Document):
|
||||
else:
|
||||
frappe.msgprint(_("No more children on Right"))
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def move_up(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
|
||||
@@ -1,47 +1,11 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import datetime
|
||||
# import frappe
|
||||
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestBisectAccountingStatements(ERPNextTestSuite):
|
||||
"""The tool bisects a date range into a tree of Bisect Nodes down to single days.
|
||||
These cover the date validation and that the bisection cleanly partitions the range."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.delete("Bisect Nodes")
|
||||
|
||||
def _leaf_days(self):
|
||||
leaves = frappe.get_all(
|
||||
"Bisect Nodes",
|
||||
filters={"left_child": ["is", "not set"]},
|
||||
fields=["period_from_date", "period_to_date"],
|
||||
)
|
||||
# every leaf spans a single day
|
||||
for leaf in leaves:
|
||||
self.assertEqual(getdate(leaf.period_from_date), getdate(leaf.period_to_date))
|
||||
return sorted(getdate(leaf.period_from_date) for leaf in leaves)
|
||||
|
||||
def test_validate_dates_rejects_reversed_range(self):
|
||||
doc = frappe.new_doc("Bisect Accounting Statements")
|
||||
doc.from_date = "2026-01-08"
|
||||
doc.to_date = "2026-01-01"
|
||||
self.assertRaises(frappe.ValidationError, doc.validate)
|
||||
|
||||
def test_bfs_partitions_range_into_single_days(self):
|
||||
doc = frappe.new_doc("Bisect Accounting Statements")
|
||||
doc.bfs(datetime.datetime(2026, 1, 1), datetime.datetime(2026, 1, 8))
|
||||
|
||||
# the 8-day span Jan 1..Jan 8 becomes exactly 8 contiguous single-day leaves
|
||||
self.assertEqual(self._leaf_days(), [getdate(f"2026-01-0{n}") for n in range(1, 9)])
|
||||
|
||||
def test_dfs_produces_the_same_partition_as_bfs(self):
|
||||
doc = frappe.new_doc("Bisect Accounting Statements")
|
||||
doc.dfs(datetime.datetime(2026, 1, 1), datetime.datetime(2026, 1, 8))
|
||||
self.assertEqual(self._leaf_days(), [getdate(f"2026-01-0{n}") for n in range(1, 9)])
|
||||
pass
|
||||
|
||||
@@ -878,7 +878,7 @@ def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
|
||||
return from_year.year_start_date, to_year.year_end_date
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def revise_budget(budget_name: str):
|
||||
old_budget = frappe.get_doc("Budget", budget_name)
|
||||
|
||||
|
||||
@@ -1,67 +1,8 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
DATE = "2026-06-15"
|
||||
|
||||
|
||||
class TestCashierClosing(ERPNextTestSuite):
|
||||
"""Cashier Closing reconciles a shift: it pulls outstanding invoices in a
|
||||
date/time window and rolls payments, expense, custody and returns into net_amount."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_invoice_in_window(self, rate=100):
|
||||
si = create_sales_invoice(rate=rate, qty=1, posting_date=DATE, do_not_submit=True)
|
||||
si.posting_time = "10:30:00"
|
||||
si.submit()
|
||||
si.reload() # read outstanding_amount as persisted after submit
|
||||
return si
|
||||
|
||||
def make_closing(self, user="Administrator", payments=None, **args):
|
||||
doc = frappe.new_doc("Cashier Closing")
|
||||
doc.user = user
|
||||
doc.date = args.get("date", DATE)
|
||||
doc.from_time = args.get("from_time", "09:00:00")
|
||||
doc.time = args.get("time", "18:00:00")
|
||||
for amount in payments or []:
|
||||
doc.append("payments", {"mode_of_payment": "Cash", "amount": amount})
|
||||
doc.expense = args.get("expense", 0)
|
||||
doc.custody = args.get("custody", 0)
|
||||
doc.returns = args.get("returns", 0)
|
||||
return doc
|
||||
|
||||
def test_from_time_must_be_before_to_time(self):
|
||||
doc = self.make_closing(from_time="18:00:00", time="09:00:00")
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_equal_from_and_to_time_is_rejected(self):
|
||||
# validate_time uses >=, so a zero-length window is also blocked
|
||||
doc = self.make_closing(from_time="09:00:00", time="09:00:00")
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_net_amount_rolls_up_outstanding_and_adjustments(self):
|
||||
si = self.make_invoice_in_window(rate=100)
|
||||
doc = self.make_closing(payments=[500], expense=50, custody=30, returns=20)
|
||||
doc.save()
|
||||
|
||||
# the in-window invoice is picked up as outstanding
|
||||
self.assertEqual(doc.outstanding_amount, si.outstanding_amount)
|
||||
# net = payments + outstanding + expense - custody + returns
|
||||
self.assertEqual(doc.net_amount, 500 + si.outstanding_amount + 50 - 30 + 20)
|
||||
|
||||
def test_outstanding_is_scoped_to_the_invoice_owner(self):
|
||||
# The invoice is created by Administrator; a closing for a different user does
|
||||
# not see it. NOTE: get_outstanding keys on Sales Invoice.owner (the document
|
||||
# creator) rather than an explicit cashier/POS-user field, which is fragile when
|
||||
# invoices are created by a shared or system user.
|
||||
self.make_invoice_in_window(rate=100)
|
||||
doc = self.make_closing(user="Guest", payments=[500])
|
||||
doc.save()
|
||||
self.assertEqual(doc.outstanding_amount, 0)
|
||||
self.assertEqual(doc.net_amount, 500)
|
||||
pass
|
||||
|
||||
@@ -1,54 +1,8 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import (
|
||||
build_forest,
|
||||
validate_columns,
|
||||
validate_missing_roots,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
# columns: account_name, parent_account, account_number, parent_account_number,
|
||||
# is_group, account_type, root_type, account_currency
|
||||
ROOT = ["Assets", "Assets", "", "", 1, "", "Asset", "INR"]
|
||||
CHILD = ["Cash", "Assets", "", "", 0, "Cash", "Asset", "INR"]
|
||||
|
||||
|
||||
class TestChartofAccountsImporter(ERPNextTestSuite):
|
||||
"""The importer parses an uploaded CoA into a nested tree and validates its
|
||||
shape. These cover the parsing/validation helpers without a file upload."""
|
||||
|
||||
def test_validate_columns_rejects_blank_file(self):
|
||||
self.assertRaises(frappe.ValidationError, validate_columns, [])
|
||||
|
||||
def test_validate_columns_requires_eight_columns(self):
|
||||
self.assertRaises(frappe.ValidationError, validate_columns, [["a", "b", "c"]])
|
||||
# the standard template width passes
|
||||
validate_columns([ROOT])
|
||||
|
||||
def test_build_forest_nests_child_under_parent(self):
|
||||
forest = build_forest([ROOT, CHILD])
|
||||
self.assertIn("Assets", forest)
|
||||
self.assertIn("Cash", forest["Assets"])
|
||||
|
||||
def test_build_forest_rejects_unknown_parent(self):
|
||||
orphan = ["Cash", "Missing Parent", "", "", 0, "Cash", "Asset", "INR"]
|
||||
self.assertRaises(frappe.ValidationError, build_forest, [orphan])
|
||||
|
||||
def test_build_forest_requires_account_name(self):
|
||||
nameless = ["", "Assets", "", "", 0, "Cash", "Asset", "INR"]
|
||||
self.assertRaises(frappe.ValidationError, build_forest, [ROOT, nameless])
|
||||
|
||||
def test_validate_missing_roots_requires_all_root_types(self):
|
||||
present = ("Asset", "Liability", "Expense", "Income") # Equity missing
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
validate_missing_roots,
|
||||
[{"root_type": rt} for rt in present],
|
||||
)
|
||||
# all five root types present -> no error
|
||||
validate_missing_roots(
|
||||
[{"root_type": rt} for rt in ("Asset", "Liability", "Expense", "Income", "Equity")]
|
||||
)
|
||||
pass
|
||||
|
||||
@@ -46,7 +46,7 @@ class ChequePrintTemplate(Document):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def create_or_update_cheque_print_format(template_name: str):
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
|
||||
@@ -298,65 +298,3 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
|
||||
|
||||
for key, _val in expected_data.items():
|
||||
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||
|
||||
|
||||
class TestExchangeRateRevaluationValidation(ERPNextTestSuite):
|
||||
"""Validation and gain/loss calculation paths, exercised on the document directly
|
||||
so they don't need the multi-currency GL setup the integration tests above build."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
self.company = "_Test Company"
|
||||
|
||||
def _revaluation_with_rows(self, rows, rounding_loss_allowance=0.05):
|
||||
doc = frappe.new_doc("Exchange Rate Revaluation")
|
||||
doc.company = self.company
|
||||
doc.posting_date = today()
|
||||
doc.rounding_loss_allowance = rounding_loss_allowance
|
||||
for row in rows:
|
||||
doc.append("accounts", row)
|
||||
return doc
|
||||
|
||||
def test_rounding_loss_allowance_must_be_between_0_and_1(self):
|
||||
for bad in (-0.1, 1, 1.5):
|
||||
doc = self._revaluation_with_rows([], rounding_loss_allowance=bad)
|
||||
self.assertRaises(frappe.ValidationError, doc.validate)
|
||||
# values inside [0, 1) are accepted, at the lower bound and mid-range
|
||||
for good in (0.0, 0.5):
|
||||
self._revaluation_with_rows([], rounding_loss_allowance=good).validate()
|
||||
|
||||
def test_gain_loss_computed_and_split_by_zero_balance(self):
|
||||
doc = self._revaluation_with_rows(
|
||||
[
|
||||
# open (unbooked) row: base balance moved 1000 -> 1100, a 100 gain
|
||||
{"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100},
|
||||
# already-settled (zero_balance) row carries a booked loss of 40
|
||||
{"zero_balance": 1, "gain_loss": -40},
|
||||
]
|
||||
)
|
||||
doc.validate()
|
||||
|
||||
# gain_loss is derived only for open rows; the zero-balance row keeps its value
|
||||
self.assertEqual(doc.accounts[0].gain_loss, 100)
|
||||
self.assertEqual(doc.gain_loss_unbooked, 100)
|
||||
self.assertEqual(doc.gain_loss_booked, -40)
|
||||
self.assertEqual(doc.total_gain_loss, 60)
|
||||
|
||||
def test_before_submit_drops_rows_without_gain_loss(self):
|
||||
doc = self._revaluation_with_rows(
|
||||
[
|
||||
{"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100},
|
||||
{"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500},
|
||||
]
|
||||
)
|
||||
doc.validate() # second row nets to a 0 gain_loss
|
||||
doc.remove_accounts_without_gain_loss()
|
||||
self.assertEqual(len(doc.accounts), 1)
|
||||
self.assertEqual(doc.accounts[0].gain_loss, 100)
|
||||
|
||||
def test_before_submit_requires_at_least_one_gain_loss_row(self):
|
||||
doc = self._revaluation_with_rows(
|
||||
[{"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500}]
|
||||
)
|
||||
doc.validate()
|
||||
self.assertRaises(frappe.ValidationError, doc.remove_accounts_without_gain_loss)
|
||||
|
||||
@@ -1,62 +1,8 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
TAX_ACCOUNT = "_Test Account VAT - _TC"
|
||||
RECEIVABLE_ACCOUNT = "Debtors - _TC"
|
||||
|
||||
|
||||
class TestItemTaxTemplate(ERPNextTestSuite):
|
||||
"""Item Tax Template validates its tax rows: each account must belong to the
|
||||
company, be a tax-like account type, and appear only once."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_template(self, rows, title="_Test ITT"):
|
||||
doc = frappe.new_doc("Item Tax Template")
|
||||
doc.title = f"{title} {frappe.generate_hash(length=6)}"
|
||||
doc.company = COMPANY
|
||||
for account, rate, not_applicable in rows:
|
||||
doc.append(
|
||||
"taxes",
|
||||
{"tax_type": account, "tax_rate": rate, "not_applicable": not_applicable},
|
||||
)
|
||||
return doc
|
||||
|
||||
def test_valid_template_saves_and_is_named_with_abbr(self):
|
||||
doc = self.make_template([(TAX_ACCOUNT, 9, 0)])
|
||||
doc.insert()
|
||||
self.assertTrue(doc.name.endswith(" - _TC"))
|
||||
self.assertTrue(doc.name.startswith(doc.title))
|
||||
|
||||
def test_duplicate_tax_type_throws(self):
|
||||
doc = self.make_template([(TAX_ACCOUNT, 9, 0), (TAX_ACCOUNT, 5, 0)])
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_account_of_wrong_company_throws(self):
|
||||
other_account = frappe.db.get_value("Account", {"company": "_Test Company 1", "is_group": 0}, "name")
|
||||
self.assertTrue(other_account, "need a non-group account in _Test Company 1")
|
||||
doc = self.make_template([(other_account, 9, 0)])
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_disallowed_account_type_throws(self):
|
||||
# a Receivable account is not Tax/Chargeable/Income/Expense
|
||||
doc = self.make_template([(RECEIVABLE_ACCOUNT, 9, 0)])
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_not_applicable_row_has_rate_zeroed(self):
|
||||
doc = self.make_template([(TAX_ACCOUNT, 18, 1)])
|
||||
doc.insert()
|
||||
self.assertEqual(doc.taxes[0].tax_rate, 0)
|
||||
|
||||
def test_negative_tax_rate_is_accepted(self):
|
||||
# SUSPECTED BUG: validate never bounds tax_rate, so a negative (or >100) rate
|
||||
# saves silently. Locking the current (wrong) behaviour.
|
||||
doc = self.make_template([(TAX_ACCOUNT, -5, 0)])
|
||||
doc.insert()
|
||||
self.assertEqual(doc.taxes[0].tax_rate, -5)
|
||||
pass
|
||||
|
||||
@@ -45,20 +45,6 @@ class JournalEntryTemplate(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_party()
|
||||
self.validate_account_company()
|
||||
|
||||
def validate_account_company(self):
|
||||
"""Each row's account must belong to the template's company."""
|
||||
for account in self.accounts:
|
||||
if (
|
||||
account.account
|
||||
and frappe.get_cached_value("Account", account.account, "company") != self.company
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Account {1} does not belong to company {2}").format(
|
||||
account.idx, account.account, self.company
|
||||
)
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
"""
|
||||
|
||||
@@ -1,45 +1,9 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
|
||||
|
||||
class TestJournalEntryTemplate(ERPNextTestSuite):
|
||||
"""Journal Entry Template's only real rule is validate_party: party_type is
|
||||
allowed only on Receivable/Payable accounts, and a party needs a party_type."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_template(self, rows, company=COMPANY):
|
||||
doc = frappe.new_doc("Journal Entry Template")
|
||||
doc.template_title = f"_Test JET {frappe.generate_hash(length=6)}"
|
||||
doc.company = company
|
||||
doc.voucher_type = "Journal Entry"
|
||||
doc.naming_series = frappe.get_meta("Journal Entry").get_field("naming_series").options.split("\n")[0]
|
||||
for row in rows:
|
||||
doc.append("accounts", row)
|
||||
return doc
|
||||
|
||||
def test_party_type_only_on_receivable_or_payable_account(self):
|
||||
# Cash is neither Receivable nor Payable, so a party_type here is invalid
|
||||
doc = self.make_template([{"account": "Cash - _TC", "party_type": "Customer"}])
|
||||
self.assertRaises(frappe.ValidationError, doc.validate)
|
||||
|
||||
def test_party_requires_party_type(self):
|
||||
doc = self.make_template([{"account": "Debtors - _TC", "party": "_Test Customer"}])
|
||||
self.assertRaises(frappe.ValidationError, doc.validate)
|
||||
|
||||
def test_account_from_other_company_is_rejected(self):
|
||||
other_receivable = frappe.db.get_value(
|
||||
"Account", {"company": "_Test Company 1", "account_type": "Receivable", "is_group": 0}, "name"
|
||||
)
|
||||
self.assertTrue(other_receivable, "need a receivable account in _Test Company 1")
|
||||
doc = self.make_template(
|
||||
[{"account": other_receivable, "party_type": "Customer", "party": "_Test Customer"}]
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
pass
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.ui.form.on("Loyalty Program", {
|
||||
var help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
<tr><td>
|
||||
<h4>
|
||||
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
${__("Notes")}
|
||||
</h4>
|
||||
<ul>
|
||||
|
||||
@@ -5,59 +5,9 @@ import frappe
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
|
||||
|
||||
class TestModeofPayment(ERPNextTestSuite):
|
||||
"""Mode of Payment validates its per-company default accounts (account company
|
||||
must match the row, no company twice) and blocks disabling while a POS Profile
|
||||
still references it."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_mop(self, accounts=None, enabled=1):
|
||||
doc = frappe.new_doc("Mode of Payment")
|
||||
doc.mode_of_payment = f"_Test MoP {frappe.generate_hash(length=6)}"
|
||||
doc.type = "General"
|
||||
doc.enabled = enabled
|
||||
for company, account in accounts or []:
|
||||
doc.append("accounts", {"company": company, "default_account": account})
|
||||
return doc
|
||||
|
||||
def test_valid_mode_of_payment_saves(self):
|
||||
doc = self.make_mop(accounts=[(COMPANY, "Cash - _TC")])
|
||||
doc.insert()
|
||||
self.assertTrue(doc.name)
|
||||
|
||||
def test_account_of_wrong_company_throws(self):
|
||||
other_account = frappe.db.get_value("Account", {"company": "_Test Company 1", "is_group": 0}, "name")
|
||||
self.assertTrue(other_account, "need a non-group account in _Test Company 1")
|
||||
doc = self.make_mop(accounts=[(COMPANY, other_account)])
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_repeating_company_throws(self):
|
||||
doc = self.make_mop(accounts=[(COMPANY, "Cash - _TC"), (COMPANY, "Debtors - _TC")])
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_disabling_mode_referenced_by_pos_profile_is_not_blocked(self):
|
||||
# SUSPECTED BUG: validate_pos_mode_of_payment queries "Sales Invoice Payment"
|
||||
# rows with parenttype "POS Profile", but a POS Profile's payments are stored
|
||||
# as "POS Payment Method" rows. The filter never matches, so the guard is dead
|
||||
# and a mode still referenced by a POS Profile disables without complaint.
|
||||
# Locking the current (wrong) behaviour so a fix to the guard trips this test.
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
|
||||
make_pos_profile() # its payments row references the "Cash" mode of payment
|
||||
cash = frappe.get_doc("Mode of Payment", "Cash")
|
||||
cash.enabled = 0
|
||||
cash.save()
|
||||
self.assertEqual(frappe.db.get_value("Mode of Payment", "Cash", "enabled"), 0)
|
||||
|
||||
def test_disabling_unreferenced_mode_succeeds(self):
|
||||
doc = self.make_mop(accounts=[(COMPANY, "Cash - _TC")], enabled=0)
|
||||
doc.insert()
|
||||
self.assertEqual(doc.enabled, 0)
|
||||
pass
|
||||
|
||||
|
||||
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):
|
||||
|
||||
@@ -1,67 +1,8 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.doctype.monthly_distribution.monthly_distribution import (
|
||||
get_percentage,
|
||||
get_periodwise_distribution_data,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestMonthlyDistribution(ERPNextTestSuite):
|
||||
"""Monthly Distribution spreads an amount across months. validate() enforces a
|
||||
100% total; get_percentage() sums the months that fall inside a period window."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_distribution(self, allocations):
|
||||
doc = frappe.new_doc("Monthly Distribution")
|
||||
doc.distribution_id = f"_Test MD {frappe.generate_hash(length=6)}"
|
||||
for month, pct in allocations:
|
||||
doc.append("percentages", {"month": month, "percentage_allocation": pct})
|
||||
return doc
|
||||
|
||||
def test_get_months_populates_twelve_even_rows(self):
|
||||
doc = frappe.new_doc("Monthly Distribution")
|
||||
doc.distribution_id = "_Test MD Even"
|
||||
doc.get_months()
|
||||
|
||||
self.assertEqual(len(doc.percentages), 12)
|
||||
self.assertEqual(doc.percentages[0].month, "January")
|
||||
self.assertEqual(doc.percentages[-1].month, "December")
|
||||
self.assertEqual([d.idx for d in doc.percentages], list(range(1, 13)))
|
||||
for d in doc.percentages:
|
||||
self.assertAlmostEqual(d.percentage_allocation, 100.0 / 12, places=4)
|
||||
# the auto-populated rows round to exactly 100 and pass validation
|
||||
doc.validate()
|
||||
|
||||
def test_validate_rejects_total_other_than_100(self):
|
||||
doc = self.make_distribution([("January", 50), ("February", 30)]) # sums to 80
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_get_percentage_sums_period_window(self):
|
||||
doc = self.make_distribution([("January", 50), ("February", 30), ("March", 20)])
|
||||
doc.insert() # total is 100, so validate passes
|
||||
|
||||
# a quarter starting in January covers Jan+Feb+Mar
|
||||
self.assertEqual(get_percentage(doc, getdate("2026-01-01"), 3), 100)
|
||||
# a single month picks up only that month
|
||||
self.assertEqual(get_percentage(doc, getdate("2026-02-01"), 1), 30)
|
||||
# months with no row simply contribute 0 (there is no guard that all 12 exist)
|
||||
self.assertEqual(get_percentage(doc, getdate("2026-04-01"), 1), 0)
|
||||
|
||||
def test_periodwise_distribution_maps_each_period(self):
|
||||
doc = self.make_distribution([("January", 50), ("February", 30), ("March", 20)])
|
||||
doc.insert()
|
||||
|
||||
period_list = [
|
||||
frappe._dict(key="q1", from_date=getdate("2026-01-01")),
|
||||
frappe._dict(key="q2", from_date=getdate("2026-04-01")),
|
||||
]
|
||||
data = get_periodwise_distribution_data(doc.name, period_list, "Quarterly")
|
||||
self.assertEqual(data["q1"], 100) # Jan+Feb+Mar
|
||||
self.assertEqual(data["q2"], 0) # Apr+May+Jun carry no allocation
|
||||
pass
|
||||
|
||||
@@ -1,67 +1,9 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
CUSTOMER = "_Test Customer"
|
||||
SUPPLIER = "_Test Supplier"
|
||||
SUPPLIER_2 = "_Test Supplier 1"
|
||||
|
||||
|
||||
class TestPartyLink(ERPNextTestSuite):
|
||||
"""Party Link ties a Customer and a Supplier together as one underlying party.
|
||||
validate() constrains the primary role and blocks duplicate links."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_create_party_link_with_customer_primary(self):
|
||||
link = create_party_link("Customer", CUSTOMER, SUPPLIER)
|
||||
self.assertEqual(link.primary_role, "Customer")
|
||||
self.assertEqual(link.secondary_role, "Supplier")
|
||||
self.assertEqual(link.primary_party, CUSTOMER)
|
||||
self.assertEqual(link.secondary_party, SUPPLIER)
|
||||
self.assertTrue(frappe.db.exists("Party Link", link.name))
|
||||
|
||||
def test_create_party_link_with_supplier_primary(self):
|
||||
link = create_party_link("Supplier", SUPPLIER, CUSTOMER)
|
||||
self.assertEqual(link.primary_role, "Supplier")
|
||||
self.assertEqual(link.secondary_role, "Customer")
|
||||
self.assertEqual(link.primary_party, SUPPLIER)
|
||||
self.assertEqual(link.secondary_party, CUSTOMER)
|
||||
self.assertTrue(frappe.db.exists("Party Link", link.name))
|
||||
|
||||
def test_primary_role_must_be_customer_or_supplier(self):
|
||||
doc = frappe.new_doc("Party Link")
|
||||
doc.primary_role = "Employee"
|
||||
doc.primary_party = CUSTOMER
|
||||
doc.secondary_role = "Supplier"
|
||||
doc.secondary_party = SUPPLIER
|
||||
# validate() alone isolates the role rule from the dynamic-link checks
|
||||
self.assertRaises(frappe.ValidationError, doc.validate)
|
||||
|
||||
def test_duplicate_link_throws(self):
|
||||
create_party_link("Customer", CUSTOMER, SUPPLIER)
|
||||
dup = frappe.new_doc("Party Link")
|
||||
dup.primary_role = "Customer"
|
||||
dup.primary_party = CUSTOMER
|
||||
dup.secondary_role = "Supplier"
|
||||
dup.secondary_party = SUPPLIER
|
||||
self.assertRaises(frappe.ValidationError, dup.insert)
|
||||
|
||||
def test_party_can_wrongly_be_primary_in_two_links(self):
|
||||
# SUSPECTED BUG: the uniqueness checks are asymmetric - a party already a
|
||||
# *primary* in another link isn't blocked, so one customer can be linked to two
|
||||
# different suppliers, breaking the 1:1 mapping. Locking the current (wrong)
|
||||
# behaviour so a fix that blocks primary reuse trips this test.
|
||||
create_party_link("Customer", CUSTOMER, SUPPLIER)
|
||||
link2 = frappe.new_doc("Party Link")
|
||||
link2.primary_role = "Customer"
|
||||
link2.primary_party = CUSTOMER
|
||||
link2.secondary_role = "Supplier"
|
||||
link2.secondary_party = SUPPLIER_2
|
||||
link2.insert()
|
||||
self.assertTrue(frappe.db.exists("Party Link", link2.name))
|
||||
pass
|
||||
|
||||
@@ -414,17 +414,21 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
show_general_ledger: function (frm) {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(__("Ledger"), function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
"fa fa-table"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -2317,65 +2317,3 @@ def create_customer(name="_Test Customer 2 USD", currency="USD"):
|
||||
customer.save()
|
||||
customer = customer.name
|
||||
return customer
|
||||
|
||||
|
||||
class TestPaymentEntryValidation(ERPNextTestSuite):
|
||||
"""Field-level validations invoked on the document directly, covering branches the
|
||||
integration suite above doesn't reach (no GL / reconciliation setup needed)."""
|
||||
|
||||
def make_pe(self, **fields):
|
||||
doc = frappe.new_doc("Payment Entry")
|
||||
doc.update(fields)
|
||||
return doc
|
||||
|
||||
def test_payment_type_must_be_a_known_value(self):
|
||||
self.assertRaises(frappe.ValidationError, self.make_pe(payment_type="Foo").validate_payment_type)
|
||||
self.make_pe(payment_type="Receive").validate_payment_type() # valid value passes
|
||||
|
||||
def test_nonexistent_party_is_rejected(self):
|
||||
doc = self.make_pe(party_type="Customer", party="__No Such Customer__")
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_party_details)
|
||||
|
||||
def test_amount_and_exchange_rate_fields_are_mandatory(self):
|
||||
# every field but target_exchange_rate is set, so that missing one raises
|
||||
doc = self.make_pe(
|
||||
paid_amount=100, received_amount=100, source_exchange_rate=1, target_exchange_rate=0
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_mandatory)
|
||||
|
||||
def test_received_amount_cannot_exceed_paid_in_same_currency(self):
|
||||
doc = self.make_pe(
|
||||
paid_from_account_currency="INR",
|
||||
paid_to_account_currency="INR",
|
||||
paid_amount=100,
|
||||
received_amount=150,
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_received_amount)
|
||||
# received <= paid is fine
|
||||
doc.received_amount = 50
|
||||
doc.validate_received_amount()
|
||||
|
||||
def test_duplicate_reference_rows_are_rejected(self):
|
||||
doc = self.make_pe()
|
||||
for _ in range(2):
|
||||
doc.append(
|
||||
"references",
|
||||
{"reference_doctype": "Sales Invoice", "reference_name": "SI-X", "allocated_amount": 100},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_duplicate_entry)
|
||||
|
||||
def test_receive_from_customer_against_negative_outstanding_is_rejected(self):
|
||||
doc = self.make_pe(party_type="Customer", payment_type="Receive")
|
||||
doc.append(
|
||||
"references",
|
||||
{"reference_doctype": "Sales Invoice", "reference_name": "SI-Y", "allocated_amount": -100},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_payment_type_with_outstanding)
|
||||
|
||||
def test_bank_transaction_requires_a_reference_number(self):
|
||||
doc = self.make_pe(payment_type="Pay", paid_from="_Test Bank - _TC")
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_transaction_reference)
|
||||
# supplying the reference details clears the requirement
|
||||
doc.reference_no = "TXN-1"
|
||||
doc.reference_date = "2026-06-15"
|
||||
doc.validate_transaction_reference()
|
||||
|
||||
@@ -718,7 +718,7 @@ class PaymentRequest(Document):
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def make_payment_request(**args):
|
||||
"""Make payment request"""
|
||||
|
||||
|
||||
@@ -41,17 +41,21 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(__("Ledger"), function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.period_start_date,
|
||||
to_date: frm.doc.period_end_date,
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.period_start_date,
|
||||
to_date: frm.doc.period_end_date,
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
"fa fa-table"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ frappe.ui.form.on("Pricing Rule", {
|
||||
var help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
|
||||
<tr><td>
|
||||
<h4>
|
||||
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
${__("Notes")}
|
||||
</h4>
|
||||
<ul>
|
||||
@@ -63,7 +63,7 @@ frappe.ui.form.on("Pricing Rule", {
|
||||
</ul>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<h4><svg class="icon icon-sm"><use href="#icon-circle-question-mark"></use></svg>
|
||||
<h4><i class="fa fa-question-sign"></i>
|
||||
${__("How Pricing Rule is applied?")}
|
||||
</h4>
|
||||
<ol>
|
||||
|
||||
@@ -106,8 +106,6 @@ def get_pr_instance(doc: str):
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"from_payment_date",
|
||||
|
||||
@@ -1,73 +1,11 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
|
||||
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
get_pr_instance,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
|
||||
|
||||
class TestProcessPaymentReconciliation(ERPNextTestSuite):
|
||||
"""Process Payment Reconciliation validates its accounts against the company,
|
||||
moves to Queued on submit, and hands its filters to a Payment Reconciliation run."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_ppr(self, **args):
|
||||
args = frappe._dict(args)
|
||||
doc = frappe.new_doc("Process Payment Reconciliation")
|
||||
doc.company = COMPANY
|
||||
doc.party_type = "Customer"
|
||||
doc.party = "_Test Customer"
|
||||
doc.receivable_payable_account = args.get("receivable_payable_account", "Debtors - _TC")
|
||||
doc.bank_cash_account = args.get("bank_cash_account")
|
||||
doc.from_invoice_date = args.get("from_invoice_date")
|
||||
doc.to_invoice_date = args.get("to_invoice_date")
|
||||
return doc
|
||||
|
||||
def other_company_account(self, **extra):
|
||||
filters = {"company": "_Test Company 1", "is_group": 0, **extra}
|
||||
account = frappe.db.get_value("Account", filters, "name")
|
||||
self.assertTrue(account, "need a matching account in _Test Company 1")
|
||||
return account
|
||||
|
||||
def test_receivable_account_must_belong_to_company(self):
|
||||
doc = self.make_ppr(receivable_payable_account=self.other_company_account(account_type="Receivable"))
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_bank_cash_account_must_belong_to_company(self):
|
||||
doc = self.make_ppr(bank_cash_account=self.other_company_account())
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_submit_sets_status_to_queued(self):
|
||||
doc = self.make_ppr()
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
self.assertEqual(doc.status, "Queued")
|
||||
|
||||
def test_get_pr_instance_copies_filters_and_caps_limits(self):
|
||||
doc = self.make_ppr(from_invoice_date="2026-01-01", to_invoice_date="2026-06-30")
|
||||
doc.insert()
|
||||
|
||||
pr = get_pr_instance(doc.name)
|
||||
self.assertEqual(pr.company, COMPANY)
|
||||
self.assertEqual(pr.party, "_Test Customer")
|
||||
self.assertEqual(pr.receivable_payable_account, "Debtors - _TC")
|
||||
self.assertEqual(str(pr.from_invoice_date), "2026-01-01")
|
||||
# the tool run is capped so a single process can't fetch unbounded rows
|
||||
self.assertEqual(pr.invoice_limit, 1000)
|
||||
self.assertEqual(pr.payment_limit, 1000)
|
||||
|
||||
def test_get_pr_instance_copies_bank_cash_and_cost_center(self):
|
||||
doc = self.make_ppr(bank_cash_account="Cash - _TC")
|
||||
doc.cost_center = "_Test Cost Center - _TC"
|
||||
doc.insert()
|
||||
|
||||
pr = get_pr_instance(doc.name)
|
||||
self.assertEqual(pr.bank_cash_account, "Cash - _TC")
|
||||
self.assertEqual(pr.cost_center, "_Test Cost Center - _TC")
|
||||
pass
|
||||
|
||||
@@ -89,55 +89,50 @@ class ProcessPeriodClosingVoucher(Document):
|
||||
cancel_pcv_processing(self.name)
|
||||
|
||||
|
||||
def initialize_parallel_threads(docname: str):
|
||||
threads = 4
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||
|
||||
if normal_balances := (
|
||||
qb.from_(ppcvd)
|
||||
.select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
|
||||
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
|
||||
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
|
||||
.limit(threads)
|
||||
.for_update(skip_locked=True)
|
||||
.run(as_dict=True)
|
||||
):
|
||||
if not is_scheduler_inactive():
|
||||
for x in normal_balances:
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
x.name,
|
||||
"status",
|
||||
"Running",
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
row_name=x.name,
|
||||
date=x.processing_date,
|
||||
report_type=x.report_type,
|
||||
parentfield=x.parentfield,
|
||||
)
|
||||
# keep transaction on PPCV and PPCVD short
|
||||
# prevents concurrency errors - REPEATABLE READ
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
else:
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_pcv_processing(docname: str):
|
||||
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
|
||||
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
|
||||
initialize_parallel_threads(docname)
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
|
||||
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if normal_balances := (
|
||||
qb.from_(ppcvd)
|
||||
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
|
||||
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
|
||||
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
|
||||
.limit(4)
|
||||
.for_update(skip_locked=True)
|
||||
.run(as_dict=True)
|
||||
):
|
||||
if not is_scheduler_inactive():
|
||||
for x in normal_balances:
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
{
|
||||
"processing_date": x.processing_date,
|
||||
"parent": docname,
|
||||
"report_type": x.report_type,
|
||||
"parentfield": x.parentfield,
|
||||
},
|
||||
"status",
|
||||
"Running",
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
timeout=timeout,
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
date=x.processing_date,
|
||||
report_type=x.report_type,
|
||||
parentfield=x.parentfield,
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -255,11 +250,11 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
|
||||
@frappe.whitelist()
|
||||
def schedule_next_date(docname: str):
|
||||
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
if to_process := (
|
||||
qb.from_(ppcvd)
|
||||
.select(ppcvd.name, ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
|
||||
.select(ppcvd.processing_date, ppcvd.report_type, ppcvd.parentfield)
|
||||
.where(ppcvd.parent.eq(docname) & ppcvd.status.eq("Queued"))
|
||||
.orderby(ppcvd.parentfield, ppcvd.idx, ppcvd.processing_date)
|
||||
.limit(1)
|
||||
@@ -269,15 +264,15 @@ def schedule_next_date(docname: str):
|
||||
if not is_scheduler_inactive():
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
to_process[0].name,
|
||||
{
|
||||
"processing_date": to_process[0].processing_date,
|
||||
"parent": docname,
|
||||
"report_type": to_process[0].report_type,
|
||||
"parentfield": to_process[0].parentfield,
|
||||
},
|
||||
"status",
|
||||
"Running",
|
||||
)
|
||||
# keep transaction on PPCV and PPCVD short
|
||||
# prevents concurrency errors - REPEATABLE READ
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.process_individual_date",
|
||||
queue="long",
|
||||
@@ -285,7 +280,6 @@ def schedule_next_date(docname: str):
|
||||
is_async=True,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
row_name=to_process[0].name,
|
||||
date=to_process[0].processing_date,
|
||||
report_type=to_process[0].report_type,
|
||||
parentfield=to_process[0].parentfield,
|
||||
@@ -450,11 +444,6 @@ def summarize_and_post_ledger_entries(docname):
|
||||
|
||||
make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date)
|
||||
|
||||
# keep transaction on PPCV and PPCVD short
|
||||
# prevents concurrency errors - REPEATABLE READ
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
frappe.db.set_value("Period Closing Voucher", pcv.name, "gle_processing_status", "Completed")
|
||||
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
|
||||
|
||||
@@ -540,10 +529,10 @@ def build_dimension_wise_balance_dict(gl_entries):
|
||||
return dimension_balances
|
||||
|
||||
|
||||
def process_individual_date(docname: str, row_name, date, report_type, parentfield):
|
||||
def process_individual_date(docname: str, date, report_type, parentfield):
|
||||
current_date_status = frappe.db.get_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
row_name,
|
||||
{"processing_date": date, "report_type": report_type, "parentfield": parentfield},
|
||||
"status",
|
||||
)
|
||||
if current_date_status != "Running":
|
||||
@@ -591,20 +580,17 @@ def process_individual_date(docname: str, row_name, date, report_type, parentfie
|
||||
# save results
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
row_name,
|
||||
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
|
||||
"closing_balance",
|
||||
frappe.json.dumps(res),
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
row_name,
|
||||
{"processing_date": date, "parent": docname, "report_type": report_type, "parentfield": parentfield},
|
||||
"status",
|
||||
"Completed",
|
||||
)
|
||||
# commit heavy computation before touching PPCV or PPCVD
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
# chain call
|
||||
schedule_next_date(docname)
|
||||
|
||||
@@ -48,27 +48,18 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
|
||||
ppcv.save()
|
||||
return ppcv
|
||||
|
||||
def set_processing_date_status(self, row_name, status):
|
||||
def set_processing_date_status(self, date, ppcv, rpt_type, parentfield, status):
|
||||
frappe.db.set_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
row_name,
|
||||
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
|
||||
"status",
|
||||
status,
|
||||
)
|
||||
|
||||
def get_row_name(self, ppcv_name, rpt_type, parentfield):
|
||||
return frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": ppcv_name, "report_type": rpt_type, "parentfield": parentfield},
|
||||
order_by="report_type, idx",
|
||||
pluck="name",
|
||||
limit=1,
|
||||
)[0]
|
||||
|
||||
def get_processing_date_closing_balance(self, row_name):
|
||||
def get_processing_date_closing_balance(self, date, ppcv, rpt_type, parentfield):
|
||||
return frappe.db.get_value(
|
||||
"Process Period Closing Voucher Detail",
|
||||
row_name,
|
||||
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
|
||||
"closing_balance",
|
||||
)
|
||||
|
||||
@@ -106,10 +97,11 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
|
||||
parentfield = "normal_balances"
|
||||
rpt_type = "Profit and Loss"
|
||||
# status has to be set to 'Running' for logic to run
|
||||
row_name = self.get_row_name(ppcv.name, rpt_type, parentfield)
|
||||
self.set_processing_date_status(row_name, "Running")
|
||||
process_individual_date(ppcv.name, row_name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(self.get_processing_date_closing_balance(row_name))
|
||||
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
|
||||
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(
|
||||
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
|
||||
)
|
||||
self.assertEqual(len(bal), 1)
|
||||
expected_pl = {
|
||||
"account": "Sales - _TC",
|
||||
@@ -125,10 +117,11 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
|
||||
|
||||
# Balance sheet balance
|
||||
rpt_type = "Balance Sheet"
|
||||
row_name = self.get_row_name(ppcv.name, rpt_type, parentfield)
|
||||
self.set_processing_date_status(row_name, "Running")
|
||||
process_individual_date(ppcv.name, row_name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(self.get_processing_date_closing_balance(row_name))
|
||||
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
|
||||
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(
|
||||
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
|
||||
)
|
||||
self.assertEqual(len(bal), 1)
|
||||
expected_bs = {
|
||||
"account": "Debtors - _TC",
|
||||
@@ -145,10 +138,11 @@ class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
|
||||
# Opening balance
|
||||
parentfield = "z_opening_balances"
|
||||
rpt_type = "Balance Sheet"
|
||||
row_name = self.get_row_name(ppcv.name, rpt_type, parentfield)
|
||||
self.set_processing_date_status(row_name, "Running")
|
||||
process_individual_date(ppcv.name, row_name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(self.get_processing_date_closing_balance(row_name))
|
||||
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
|
||||
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
|
||||
bal = frappe.parse_json(
|
||||
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
|
||||
)
|
||||
self.assertEqual(len(bal), 2)
|
||||
opening_cash = next(x for x in bal if x["account"] == "Cash - _TC")
|
||||
expected_opening_cash = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -24,10 +24,3 @@ class ProcessPeriodClosingVoucherDetail(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index(
|
||||
"Process Period Closing Voucher Detail",
|
||||
["parent", "status", "parentfield", "idx", "processing_date"],
|
||||
)
|
||||
|
||||
@@ -113,38 +113,3 @@ def create_process_soa(**args):
|
||||
process_soa.update(soa_dict)
|
||||
process_soa.save()
|
||||
return process_soa
|
||||
|
||||
|
||||
class TestProcessStatementOfAccountsValidation(ERPNextTestSuite):
|
||||
"""validate() fills in default subject/body/pdf templates and enforces the
|
||||
basic constraints. Exercised on the document directly (no email/PDF flow)."""
|
||||
|
||||
def make_soa(self, report="Accounts Receivable", with_customer=True, **overrides):
|
||||
doc = frappe.new_doc("Process Statement Of Accounts")
|
||||
doc.report = report
|
||||
doc.company = "_Test Company"
|
||||
if with_customer:
|
||||
doc.append("customers", {"customer": "_Test Customer"})
|
||||
doc.update(overrides)
|
||||
return doc
|
||||
|
||||
def test_customers_are_required(self):
|
||||
self.assertRaises(frappe.ValidationError, self.make_soa(with_customer=False).validate)
|
||||
|
||||
def test_general_ledger_body_uses_a_date_range(self):
|
||||
doc = self.make_soa(report="General Ledger")
|
||||
doc.validate()
|
||||
self.assertIn("from {{ doc.from_date }} to {{ doc.to_date }}", doc.body)
|
||||
# subject and pdf name are also defaulted
|
||||
self.assertTrue(doc.subject)
|
||||
self.assertTrue(doc.pdf_name)
|
||||
|
||||
def test_receivable_body_uses_the_posting_date(self):
|
||||
doc = self.make_soa(report="Accounts Receivable")
|
||||
doc.validate()
|
||||
self.assertIn("until {{ doc.posting_date }}", doc.body)
|
||||
|
||||
def test_account_must_belong_to_company(self):
|
||||
other = frappe.db.get_value("Account", {"company": "_Test Company 1", "is_group": 0}, "name")
|
||||
self.assertTrue(other, "need an account in _Test Company 1")
|
||||
self.assertRaises(frappe.ValidationError, self.make_soa(account=other).validate)
|
||||
|
||||
@@ -1,56 +1,11 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from unittest.mock import patch
|
||||
# import frappe
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.process_subscription.process_subscription import (
|
||||
create_subscription_process,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription.test_subscription import (
|
||||
create_parties,
|
||||
create_subscription,
|
||||
make_plans,
|
||||
reset_settings,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestProcessSubscription(ERPNextTestSuite):
|
||||
"""Process Subscription is a batch driver: on submit it enqueues subscription.process_all
|
||||
for every non-cancelled Subscription (or just one when a subscription is named)."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
# mirror TestSubscription setup so subscriptions build against known settings
|
||||
make_plans()
|
||||
create_parties()
|
||||
reset_settings()
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||
|
||||
def enqueued_subscriptions(self, subscription=None):
|
||||
"""Submit a Process Subscription while capturing what gets enqueued."""
|
||||
calls = []
|
||||
|
||||
def capture(*args, **kwargs):
|
||||
calls.append(kwargs)
|
||||
|
||||
with patch("frappe.enqueue", side_effect=capture):
|
||||
create_subscription_process(subscription=subscription, posting_date="2026-06-15")
|
||||
|
||||
# each enqueue is handed a batch (list) of subscription names
|
||||
return [name for call in calls for name in call.get("subscription", [])]
|
||||
|
||||
def test_named_subscription_is_the_only_one_enqueued(self):
|
||||
sub = create_subscription(start_date="2026-01-01")
|
||||
self.assertEqual(self.enqueued_subscriptions(subscription=sub.name), [sub.name])
|
||||
|
||||
def test_cancelled_subscriptions_are_skipped(self):
|
||||
active = create_subscription(start_date="2026-01-01")
|
||||
cancelled = create_subscription(start_date="2026-01-01")
|
||||
cancelled.cancel_subscription()
|
||||
|
||||
enqueued = self.enqueued_subscriptions()
|
||||
self.assertIn(active.name, enqueued)
|
||||
self.assertNotIn(cancelled.name, enqueued)
|
||||
pass
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
@@ -130,6 +131,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
get_purchase_document_details,
|
||||
)
|
||||
from erpnext.stock.utils import get_valuation_method
|
||||
|
||||
doc = self.doc
|
||||
tax_service = TaxService(doc)
|
||||
@@ -329,25 +331,33 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
self.make_provisional_gl_entry(gl_entries, item)
|
||||
|
||||
if not doc.is_internal_transfer():
|
||||
# When Update Stock is disabled, this invoice has no stock impact: the linked
|
||||
# Purchase Receipt already booked the stock (at standard) and the Purchase Price
|
||||
# Variance. Here we only clear "Stock Received But Not Billed" at the full billed
|
||||
# amount against the supplier - booking PPV again would double count it and leave
|
||||
# SRBNB partially uncleared.
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": base_amount,
|
||||
"debit_in_transaction_currency": amount,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
handled = False
|
||||
if (
|
||||
item.item_code
|
||||
and item.item_code in stock_items
|
||||
and item.get("purchase_receipt")
|
||||
and not doc.is_return
|
||||
and get_valuation_method(item.item_code, doc.company) == "Standard Cost"
|
||||
):
|
||||
handled = self.make_standard_cost_srbnb_split(
|
||||
gl_entries, item, expense_account, account_currency, base_amount
|
||||
)
|
||||
|
||||
if not handled:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": base_amount,
|
||||
"debit_in_transaction_currency": amount,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# check if the exchange rate has changed
|
||||
if (
|
||||
@@ -520,6 +530,95 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
},
|
||||
)
|
||||
|
||||
def make_standard_cost_srbnb_split(
|
||||
self, gl_entries, item, expense_account, account_currency, base_amount
|
||||
):
|
||||
"""For a Standard Cost item billed against a Purchase Receipt, clear SRBNB at the standard
|
||||
value the receipt actually booked and post the (Net Amount - standard) difference to the
|
||||
Purchase Price Variance account. Returns False (caller falls back) if the receipt value
|
||||
can't be resolved."""
|
||||
from erpnext.stock.doctype.item_standard_cost.item_standard_cost import (
|
||||
get_purchase_price_variance_account,
|
||||
)
|
||||
|
||||
doc = self.doc
|
||||
precision = item.precision("base_net_amount")
|
||||
standard_value = flt(self.get_pr_stock_value(item), precision)
|
||||
if not standard_value:
|
||||
return False
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": expense_account,
|
||||
"against": doc.supplier,
|
||||
"debit": standard_value,
|
||||
"debit_in_transaction_currency": flt(standard_value / doc.conversion_rate, precision),
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
account_currency,
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
variance = flt(base_amount - standard_value, precision)
|
||||
if variance:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": get_purchase_price_variance_account(item.item_code, doc.company),
|
||||
"against": doc.supplier,
|
||||
"debit": variance,
|
||||
"debit_in_transaction_currency": flt(variance / doc.conversion_rate, precision),
|
||||
"remarks": doc.get("remarks") or _("Purchase Price Variance"),
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def get_pr_stock_value(self, item):
|
||||
"""Stock value (at standard) the linked Purchase Receipt booked for the quantity this invoice
|
||||
row is billing.
|
||||
|
||||
Accepted and rejected stock for the same receipt row share `voucher_detail_no`, so the
|
||||
warehouse filter is required: without it the accepted warehouse's SRBNB would be cleared at
|
||||
accepted + rejected value and post the wrong Purchase Price Variance amount. The accepted
|
||||
warehouse is read from the receipt row itself (not the invoice row, which may be unset on a
|
||||
non-stock invoice).
|
||||
|
||||
The receipt's full accepted value is pro-rated to the invoiced quantity, so a partial bill
|
||||
clears SRBNB (and posts PPV) for only the units it covers, not the whole receipt row."""
|
||||
pr_detail = frappe.db.get_value(
|
||||
"Purchase Receipt Item", item.pr_detail, ["warehouse", "stock_qty"], as_dict=True
|
||||
)
|
||||
if not pr_detail or not pr_detail.warehouse:
|
||||
return 0.0
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
result = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(Sum(sle.stock_value_difference))
|
||||
.where(
|
||||
(sle.voucher_type == "Purchase Receipt")
|
||||
& (sle.voucher_no == item.purchase_receipt)
|
||||
& (sle.voucher_detail_no == item.pr_detail)
|
||||
& (sle.warehouse == pr_detail.warehouse)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
).run()
|
||||
accepted_value = flt(result[0][0]) if result and result[0][0] else 0.0
|
||||
if not accepted_value or not flt(pr_detail.stock_qty):
|
||||
return accepted_value
|
||||
|
||||
# Pro-rate to the quantity being billed by this invoice row (handles partial billing).
|
||||
return accepted_value * flt(item.stock_qty) / flt(pr_detail.stock_qty)
|
||||
|
||||
def get_stock_variance_account(self, item):
|
||||
"""For Standard Cost items the purchase-price-vs-standard difference is a Purchase Price
|
||||
Variance; for all other items it keeps the existing behaviour (default expense account)."""
|
||||
|
||||
@@ -121,65 +121,3 @@ class TestShareTransfer(ERPNextTestSuite):
|
||||
}
|
||||
)
|
||||
self.assertRaises(ShareDontExists, doc.insert)
|
||||
|
||||
|
||||
class TestShareTransferValidation(ERPNextTestSuite):
|
||||
"""basic_validations() enforces the transfer's internal consistency. Exercised
|
||||
directly (to_folio_no set to skip folio auto-naming) so no shareholder fixtures
|
||||
are needed - it only reasons about the document's own fields."""
|
||||
|
||||
def make_transfer(self, **overrides):
|
||||
doc = frappe.new_doc("Share Transfer")
|
||||
doc.update(
|
||||
{
|
||||
"transfer_type": "Transfer",
|
||||
"date": "2026-01-01",
|
||||
"from_shareholder": "SH-A",
|
||||
"to_shareholder": "SH-B",
|
||||
"to_folio_no": "1",
|
||||
"share_type": "Equity",
|
||||
"from_no": 1,
|
||||
"to_no": 100,
|
||||
"no_of_shares": 100,
|
||||
"rate": 10,
|
||||
"amount": 1000,
|
||||
"company": "_Test Company",
|
||||
"equity_or_liability_account": "Creditors - _TC",
|
||||
}
|
||||
)
|
||||
doc.update(overrides)
|
||||
return doc
|
||||
|
||||
def test_baseline_transfer_is_consistent(self):
|
||||
# the helper's defaults must pass, otherwise the negative cases prove nothing
|
||||
self.make_transfer().basic_validations()
|
||||
|
||||
def test_seller_and_buyer_must_differ(self):
|
||||
doc = self.make_transfer(to_shareholder="SH-A")
|
||||
self.assertRaises(frappe.ValidationError, doc.basic_validations)
|
||||
|
||||
def test_share_count_must_match_the_number_range(self):
|
||||
# 1..100 is 100 shares, not 50
|
||||
doc = self.make_transfer(no_of_shares=50)
|
||||
self.assertRaises(frappe.ValidationError, doc.basic_validations)
|
||||
|
||||
def test_amount_must_equal_rate_times_shares(self):
|
||||
doc = self.make_transfer(amount=999) # 10 * 100 = 1000
|
||||
self.assertRaises(frappe.ValidationError, doc.basic_validations)
|
||||
|
||||
def test_amount_is_derived_when_left_blank(self):
|
||||
doc = self.make_transfer(amount=0)
|
||||
doc.basic_validations()
|
||||
self.assertEqual(doc.amount, 1000)
|
||||
|
||||
def test_equity_or_liability_account_is_required(self):
|
||||
doc = self.make_transfer(equity_or_liability_account=None)
|
||||
self.assertRaises(frappe.ValidationError, doc.basic_validations)
|
||||
|
||||
def test_issue_requires_a_to_shareholder(self):
|
||||
doc = self.make_transfer(transfer_type="Issue", to_shareholder="", asset_account="Cash - _TC")
|
||||
self.assertRaises(frappe.ValidationError, doc.basic_validations)
|
||||
|
||||
def test_purchase_requires_a_from_shareholder(self):
|
||||
doc = self.make_transfer(transfer_type="Purchase", from_shareholder="", asset_account="Cash - _TC")
|
||||
self.assertRaises(frappe.ValidationError, doc.basic_validations)
|
||||
|
||||
@@ -79,9 +79,7 @@ def get_plan_rate(
|
||||
start_date = getdate(start_date)
|
||||
end_date = getdate(end_date)
|
||||
|
||||
delta = relativedelta.relativedelta(end_date, start_date)
|
||||
# include the years component so cross-year spans aren't under-counted
|
||||
no_of_months = delta.years * 12 + delta.months + 1
|
||||
no_of_months = relativedelta.relativedelta(end_date, start_date).months + 1
|
||||
cost = plan.cost * no_of_months
|
||||
|
||||
# Adjust cost if start or end date is not month start or end
|
||||
|
||||
@@ -1,54 +1,8 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestSubscriptionPlan(ERPNextTestSuite):
|
||||
"""Subscription Plan validates its interval and computes a rate. The Monthly
|
||||
Rate branch multiplies cost by the number of months in the billing window."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_plan(self, **args):
|
||||
args = frappe._dict(args)
|
||||
plan = frappe.new_doc("Subscription Plan")
|
||||
plan.plan_name = f"_Test Plan {frappe.generate_hash(length=6)}"
|
||||
plan.item = args.item or "_Test Item"
|
||||
plan.currency = args.currency or "INR"
|
||||
plan.price_determination = args.price_determination
|
||||
plan.cost = args.cost or 0
|
||||
plan.billing_interval = args.billing_interval or "Month"
|
||||
plan.billing_interval_count = (
|
||||
args.billing_interval_count if args.billing_interval_count is not None else 1
|
||||
)
|
||||
return plan
|
||||
|
||||
def test_billing_interval_count_must_be_positive(self):
|
||||
plan = self.make_plan(price_determination="Fixed Rate", cost=100, billing_interval_count=0)
|
||||
self.assertRaises(frappe.ValidationError, plan.insert)
|
||||
|
||||
def test_fixed_rate_applies_prorate_factor(self):
|
||||
plan = self.make_plan(price_determination="Fixed Rate", cost=100)
|
||||
plan.insert()
|
||||
self.assertEqual(get_plan_rate(plan.name), 100)
|
||||
self.assertEqual(get_plan_rate(plan.name, prorate_factor=0.5), 50)
|
||||
|
||||
def test_monthly_rate_within_year(self):
|
||||
plan = self.make_plan(price_determination="Monthly Rate", cost=100)
|
||||
plan.insert()
|
||||
# Jan 1 - Mar 31 is 3 whole months; month-aligned so proration is 0
|
||||
rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2026-03-31")
|
||||
self.assertEqual(rate, 300)
|
||||
|
||||
def test_monthly_rate_across_year_boundary(self):
|
||||
# a 14-month span (Jan 2026 to Feb 2027) bills all 14 months, not just the
|
||||
# 2-month remainder that relativedelta.months alone would give
|
||||
plan = self.make_plan(price_determination="Monthly Rate", cost=100)
|
||||
plan.insert()
|
||||
rate = get_plan_rate(plan.name, start_date="2026-01-01", end_date="2027-02-28")
|
||||
self.assertEqual(rate, 1400)
|
||||
pass
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -15,7 +15,7 @@
|
||||
"label": "Banking",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-03 13:43:50.924019",
|
||||
"modified": "2026-06-14 13:43:50.924019",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Banking",
|
||||
@@ -43,7 +43,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "wrench",
|
||||
"icon": "tool",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Bank Reconciliation",
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Budgeting",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-03 04:24:48.116724",
|
||||
"modified": "2026-07-02 04:24:48.116724",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budgeting",
|
||||
@@ -59,7 +59,7 @@
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"default_workspace": 0,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Accounting Dimension",
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-07-03 13:44:08.095321",
|
||||
"modified": "2026-06-14 13:44:08.095321",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
@@ -284,7 +284,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Financial Reports",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"idx": 4,
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
@@ -587,7 +587,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-07-03 13:44:08.471142",
|
||||
"modified": "2026-06-14 13:44:08.471142",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
@@ -622,7 +622,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "house",
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
@@ -635,7 +635,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart-column",
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dashboard",
|
||||
@@ -786,7 +786,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "coins",
|
||||
"icon": "money-coins-1",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Payments",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"label": "Payments",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-03 13:43:50.184761",
|
||||
"modified": "2026-06-14 13:43:50.184761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
@@ -31,7 +31,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart-column",
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dashboard",
|
||||
@@ -44,7 +44,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "coins",
|
||||
"icon": "money-coins-1",
|
||||
"indent": 1,
|
||||
"keep_closed": 0,
|
||||
"label": "Payments",
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "coins",
|
||||
"icon": "money-coins-1",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Share Management",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-03 13:43:51.040978",
|
||||
"modified": "2026-06-14 13:43:51.040978",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Share Management",
|
||||
@@ -30,7 +30,7 @@
|
||||
{
|
||||
"child": 1,
|
||||
"collapsible": 1,
|
||||
"icon": "user",
|
||||
"icon": "customer",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Shareholder",
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Subscriptions",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-03 14:08:36.999272",
|
||||
"modified": "2026-06-14 14:08:36.999272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscriptions",
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "coins",
|
||||
"icon": "money-coins-1",
|
||||
"idx": 0,
|
||||
"indicator_color": "green",
|
||||
"is_hidden": 0,
|
||||
"label": "Taxes",
|
||||
"link_type": "DocType",
|
||||
"links": [],
|
||||
"modified": "2026-07-03 13:43:50.894825",
|
||||
"modified": "2026-06-14 13:43:50.894825",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"module_onboarding": "Accounting Onboarding",
|
||||
@@ -58,7 +58,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "package",
|
||||
"icon": "stock",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Item Tax Template",
|
||||
|
||||
@@ -587,47 +587,3 @@ def get_actual_sle_dict(name):
|
||||
}
|
||||
|
||||
return sle_dict
|
||||
|
||||
|
||||
class TestAssetCapitalizationValidation(ERPNextTestSuite):
|
||||
"""Row-level validations for the consumed/target items. Exercised on the document
|
||||
directly (the integration tests above cover the full capitalization posting)."""
|
||||
|
||||
def make_capitalization(self, **fields):
|
||||
doc = frappe.new_doc("Asset Capitalization")
|
||||
doc.company = "_Test Company"
|
||||
doc.update(fields)
|
||||
return doc
|
||||
|
||||
def test_source_items_are_mandatory(self):
|
||||
doc = self.make_capitalization()
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_source_mandatory)
|
||||
|
||||
def test_target_item_must_be_a_fixed_asset(self):
|
||||
# _Test Item is a stock item, not a fixed asset
|
||||
doc = self.make_capitalization(target_item_code="_Test Item")
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_target_item)
|
||||
|
||||
def test_consumed_stock_row_rejects_a_non_stock_item(self):
|
||||
doc = self.make_capitalization()
|
||||
doc.append("stock_items", {"item_code": "_Test Non Stock Item", "stock_qty": 1})
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_consumed_stock_item)
|
||||
|
||||
def test_consumed_stock_row_requires_positive_qty(self):
|
||||
doc = self.make_capitalization()
|
||||
doc.append("stock_items", {"item_code": "_Test Item", "stock_qty": 0})
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_consumed_stock_item)
|
||||
|
||||
def test_service_row_rejects_a_stock_item(self):
|
||||
doc = self.make_capitalization()
|
||||
doc.append("service_items", {"item_code": "_Test Item", "qty": 1, "rate": 100})
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_service_item)
|
||||
|
||||
def test_service_row_requires_positive_qty_and_rate(self):
|
||||
zero_qty = self.make_capitalization()
|
||||
zero_qty.append("service_items", {"item_code": "_Test Non Stock Item", "qty": 0, "rate": 100})
|
||||
self.assertRaises(frappe.ValidationError, zero_qty.validate_service_item)
|
||||
|
||||
zero_rate = self.make_capitalization()
|
||||
zero_rate.append("service_items", {"item_code": "_Test Non Stock Item", "qty": 1, "rate": 0})
|
||||
self.assertRaises(frappe.ValidationError, zero_rate.validate_service_item)
|
||||
|
||||
@@ -224,7 +224,7 @@ def get_children(doctype: str, parent: str | None = None, location: str | None =
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def add_node():
|
||||
from frappe.desk.treeview import make_tree_args
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "archive",
|
||||
"icon": "assets",
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "Assets",
|
||||
@@ -199,7 +199,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-07-03 13:44:08.417956",
|
||||
"modified": "2026-06-14 13:44:08.417956",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"module_onboarding": "Asset Onboarding",
|
||||
@@ -217,7 +217,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "house",
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
@@ -230,7 +230,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart-column",
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dashboard",
|
||||
@@ -295,7 +295,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "rocket",
|
||||
"icon": "getting-started",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Maintenance",
|
||||
|
||||
@@ -55,7 +55,7 @@ def make_supplier_quotation_from_rfq(
|
||||
|
||||
|
||||
# This method is used to make supplier quotation from supplier's portal.
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def create_supplier_quotation(doc: str | Document | dict):
|
||||
doc = frappe.parse_json(doc)
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ def refresh_scorecards():
|
||||
frappe.get_doc("Supplier Scorecard", sc_name).save()
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def make_all_scorecards(docname: str):
|
||||
sc = frappe.get_doc("Supplier Scorecard", docname)
|
||||
supplier = frappe.get_doc("Supplier", sc.supplier)
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,7 +13,7 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "shopping-cart",
|
||||
"icon": "buying",
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "Buying",
|
||||
@@ -501,7 +501,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-07-03 13:43:50.509039",
|
||||
"modified": "2026-06-14 13:43:50.509039",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"module_onboarding": "Buying Onboarding",
|
||||
@@ -532,7 +532,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "house",
|
||||
"icon": "home",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
@@ -545,7 +545,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart-column",
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Dashboard",
|
||||
@@ -610,7 +610,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "scale",
|
||||
"icon": "liabilities",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Purchase Invoice",
|
||||
|
||||
@@ -1724,7 +1724,7 @@ def get_missing_company_details(doctype: str, docname: str):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def update_company_master_and_address(current_doctype: str, name: str, company: str, details: dict | str):
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from frappe.query_builder.functions import (
|
||||
Substring,
|
||||
Sum,
|
||||
)
|
||||
from frappe.utils import cint, nowdate, today, unique
|
||||
from frappe.utils import nowdate, today, unique
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -808,11 +808,7 @@ def get_filtered_dimensions(
|
||||
query_filters.append(["company", "=", filters.get("company")])
|
||||
|
||||
for field in searchfields:
|
||||
df = meta.get_field(field)
|
||||
if df and df.fieldtype != "Check":
|
||||
or_filters.append([field, "LIKE", "%%%s%%" % txt])
|
||||
else:
|
||||
or_filters.append([field, "=", cint(txt)])
|
||||
or_filters.append([field, "LIKE", "%%%s%%" % txt])
|
||||
fields.append(field)
|
||||
|
||||
if dimension_filters:
|
||||
|
||||
@@ -653,7 +653,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
|
||||
return [item for item in items if item.get("item_code") in inspection_required_items]
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(
|
||||
company: str, doctype: str, docname: str, items: str | list, inspection_type: str
|
||||
):
|
||||
|
||||
@@ -17,7 +17,7 @@ frappe.ui.form.on("Campaign", {
|
||||
frappe.route_options = { utm_source: "Campaign", utm_campaign: frm.doc.name };
|
||||
frappe.set_route("List", "Lead");
|
||||
},
|
||||
null,
|
||||
"fa fa-list",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,23 +26,23 @@ class Campaign(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def after_insert(self):
|
||||
self.sync_utm_campaign()
|
||||
|
||||
def on_change(self):
|
||||
self.sync_utm_campaign()
|
||||
|
||||
def sync_utm_campaign(self):
|
||||
# look up the existing mirror by the stable Campaign link first, so editing
|
||||
# campaign_name updates that mirror instead of creating a duplicate
|
||||
existing = frappe.db.get_value("UTM Campaign", {"crm_campaign": self.name}) or self.campaign_name
|
||||
try:
|
||||
mc = frappe.get_doc("UTM Campaign", existing)
|
||||
mc = frappe.get_doc("UTM Campaign", self.campaign_name)
|
||||
except frappe.DoesNotExistError:
|
||||
mc = frappe.new_doc("UTM Campaign")
|
||||
mc.name = self.campaign_name
|
||||
mc.campaign_description = self.description
|
||||
# link by the document name, which differs from campaign_name when a naming series is used
|
||||
mc.crm_campaign = self.name
|
||||
mc.crm_campaign = self.campaign_name
|
||||
mc.save(ignore_permissions=True)
|
||||
|
||||
def on_change(self):
|
||||
try:
|
||||
mc = frappe.get_doc("UTM Campaign", self.campaign_name)
|
||||
except frappe.DoesNotExistError:
|
||||
mc = frappe.new_doc("UTM Campaign")
|
||||
mc.name = self.campaign_name
|
||||
mc.campaign_description = self.description
|
||||
mc.crm_campaign = self.campaign_name
|
||||
mc.save(ignore_permissions=True)
|
||||
|
||||
def autoname(self):
|
||||
|
||||
@@ -1,52 +1,9 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestCampaign(ERPNextTestSuite):
|
||||
"""Campaign names itself from the campaign name (or a naming series) and mirrors
|
||||
itself into a UTM Campaign."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_campaign(self, **fields):
|
||||
doc = frappe.new_doc("Campaign")
|
||||
doc.campaign_name = fields.pop("campaign_name", f"_Test Campaign {frappe.generate_hash(length=6)}")
|
||||
doc.update(fields)
|
||||
return doc.insert()
|
||||
|
||||
def test_autoname_uses_the_campaign_name_by_default(self):
|
||||
campaign = self.make_campaign(campaign_name="_Test Campaign Named")
|
||||
self.assertEqual(campaign.name, "_Test Campaign Named")
|
||||
|
||||
def test_autoname_uses_naming_series_when_configured(self):
|
||||
# regression: with a naming series the document name differs from campaign_name,
|
||||
# and the UTM sync must still link back to a valid Campaign (self.name)
|
||||
original = frappe.defaults.get_global_default("campaign_naming_by")
|
||||
frappe.defaults.set_global_default("campaign_naming_by", "Naming Series")
|
||||
try:
|
||||
campaign = self.make_campaign(naming_series="SAL-CAM-.YYYY.-")
|
||||
self.assertTrue(campaign.name.startswith("SAL-CAM-"))
|
||||
utm = frappe.get_doc("UTM Campaign", campaign.campaign_name)
|
||||
self.assertEqual(utm.crm_campaign, campaign.name)
|
||||
finally:
|
||||
frappe.defaults.set_global_default("campaign_naming_by", original or "")
|
||||
|
||||
def test_inserting_mirrors_into_a_utm_campaign(self):
|
||||
campaign = self.make_campaign(campaign_name="_Test Campaign UTM", description="Spring push")
|
||||
self.assertTrue(frappe.db.exists("UTM Campaign", campaign.campaign_name))
|
||||
utm = frappe.get_doc("UTM Campaign", campaign.campaign_name)
|
||||
self.assertEqual(utm.campaign_description, "Spring push")
|
||||
self.assertEqual(utm.crm_campaign, campaign.name)
|
||||
|
||||
def test_editing_campaign_name_reuses_the_same_utm_campaign(self):
|
||||
campaign = self.make_campaign(campaign_name="_Test Campaign Rename A")
|
||||
campaign.campaign_name = "_Test Campaign Rename B"
|
||||
campaign.save()
|
||||
# the edit updates the existing mirror rather than creating a second one
|
||||
mirrors = frappe.get_all("UTM Campaign", filters={"crm_campaign": campaign.name})
|
||||
self.assertEqual(len(mirrors), 1)
|
||||
pass
|
||||
|
||||
@@ -380,7 +380,7 @@ def get_lead_with_phone_number(number):
|
||||
return lead
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def add_lead_to_prospect(lead: str, prospect: str):
|
||||
prospect = frappe.get_doc("Prospect", prospect)
|
||||
prospect.append("leads", {"lead": lead})
|
||||
|
||||
@@ -110,7 +110,7 @@ def make_quotation(source_name: str, target_doc: str | Document | None = None):
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
|
||||
"""raise a issue from email"""
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ def make_supplier_quotation(source_name: str, target_doc: str | Document | None
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def make_opportunity_from_communication(
|
||||
communication: str, company: str, ignore_communication_links: bool = False
|
||||
):
|
||||
|
||||
@@ -389,7 +389,7 @@ def get_item_details(item_code: str):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def set_multiple_status(names: str | list[str], status: str):
|
||||
names = frappe.parse_json(names)
|
||||
for name in names:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"doctype": "Workspace",
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "handshake",
|
||||
"icon": "crm",
|
||||
"idx": 0,
|
||||
"is_hidden": 0,
|
||||
"label": "CRM",
|
||||
@@ -421,7 +421,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-07-03 13:44:08.297053",
|
||||
"modified": "2026-06-14 13:44:08.297053",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM",
|
||||
@@ -471,7 +471,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "chart-column",
|
||||
"icon": "chart",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Home",
|
||||
@@ -510,7 +510,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "user",
|
||||
"icon": "customer",
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Customer",
|
||||
@@ -644,7 +644,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "rocket",
|
||||
"icon": "getting-started",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Maintenance",
|
||||
@@ -776,7 +776,7 @@
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"icon": "store",
|
||||
"icon": "sell",
|
||||
"indent": 1,
|
||||
"keep_closed": 1,
|
||||
"label": "Campaign",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"icon_type": "Folder",
|
||||
"idx": 1,
|
||||
"label": "Accounting",
|
||||
"link_to": "",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 17:04:04.351402",
|
||||
"modified": "2026-01-27 17:04:04.351402",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Accounting",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "archive",
|
||||
"icon": "assets",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Assets",
|
||||
"link_to": "Assets",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.220411",
|
||||
"modified": "2026-01-01 20:07:01.220411",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Assets",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "chart-pie",
|
||||
"icon": "expenses",
|
||||
"icon_type": "Link",
|
||||
"idx": 6,
|
||||
"label": "Budget",
|
||||
"link_to": "Budget",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 14:39:30.839274",
|
||||
"modified": "2026-01-23 14:39:30.839274",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Budget",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "shopping-cart",
|
||||
"icon": "buying",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Buying",
|
||||
"link_to": "Buying",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.196163",
|
||||
"modified": "2026-01-01 20:07:01.196163",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Buying",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 1,
|
||||
"icon": "handshake",
|
||||
"icon": "crm",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "CRM",
|
||||
"link_to": "CRM",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 14:54:05.112927",
|
||||
"modified": "2026-01-06 14:54:05.112927",
|
||||
"modified_by": "Administrator",
|
||||
"name": "CRM",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "settings",
|
||||
"icon": "setting",
|
||||
"icon_type": "Link",
|
||||
"idx": 10,
|
||||
"label": "ERPNext Settings",
|
||||
"link_to": "ERPNext Settings",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"logo_url": "",
|
||||
"modified": "2026-07-03 14:59:56.044037",
|
||||
"modified": "2026-01-09 14:59:56.044037",
|
||||
"modified_by": "Administrator",
|
||||
"name": "ERPNext Settings",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 1,
|
||||
"icon": "house",
|
||||
"icon": "home",
|
||||
"icon_type": "Link",
|
||||
"idx": 0,
|
||||
"label": "Home",
|
||||
"link_to": "Home",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.174950",
|
||||
"modified": "2026-01-01 20:07:01.174950",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Home",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "wallet",
|
||||
"icon": "accounting",
|
||||
"icon_type": "Link",
|
||||
"idx": 0,
|
||||
"label": "Invoicing",
|
||||
"link_to": "Invoicing",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 15:17:23.564795",
|
||||
"modified": "2026-01-23 15:17:23.564795",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Invoicing",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "factory",
|
||||
"icon": "organization",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Manufacturing",
|
||||
"link_to": "Manufacturing",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.246693",
|
||||
"modified": "2026-01-01 20:07:01.246693",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Manufacturing",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "folder-kanban",
|
||||
"icon": "project",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Projects",
|
||||
"link_to": "Projects",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.226383",
|
||||
"modified": "2026-01-01 20:07:01.226383",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Projects",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "shield-check",
|
||||
"icon": "quality",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Quality",
|
||||
"link_to": "Quality",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.239523",
|
||||
"modified": "2026-01-01 20:07:01.239523",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Quality",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "store",
|
||||
"icon": "sell",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Selling",
|
||||
"link_to": "Selling",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.189446",
|
||||
"modified": "2026-01-01 20:07:01.189446",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Selling",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "package",
|
||||
"icon": "stock",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Stock",
|
||||
"link_to": "Stock",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 20:07:01.212940",
|
||||
"modified": "2026-01-01 20:07:01.212940",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Stock",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 0,
|
||||
"icon": "package-2",
|
||||
"icon": "getting-started",
|
||||
"icon_type": "Link",
|
||||
"idx": 6,
|
||||
"label": "Subcontracting",
|
||||
"link_to": "Subcontracting",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"logo_url": "/assets/erpnext/desktop_icons/subcontracting.svg",
|
||||
"modified": "2026-07-03 20:07:01.323508",
|
||||
"modified": "2026-01-01 20:07:01.323508",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Subcontracting",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Desktop Icon",
|
||||
"hidden": 1,
|
||||
"icon": "headset",
|
||||
"icon": "support",
|
||||
"icon_type": "Link",
|
||||
"idx": 1,
|
||||
"label": "Support",
|
||||
"link_to": "Support",
|
||||
"link_type": "Workspace Sidebar",
|
||||
"modified": "2026-07-03 14:53:54.100467",
|
||||
"modified": "2026-01-06 14:53:54.100467",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Support",
|
||||
"owner": "Administrator",
|
||||
|
||||
@@ -50,7 +50,7 @@ def get_plaid_configuration():
|
||||
return "disabled"
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def add_institution(token: str, response: str | dict):
|
||||
response = frappe.parse_json(response)
|
||||
|
||||
@@ -79,7 +79,7 @@ def add_institution(token: str, response: str | dict):
|
||||
return bank
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def add_bank_accounts(response: str | dict, bank: str | dict, company: str):
|
||||
response = frappe.parse_json(response)
|
||||
bank = frappe.parse_json(bank)
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-28 10:20+0000\n"
|
||||
"PO-Revision-Date: 2026-07-01 20:39\n"
|
||||
"PO-Revision-Date: 2026-07-03 21:32\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Bosnian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -8214,7 +8214,7 @@ msgstr "Šarža {0} artikla {1} je onemogućena."
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Batch-Wise Balance History"
|
||||
msgstr "Istorija Stanja na osnovu Šarže"
|
||||
msgstr "Historija Stanja na osnovu Šarže"
|
||||
|
||||
#: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164
|
||||
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:183
|
||||
@@ -18764,7 +18764,7 @@ msgstr "Obuka Personala"
|
||||
#. Name of a DocType
|
||||
#: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json
|
||||
msgid "Employee External Work History"
|
||||
msgstr "Eksterna radna istorija Personala"
|
||||
msgstr "Eksterna Radna Historija Personala"
|
||||
|
||||
#. Label of the employee_group (Link) field in DocType 'Communication Medium
|
||||
#. Timeslot'
|
||||
@@ -18786,7 +18786,7 @@ msgstr "ID Personala"
|
||||
#. Name of a DocType
|
||||
#: erpnext/setup/doctype/employee_internal_work_history/employee_internal_work_history.json
|
||||
msgid "Employee Internal Work History"
|
||||
msgstr "Interna radna istorija Personala"
|
||||
msgstr "Eksterna Radna Historija Personala"
|
||||
|
||||
#. Label of the employee_name (Data) field in DocType 'Activity Cost'
|
||||
#. Label of the employee_name (Data) field in DocType 'Timesheet'
|
||||
@@ -20111,7 +20111,7 @@ msgstr "Prošireni Bankovni Izvod"
|
||||
#. Label of the external_work_history (Table) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "External Work History"
|
||||
msgstr "Eksterna Radna Istorija"
|
||||
msgstr "Eksterna RadnaHstorija"
|
||||
|
||||
#: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:148
|
||||
msgid "Extra Consumed Qty"
|
||||
@@ -20288,7 +20288,7 @@ msgstr "Greška: {0}"
|
||||
#. Label of the family_background (Small Text) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Family Background"
|
||||
msgstr "Porodična Istorija"
|
||||
msgstr "Porodična Historija"
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -23173,7 +23173,7 @@ msgstr "Što je veći broj, veći je prioritet"
|
||||
#. Label of the history_in_company (Section Break) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "History In Company"
|
||||
msgstr "Istorija u Poduzeću"
|
||||
msgstr "Historija u Poduzeću"
|
||||
|
||||
#: erpnext/buying/doctype/purchase_order/purchase_order.js:314
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.js:1033
|
||||
@@ -25234,7 +25234,7 @@ msgstr "Interni Prenosi"
|
||||
#. Label of the internal_work_history (Table) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Internal Work History"
|
||||
msgstr "Interna Radna Istorija"
|
||||
msgstr "Interna Radna Historija"
|
||||
|
||||
#. Description of the 'Customer Details' (Text) field in DocType 'Customer'
|
||||
#: erpnext/selling/doctype/customer/customer.json
|
||||
@@ -27986,7 +27986,7 @@ msgstr "Nabavni Registar po Artiklu"
|
||||
#: erpnext/selling/workspace/selling/selling.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Item-wise Sales History"
|
||||
msgstr "Istorija Prodaje po Artiklu"
|
||||
msgstr "Historija Prodaje po Artiklu"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Workspace Sidebar Item
|
||||
@@ -47609,7 +47609,7 @@ msgstr "Prodajna Faktura {0} mora se izbrisati prije otkazivanja ovog Prodajnog
|
||||
#. Label of the sales_monthly_history (Small Text) field in DocType 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Sales Monthly History"
|
||||
msgstr "Mjesečna Istorija Prodaje"
|
||||
msgstr "Mjesečna Historija Prodaje"
|
||||
|
||||
#: erpnext/selling/page/sales_funnel/sales_funnel.js:153
|
||||
msgid "Sales Opportunities by Campaign"
|
||||
@@ -57913,7 +57913,7 @@ msgstr "Transakcije"
|
||||
#. Label of the transactions_annual_history (Code) field in DocType 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Transactions Annual History"
|
||||
msgstr "Godišnja Istorija Transakcije"
|
||||
msgstr "Godišnja Historija Transakcije"
|
||||
|
||||
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:117
|
||||
msgid "Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-28 10:20+0000\n"
|
||||
"PO-Revision-Date: 2026-07-01 20:39\n"
|
||||
"PO-Revision-Date: 2026-07-03 21:32\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Croatian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -8214,7 +8214,7 @@ msgstr "Šarža {0} artikla {1} je onemogućena."
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Batch-Wise Balance History"
|
||||
msgstr "Istorija Stanja na osnovu Šarže"
|
||||
msgstr "Povijest Stanja na temelju Šarže"
|
||||
|
||||
#: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164
|
||||
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:183
|
||||
@@ -18764,7 +18764,7 @@ msgstr "Obrazovanje Osoblja"
|
||||
#. Name of a DocType
|
||||
#: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json
|
||||
msgid "Employee External Work History"
|
||||
msgstr "Eksterna radna povijest Osoblja"
|
||||
msgstr "Vanjska radna povijest Osoblja"
|
||||
|
||||
#. Label of the employee_group (Link) field in DocType 'Communication Medium
|
||||
#. Timeslot'
|
||||
@@ -18786,7 +18786,7 @@ msgstr "ID Osoblja"
|
||||
#. Name of a DocType
|
||||
#: erpnext/setup/doctype/employee_internal_work_history/employee_internal_work_history.json
|
||||
msgid "Employee Internal Work History"
|
||||
msgstr "Interna radna povijest Osoblja"
|
||||
msgstr "Unutarnja radna povijest Osoblja"
|
||||
|
||||
#. Label of the employee_name (Data) field in DocType 'Activity Cost'
|
||||
#. Label of the employee_name (Data) field in DocType 'Timesheet'
|
||||
@@ -20111,7 +20111,7 @@ msgstr "Prošireni Bankovni Izvod"
|
||||
#. Label of the external_work_history (Table) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "External Work History"
|
||||
msgstr "Eksterna Radna Istorija"
|
||||
msgstr "Vanjska Radna Povijest"
|
||||
|
||||
#: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.py:148
|
||||
msgid "Extra Consumed Qty"
|
||||
@@ -25234,7 +25234,7 @@ msgstr "Interni Prenosi"
|
||||
#. Label of the internal_work_history (Table) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Internal Work History"
|
||||
msgstr "Interna Radna Istorija"
|
||||
msgstr "Unutarnja Radna Povijest"
|
||||
|
||||
#. Description of the 'Customer Details' (Text) field in DocType 'Customer'
|
||||
#: erpnext/selling/doctype/customer/customer.json
|
||||
@@ -27986,7 +27986,7 @@ msgstr "Registar Nabave po Artiklu"
|
||||
#: erpnext/selling/workspace/selling/selling.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Item-wise Sales History"
|
||||
msgstr "Istorija Prodaje po Artiklu"
|
||||
msgstr "Povijest Prodaje po Artiklu"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Workspace Sidebar Item
|
||||
@@ -47609,7 +47609,7 @@ msgstr "Prodajna Faktura {0} mora se izbrisati prije otkazivanja ovog Prodajnog
|
||||
#. Label of the sales_monthly_history (Small Text) field in DocType 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Sales Monthly History"
|
||||
msgstr "Mjesečna Istorija Prodaje"
|
||||
msgstr "Mjesečna Povijest Prodaje"
|
||||
|
||||
#: erpnext/selling/page/sales_funnel/sales_funnel.js:153
|
||||
msgid "Sales Opportunities by Campaign"
|
||||
@@ -57913,7 +57913,7 @@ msgstr "Transakcije"
|
||||
#. Label of the transactions_annual_history (Code) field in DocType 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Transactions Annual History"
|
||||
msgstr "Godišnja Istorija Transakcije"
|
||||
msgstr "Godišnja Povijest Transakcija"
|
||||
|
||||
#: erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js:117
|
||||
msgid "Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-06-28 10:20+0000\n"
|
||||
"PO-Revision-Date: 2026-07-01 20:39\n"
|
||||
"PO-Revision-Date: 2026-07-03 21:32\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -5054,7 +5054,7 @@ msgstr "Fel uppstod under uppdatering process"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:368
|
||||
msgid "An error occurred for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
|
||||
msgstr "Fel uppstod för vissa artiklar när Material Begäran skapades baserat på beställning nivå. Vänligen åtgärda dessa problem:"
|
||||
msgstr "Fel uppstod för vissa artiklar när Material Begäran skapades baserat på återbeställning nivå. Vänligen åtgärda dessa problem:"
|
||||
|
||||
#: erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.html:124
|
||||
msgid "Analysis Chart"
|
||||
@@ -6451,7 +6451,7 @@ msgstr "Automatiskt Skapad"
|
||||
#. Request'
|
||||
#: erpnext/stock/doctype/material_request/material_request.json
|
||||
msgid "Auto Created (Reorder)"
|
||||
msgstr "Skapas automatiskt (ombeställning)"
|
||||
msgstr "Skapas automatiskt (återbeställning)"
|
||||
|
||||
#. Label of the auto_created_serial_and_batch_bundle (Check) field in DocType
|
||||
#. 'Stock Ledger Entry'
|
||||
@@ -6569,7 +6569,7 @@ msgstr "Automatiskt avstämning av Parti i Bank Transaktioner"
|
||||
#. Label of the reorder_section (Section Break) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Auto re-order"
|
||||
msgstr "Automatisk Ombeställning"
|
||||
msgstr "Automatisk Återbeställning"
|
||||
|
||||
#. Label of the auto_reconcile_payments (Check) field in DocType 'Accounts
|
||||
#. Settings'
|
||||
@@ -8221,7 +8221,7 @@ msgstr "Parti {0} av Artikel {1} är Inaktiverad."
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Batch-Wise Balance History"
|
||||
msgstr "Saldo Historik per Parti"
|
||||
msgstr "Partibaserad Saldo Historik"
|
||||
|
||||
#: erpnext/stock/report/fifo_queue_vs_qty_after_transaction_comparison/fifo_queue_vs_qty_after_transaction_comparison.py:164
|
||||
#: erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py:183
|
||||
@@ -9814,7 +9814,7 @@ msgstr "Kan inte demontera {0} mot Lager Post {1}. Endast {2} tillgängliga för
|
||||
|
||||
#: erpnext/setup/doctype/company/company.py:233
|
||||
msgid "Cannot enable Item-wise Inventory Account, as there are existing Stock Ledger Entries for the company {0} with Warehouse-wise Inventory Account. Please cancel the stock transactions first and try again."
|
||||
msgstr "Kan inte aktivera Lager Konto per Lager, eftersom det redan finns befintliga Lager Register Poster för {0} med Lager Konto per Lager. Avbryt lager transaktioner först och försök igen."
|
||||
msgstr "Kan inte aktivera Artikelbaserad Lager Konto, eftersom det redan finns befintliga Lager Register Poster för {0} med Lagerbaserad Lager Konto. Avbryt lager transaktioner först och försök igen."
|
||||
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.py:43
|
||||
msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
|
||||
@@ -10213,7 +10213,7 @@ msgstr "Kategori Detaljer"
|
||||
|
||||
#: erpnext/assets/dashboard_fixtures.py:93
|
||||
msgid "Category-wise Asset Value"
|
||||
msgstr "Tillgång Värde per Kategori"
|
||||
msgstr "Kategoribaserad Tillgång Värde"
|
||||
|
||||
#: erpnext/buying/doctype/purchase_order/purchase_order.py:289
|
||||
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.py:140
|
||||
@@ -15118,7 +15118,7 @@ msgstr "Kund eller Artikel"
|
||||
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.py:93
|
||||
msgid "Customer required for 'Customerwise Discount'"
|
||||
msgstr "Kund erfordras för \"Kund Rabatt\""
|
||||
msgstr "Kund erfordras för \"Kundbaserad Rabatt\""
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:885
|
||||
#: erpnext/selling/doctype/sales_order/sales_order.py:392
|
||||
@@ -15171,7 +15171,7 @@ msgstr "Kundens Leverantör"
|
||||
#. Name of a report
|
||||
#: erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.json
|
||||
msgid "Customer-wise Item Price"
|
||||
msgstr "Artikel Pris per Kund"
|
||||
msgstr "Kundbaserad Artikel Pris"
|
||||
|
||||
#: erpnext/crm/report/lost_opportunity/lost_opportunity.py:43
|
||||
msgid "Customer/Lead Name"
|
||||
@@ -15206,7 +15206,7 @@ msgstr "Kunder inte valda."
|
||||
#. Option for the 'Based On' (Select) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Customerwise Discount"
|
||||
msgstr "Rabatt per Kund"
|
||||
msgstr "Kundbaserad Rabatt"
|
||||
|
||||
#. Name of a DocType
|
||||
#. Label of the customs_tariff_number (Link) field in DocType 'Item'
|
||||
@@ -17168,7 +17168,7 @@ msgstr "Dimension Namn"
|
||||
#. Name of a report
|
||||
#: erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json
|
||||
msgid "Dimension-wise Accounts Balance Report"
|
||||
msgstr "Bokföring Saldo Rapport per Dimension"
|
||||
msgstr "Dimension baserad Bokföring Saldo Rapport"
|
||||
|
||||
#. Label of the dimensions_section (Section Break) field in DocType 'GL Entry'
|
||||
#: erpnext/accounts/doctype/gl_entry/gl_entry.json
|
||||
@@ -17872,7 +17872,7 @@ msgstr "Utvidga Ej"
|
||||
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.py:129
|
||||
msgid "Do Not Use Batchwise Valuation"
|
||||
msgstr "Använd inte Parti baserad Värdering"
|
||||
msgstr "Använd inte Partibaserad Värdering"
|
||||
|
||||
#. Label of the do_not_fetch_incoming_rate_from_serial_no (Check) field in
|
||||
#. DocType 'Stock Reposting Settings'
|
||||
@@ -18890,7 +18890,7 @@ msgstr "Aktivera Automatisk E-post"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:1171
|
||||
msgid "Enable Auto Re-Order"
|
||||
msgstr "Aktivera Automatisk Ombeställning"
|
||||
msgstr "Aktivera Automatisk Återbeställning"
|
||||
|
||||
#. Label of the enable_party_matching (Check) field in DocType 'Accounts
|
||||
#. Settings'
|
||||
@@ -18969,7 +18969,7 @@ msgstr "Aktivera Oförenderlig Bokföring"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Enable Item-wise Inventory Account"
|
||||
msgstr "Aktivera Lager Konto per Artikel"
|
||||
msgstr "Aktivera Artikelbaserad Lager Konto"
|
||||
|
||||
#. Label of the enable_loyalty_point_program (Check) field in DocType 'Accounts
|
||||
#. Settings'
|
||||
@@ -21056,7 +21056,7 @@ msgstr "Följ Kalender Månader"
|
||||
|
||||
#: erpnext/templates/emails/reorder_item.html:1
|
||||
msgid "Following Material Requests have been raised automatically based on Item's re-order level"
|
||||
msgstr "Följande Material Begäran skapades automatiskt baserat på Artikel beställning nivå"
|
||||
msgstr "Följande Material Begäran skapades automatiskt baserat på Artikel återbeställning nivå"
|
||||
|
||||
#: erpnext/selling/doctype/customer/mapper.py:173
|
||||
msgid "Following fields are mandatory to create address:"
|
||||
@@ -23792,7 +23792,7 @@ msgstr "Om artikel handlas som Noll Värdering Pris i denna post, aktivera 'Till
|
||||
#. Request Item'
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
msgid "If the reorder check is set at the Group warehouse level, the available quantity becomes the sum of the projected quantities of all its child warehouses."
|
||||
msgstr "Om ombeställning kontroll är angiven på grupp lager nivå blir tillgänglig kvantitet summa av planerad kvantitet för alla underordnade lager."
|
||||
msgstr "Om återbeställning kontroll är angiven på grupp lager nivå blir tillgänglig kvantitet summa av planerad kvantitet för alla underordnade lager."
|
||||
|
||||
#: erpnext/manufacturing/doctype/work_order/work_order.js:1286
|
||||
msgid "If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed."
|
||||
@@ -24688,7 +24688,7 @@ msgstr "Felaktig Parti Förbrukad"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:602
|
||||
msgid "Incorrect Check in (group) Warehouse for Reorder"
|
||||
msgstr "Felaktig vald (grupp) Lager för Ombeställning"
|
||||
msgstr "Felaktig vald (grupp) Lager för Återbeställning"
|
||||
|
||||
#: erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py:148
|
||||
msgid "Incorrect Company"
|
||||
@@ -27177,7 +27177,7 @@ msgstr "Artikel Grupp inte angiven i Artikel Inställningar för Artikel {0}"
|
||||
#. Option for the 'Based On' (Select) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Item Group wise Discount"
|
||||
msgstr "Rabatt per Artikel Grupp"
|
||||
msgstr "Artikel Grupp baserad Rabatt"
|
||||
|
||||
#. Label of the item_groups (Table) field in DocType 'POS Profile'
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.json
|
||||
@@ -27519,7 +27519,7 @@ msgstr "Artikel Referens"
|
||||
#: erpnext/stock/doctype/item_reorder/item_reorder.json
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
msgid "Item Reorder"
|
||||
msgstr "Artikel Ombeställning"
|
||||
msgstr "Artikel Återbeställning"
|
||||
|
||||
#. Label of the item_row (Data) field in DocType 'Item Wise Tax Detail'
|
||||
#: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json
|
||||
@@ -27730,12 +27730,12 @@ msgstr "Var Används Artikel"
|
||||
#: erpnext/stock/report/item_wise_consumption/item_wise_consumption.json
|
||||
#: erpnext/workspace_sidebar/buying.json
|
||||
msgid "Item Wise Consumption"
|
||||
msgstr "Artikelvis Förbrukning"
|
||||
msgstr "Artikelbaserad Förbrukning"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json
|
||||
msgid "Item Wise Tax Detail"
|
||||
msgstr "Moms Detalj per Artikel"
|
||||
msgstr "Artikelbaserad Moms Detalj"
|
||||
|
||||
#. Label of the item_wise_tax_details (Table) field in DocType 'POS Invoice'
|
||||
#. Label of the item_wise_tax_details (Table) field in DocType 'Purchase
|
||||
@@ -27759,11 +27759,11 @@ msgstr "Moms Detalj per Artikel"
|
||||
#: erpnext/stock/doctype/delivery_note/delivery_note.json
|
||||
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
|
||||
msgid "Item Wise Tax Details"
|
||||
msgstr "Artikel Moms Detaljer"
|
||||
msgstr "Artikelbaserade Moms Detaljer"
|
||||
|
||||
#: erpnext/controllers/taxes_and_totals.py:562
|
||||
msgid "Item Wise Tax Details do not match with Taxes and Charges at the following rows:"
|
||||
msgstr "Artikel Moms Detaljer stämmer inte överens med Moms och Avgifter på följande rader:"
|
||||
msgstr "Artikelbaserade Moms Detaljer stämmer inte med Moms och Avgifter på följande rader:"
|
||||
|
||||
#. Label of the section_break_rrrx (Section Break) field in DocType 'Sales
|
||||
#. Forecast'
|
||||
@@ -27967,7 +27967,7 @@ msgstr "Artikel {0}: {1} Kvantitet producerad ."
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/item_wise_price_list_rate/item_wise_price_list_rate.json
|
||||
msgid "Item-wise Price List Rate"
|
||||
msgstr "Prislista Pris per Artikel"
|
||||
msgstr "Artikelbaserad Prislista Pris "
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Buying Workspace
|
||||
@@ -27976,14 +27976,14 @@ msgstr "Prislista Pris per Artikel"
|
||||
#: erpnext/buying/workspace/buying/buying.json
|
||||
#: erpnext/workspace_sidebar/buying.json
|
||||
msgid "Item-wise Purchase History"
|
||||
msgstr "Inköp Historik per Artikel"
|
||||
msgstr "Artikelbaserad Inköp Historik"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.json
|
||||
#: erpnext/workspace_sidebar/financial_reports.json
|
||||
msgid "Item-wise Purchase Register"
|
||||
msgstr "Inköp Register per Artikel"
|
||||
msgstr "Artikelbaserad Inköp Register"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Selling Workspace
|
||||
@@ -27992,19 +27992,19 @@ msgstr "Inköp Register per Artikel"
|
||||
#: erpnext/selling/workspace/selling/selling.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Item-wise Sales History"
|
||||
msgstr "Försäljning Historik per Artikel"
|
||||
msgstr "Artikelbaserad Försäljning Historik"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Item-wise Sales Register"
|
||||
msgstr "Försäljning Register per Artikel"
|
||||
msgstr "Artikelbaserad Försäljning Register"
|
||||
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/workspace_sidebar/financial_reports.json
|
||||
msgid "Item-wise sales Register"
|
||||
msgstr "Försäljning Register per Artikel"
|
||||
msgstr "Artikelbaserad Försäljning Register"
|
||||
|
||||
#: erpnext/stock/get_item_details.py:769
|
||||
msgid "Item/Item Code required to get Item Tax Template."
|
||||
@@ -28111,7 +28111,7 @@ msgstr "Artikel {0} saknas i Artikel Register."
|
||||
#. Option for the 'Based On' (Select) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Itemwise Discount"
|
||||
msgstr "Rabatt per Artikel"
|
||||
msgstr "Artikelbaserad Rabatt"
|
||||
|
||||
#. Name of a report
|
||||
#. Label of a Link in the Stock Workspace
|
||||
@@ -28120,7 +28120,7 @@ msgstr "Rabatt per Artikel"
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Itemwise Recommended Reorder Level"
|
||||
msgstr "Rekommenderad Ombeställning Nivå per Artikel"
|
||||
msgstr "Artikelbaserad Rekommenderad Återbeställning Nivå"
|
||||
|
||||
#. Option for the 'Barcode Type' (Select) field in DocType 'Item Barcode'
|
||||
#: erpnext/stock/doctype/item_barcode/item_barcode.json
|
||||
@@ -40686,16 +40686,16 @@ msgstr "Projekt kommer att vara tillgänglig på hemsida till dessa Användare"
|
||||
#: erpnext/projects/workspace/projects/projects.json
|
||||
#: erpnext/workspace_sidebar/projects.json
|
||||
msgid "Project wise Stock Tracking"
|
||||
msgstr "Lager Spårning per Projekt"
|
||||
msgstr "Projektbaserad Lager Spårning"
|
||||
|
||||
#. Name of a report
|
||||
#: erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.json
|
||||
msgid "Project wise Stock Tracking "
|
||||
msgstr "Lager Spårning per Projekt"
|
||||
msgstr "Projektbaserad Lager Spårning "
|
||||
|
||||
#: erpnext/controllers/trends.py:457
|
||||
msgid "Project-wise data is not available for Quotation"
|
||||
msgstr "Data per Projekt finns inte tillgängligt för Försäljning Offert"
|
||||
msgstr "Projektbaserad data är inte tillgängligt för Försäljning Offert"
|
||||
|
||||
#. Label of the projected_on_hand (Float) field in DocType 'Material Request
|
||||
#. Item'
|
||||
@@ -41821,7 +41821,7 @@ msgstr "Kvantitet att Producera"
|
||||
|
||||
#: erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py:56
|
||||
msgid "Qty Wise Chart"
|
||||
msgstr "Kvantitet Diagram"
|
||||
msgstr "Kvantitetbaserad Diagram"
|
||||
|
||||
#. Label of the section_break_6 (Section Break) field in DocType 'Asset
|
||||
#. Capitalization Service Item'
|
||||
@@ -42669,7 +42669,7 @@ msgstr "Inköp Offerter är inte tillåtna för {0} på grund av Resultat Kort v
|
||||
#. Label of the auto_indent (Check) field in DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
msgid "Raise Material Request when stock reaches re-order level"
|
||||
msgstr "Skapa Material Begäran när Lager når ombeställning nivå"
|
||||
msgstr "Skapa Material Begäran när Lager når återbeställning nivå"
|
||||
|
||||
#. Label of the complaint_raised_by (Data) field in DocType 'Warranty Claim'
|
||||
#: erpnext/support/doctype/warranty_claim/warranty_claim.json
|
||||
@@ -43168,12 +43168,12 @@ msgstr "Återöppna"
|
||||
#. Label of the warehouse_reorder_level (Float) field in DocType 'Item Reorder'
|
||||
#: erpnext/stock/doctype/item_reorder/item_reorder.json
|
||||
msgid "Re-order Level"
|
||||
msgstr "Ombeställning Nivå"
|
||||
msgstr "Återbeställning Nivå"
|
||||
|
||||
#. Label of the warehouse_reorder_qty (Float) field in DocType 'Item Reorder'
|
||||
#: erpnext/stock/doctype/item_reorder/item_reorder.json
|
||||
msgid "Re-order Qty"
|
||||
msgstr "Ombeställning Kvantitet"
|
||||
msgstr "Återbeställning Kvantitet"
|
||||
|
||||
#: erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py:227
|
||||
msgid "Reached Root"
|
||||
@@ -44313,18 +44313,18 @@ msgstr "Hyrd"
|
||||
#: erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py:64
|
||||
#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:213
|
||||
msgid "Reorder Level"
|
||||
msgstr "Ombeställning Nivå"
|
||||
msgstr "Återbeställning Nivå"
|
||||
|
||||
#. Label of the reorder_qty (Float) field in DocType 'Material Request Item'
|
||||
#: erpnext/stock/doctype/material_request_item/material_request_item.json
|
||||
#: erpnext/stock/report/stock_projected_qty/stock_projected_qty.py:220
|
||||
msgid "Reorder Qty"
|
||||
msgstr "Ombeställning Kvantitet"
|
||||
msgstr "Återbeställning Kvantitet"
|
||||
|
||||
#. Label of the reorder_levels (Table) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Reorder level based on Warehouse"
|
||||
msgstr "Ombeställning Nivå Baserad på Lager"
|
||||
msgstr "Återbeställning Nivå Baserad på Lager"
|
||||
|
||||
#. Option for the 'Purpose' (Select) field in DocType 'Stock Entry'
|
||||
#. Option for the 'Purpose' (Select) field in DocType 'Stock Entry Type'
|
||||
@@ -46411,7 +46411,7 @@ msgstr "Rad #{0}: Välj Underenhet Lager"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:590
|
||||
msgid "Row #{0}: Please set reorder quantity"
|
||||
msgstr "Rad # {0}: Ange Ombeställning Kvantitet"
|
||||
msgstr "Rad #{0}: Ange Återbeställning Kvantitet"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:522
|
||||
msgid "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master"
|
||||
@@ -48044,7 +48044,7 @@ msgstr "Säljare Mål"
|
||||
#: erpnext/selling/workspace/selling/selling.json
|
||||
#: erpnext/workspace_sidebar/selling.json
|
||||
msgid "Sales Person-wise Transaction Summary"
|
||||
msgstr "Transaktion Översikt per Säljare"
|
||||
msgstr "Säljarebaserad Transaktion Översikt"
|
||||
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/selling/page/sales_funnel/sales_funnel.js:50
|
||||
@@ -49993,7 +49993,7 @@ msgstr "Ange Total Summa till Standard Betalning Metod"
|
||||
#. 'Territory'
|
||||
#: erpnext/setup/doctype/territory/territory.json
|
||||
msgid "Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution."
|
||||
msgstr "Ange Budget per Artikel Grupp för detta Distrikt. Man kan även inkludera säsongvariationer genom att ange Fördelning."
|
||||
msgstr "Ange Artikel Grupp baserad Budget för detta Distrikt. Inkludera även säsongvariationer genom att ange Fördelning."
|
||||
|
||||
#. Label of the set_landed_cost_based_on_purchase_invoice_rate (Check) field in
|
||||
#. DocType 'Buying Settings'
|
||||
@@ -50726,7 +50726,7 @@ msgstr "Visa Kumulativ Belopp"
|
||||
|
||||
#: erpnext/stock/report/stock_balance/stock_balance.js:143
|
||||
msgid "Show Dimension Wise Stock"
|
||||
msgstr "Visa Lager per Dimension"
|
||||
msgstr "Visa Dimensionbaserad Lager"
|
||||
|
||||
#: erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js:29
|
||||
msgid "Show Disabled Items"
|
||||
@@ -50863,7 +50863,7 @@ msgstr "Visa Varianter"
|
||||
|
||||
#: erpnext/stock/report/stock_ageing/stock_ageing.js:64
|
||||
msgid "Show Warehouse-wise Stock"
|
||||
msgstr "Visa Lager Värde per Lager"
|
||||
msgstr "Visa Lagerbaserad Lager Värde"
|
||||
|
||||
#: erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js:26
|
||||
msgid "Show availability of exploded items"
|
||||
@@ -55085,7 +55085,7 @@ msgstr "Distrikt Mål"
|
||||
#. Name of a report
|
||||
#: erpnext/selling/report/territory_wise_sales/territory_wise_sales.json
|
||||
msgid "Territory-wise Sales"
|
||||
msgstr "Försäljning per Distrikt"
|
||||
msgstr "Distriktbaserad Försäljning"
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -55656,7 +55656,7 @@ msgstr "{0} innehåller Enhet Pris Artiklar."
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:491
|
||||
msgid "The {0} prefix '{1}' already exists. Please change the Serial No Series, otherwise you will get a Duplicate Entry error."
|
||||
msgstr "Prefix {0} '{1}' finns redan. Ändra serienummer, annars blir det dubblett post."
|
||||
msgstr "Prefix {0} '{1}' finns redan. Ändra serie nummer, annars blir det Dubbel Post."
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:572
|
||||
msgid "The {0} {1} created successfully"
|
||||
@@ -60435,7 +60435,7 @@ msgstr "Verifikat {0} är övertilldelad av {1}"
|
||||
#. Name of a report
|
||||
#: erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json
|
||||
msgid "Voucher-wise Balance"
|
||||
msgstr "Saldo per Verifikat"
|
||||
msgstr "Verifikatbaserad Saldo"
|
||||
|
||||
#. Label of the vouchers (Table) field in DocType 'Repost Accounting Ledger'
|
||||
#. Label of the selected_vouchers_section (Section Break) field in DocType
|
||||
@@ -60563,7 +60563,7 @@ msgstr "Lager Typ"
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Warehouse Wise Stock Balance"
|
||||
msgstr "Lager Saldo per Lager"
|
||||
msgstr "Lagerbaserad Lager Saldo"
|
||||
|
||||
#. Label of the warehouse_and_reference (Section Break) field in DocType
|
||||
#. 'Request for Quotation Item'
|
||||
@@ -60616,7 +60616,7 @@ msgstr "Lager erfodras för Lager Artikel {0}"
|
||||
#. Name of a report
|
||||
#: erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.json
|
||||
msgid "Warehouse wise Item Balance Age and Value"
|
||||
msgstr "Artikel Saldo Ålder och Värde per Lager"
|
||||
msgstr "Lagerbaserad Artikel Saldo, Ålder och Värde"
|
||||
|
||||
#: erpnext/stock/doctype/warehouse/warehouse.py:95
|
||||
msgid "Warehouse {0} can not be deleted as quantity exists for Item {1}"
|
||||
@@ -61926,7 +61926,7 @@ msgstr "Du har inte utfört några avstämningar i denna sessionen ännu."
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:1170
|
||||
msgid "You have to enable auto re-order in Stock Settings to maintain re-order levels."
|
||||
msgstr "Du måste aktivera automatisk ombeställning i lager inställningar för att behålla ombeställning nivåer."
|
||||
msgstr "Du måste aktivera automatisk återbeställning i Lager Inställningar för att behålla återbeställning nivåer."
|
||||
|
||||
#: erpnext/selling/page/point_of_sale/pos_controller.js:272
|
||||
msgid "You have unsaved changes. Do you want to save the invoice?"
|
||||
@@ -62020,7 +62020,7 @@ msgstr "Zip Fil"
|
||||
|
||||
#: erpnext/stock/reorder_item.py:364
|
||||
msgid "[Important] [ERPNext] Auto Reorder Errors"
|
||||
msgstr "[Viktigt] [System] Automatisk Ombeställning Fel"
|
||||
msgstr "[Viktigt] [System] Automatisk Återbeställning Fel"
|
||||
|
||||
#: erpnext/controllers/status_updater.py:306
|
||||
msgid "`Allow Negative rates for Items`"
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Open\nWork In Progress\nPartially Transferred\nMaterial Transferred\nOn Hold\nSubmitted\nTo Manufacture\nCancelled\nCompleted",
|
||||
"options": "Open\nWork In Progress\nPartially Transferred\nMaterial Transferred\nOn Hold\nSubmitted\nCancelled\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -695,7 +695,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-20 17:39:42.293242",
|
||||
"modified": "2026-06-19 17:39:42.293242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -131,7 +131,6 @@ class JobCard(Document):
|
||||
"Material Transferred",
|
||||
"On Hold",
|
||||
"Submitted",
|
||||
"To Manufacture",
|
||||
"Cancelled",
|
||||
"Completed",
|
||||
]
|
||||
@@ -1303,13 +1302,8 @@ class JobCard(Document):
|
||||
self.update_workstation_status()
|
||||
|
||||
def set_finished_good_status(self):
|
||||
# Only reached for a submitted job card (docstatus == 1) with a finished good, see set_status().
|
||||
if (self.manufactured_qty + self.process_loss_qty) >= self.for_quantity:
|
||||
self.status = "Completed"
|
||||
elif (self.total_completed_qty + self.process_loss_qty) >= self.for_quantity:
|
||||
# Production is done and the card is submitted, but the finished goods have not been
|
||||
# booked into stock yet (Manufacture Stock Entry pending) — distinct from active WIP.
|
||||
self.status = "To Manufacture"
|
||||
elif self.transferred_qty > 0 or self.skip_material_transfer:
|
||||
self.status = "Work In Progress"
|
||||
|
||||
|
||||
@@ -86,10 +86,6 @@ def make_material_request(source_name: str, target_doc: Document | str | None =
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_entry(source_name: str, target_doc: Document | str | None = None):
|
||||
from erpnext.stock.doctype.stock_entry.services.manufacturing import (
|
||||
set_previous_operation_serial_batch,
|
||||
)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
target.t_warehouse = source_parent.wip_warehouse
|
||||
|
||||
@@ -129,7 +125,6 @@ def make_stock_entry(source_name: str, target_doc: Document | str | None = None)
|
||||
wo_allows_alternate_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")
|
||||
)
|
||||
set_previous_operation_serial_batch(target, item)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Job Card",
|
||||
|
||||
@@ -1061,9 +1061,6 @@ class TestJobCard(ERPNextTestSuite):
|
||||
job_card.submit()
|
||||
|
||||
for row in fg_bom.items:
|
||||
if row.item_code == sfg.name:
|
||||
continue
|
||||
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target="Stores - _TC",
|
||||
@@ -1074,301 +1071,9 @@ class TestJobCard(ERPNextTestSuite):
|
||||
manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
|
||||
manufacturing_entry.submit()
|
||||
|
||||
sfg_row = next(row for row in manufacturing_entry.items if row.item_code == sfg.name)
|
||||
self.assertEqual(flt(sfg_row.basic_rate, 3), 95.0)
|
||||
|
||||
self.assertEqual(manufacturing_entry.items[2].item_code, scrap2.name)
|
||||
self.assertEqual(manufacturing_entry.items[2].qty, 9)
|
||||
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.278)
|
||||
|
||||
def test_semi_fg_batch_auto_pull_on_manufacture(self):
|
||||
"""Batch produced by an operation should auto-pull into the next operation's
|
||||
semi-finished consumption row (skip-transfer Manufacture entry)."""
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
|
||||
frappe.db.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)
|
||||
warehouse = "Stores - _TC"
|
||||
|
||||
rm1 = make_item("Auto Pull RM 1", {"is_stock_item": 1}).name
|
||||
rm2 = make_item("Auto Pull RM 2", {"is_stock_item": 1}).name
|
||||
fg1 = make_item("Auto Pull FG 1", {"is_stock_item": 1}).name
|
||||
sfg = make_item(
|
||||
"Auto Pull SFG 1",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "AP-SFG-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
sfg_bom = frappe.new_doc("BOM", company="_Test Company", item=sfg, quantity=1)
|
||||
sfg_bom.append("items", {"item_code": rm1, "qty": 1})
|
||||
sfg_bom.insert()
|
||||
sfg_bom.submit()
|
||||
|
||||
fg_bom = frappe.new_doc(
|
||||
"BOM",
|
||||
company="_Test Company",
|
||||
item=fg1,
|
||||
quantity=1,
|
||||
with_operations=1,
|
||||
track_semi_finished_goods=1,
|
||||
)
|
||||
fg_bom.append("items", {"item_code": rm2, "qty": 1})
|
||||
|
||||
operation1 = {
|
||||
"operation": "Auto Pull Op A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"finished_good": sfg,
|
||||
"bom_no": sfg_bom.name,
|
||||
"finished_good_qty": 1,
|
||||
"sequence_id": 1,
|
||||
"time_in_mins": 60,
|
||||
"source_warehouse": warehouse,
|
||||
"fg_warehouse": warehouse,
|
||||
"skip_material_transfer": 1,
|
||||
}
|
||||
operation2 = {
|
||||
"operation": "Auto Pull Op B",
|
||||
"workstation": "_Test Workstation A",
|
||||
"finished_good": fg1,
|
||||
"finished_good_qty": 1,
|
||||
"is_final_finished_good": 1,
|
||||
"sequence_id": 2,
|
||||
"time_in_mins": 60,
|
||||
"source_warehouse": warehouse,
|
||||
"fg_warehouse": warehouse,
|
||||
"skip_material_transfer": 1,
|
||||
}
|
||||
|
||||
make_workstation(operation1)
|
||||
make_operation(operation1)
|
||||
make_operation(operation2)
|
||||
|
||||
fg_bom.append("operations", operation1)
|
||||
fg_bom.append("operations", operation2)
|
||||
fg_bom.append("items", {"item_code": sfg, "qty": 1, "uom": "Nos", "operation_row_id": 2})
|
||||
fg_bom.insert()
|
||||
fg_bom.submit()
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item=fg1,
|
||||
qty=5,
|
||||
source_warehouse=warehouse,
|
||||
fg_warehouse=warehouse,
|
||||
bom_no=fg_bom.name,
|
||||
skip_transfer=1,
|
||||
do_not_save=True,
|
||||
)
|
||||
work_order.operations[0].time_in_mins = 60
|
||||
work_order.operations[1].time_in_mins = 60
|
||||
work_order.save()
|
||||
work_order.submit()
|
||||
|
||||
make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100)
|
||||
make_stock_entry(item_code=rm2, target=warehouse, qty=10, basic_rate=100)
|
||||
|
||||
# Operation A -> produces the SFG batch
|
||||
jc_a = frappe.get_doc(
|
||||
"Job Card",
|
||||
frappe.db.get_value(
|
||||
"Job Card", {"work_order": work_order.name, "operation": "Auto Pull Op A"}, "name"
|
||||
),
|
||||
)
|
||||
jc_a.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2024-01-01 08:00:00",
|
||||
"to_time": "2024-01-01 09:00:00",
|
||||
"completed_qty": jc_a.for_quantity,
|
||||
},
|
||||
)
|
||||
jc_a.submit()
|
||||
me_a = frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item())
|
||||
me_a.submit()
|
||||
|
||||
me_a.reload()
|
||||
sfg_fg_row = next(r for r in me_a.items if r.is_finished_item and r.item_code == sfg)
|
||||
self.assertTrue(sfg_fg_row.serial_and_batch_bundle)
|
||||
produced_batches = get_batches_from_bundle(sfg_fg_row.serial_and_batch_bundle)
|
||||
|
||||
# Operation B -> consumes the SFG; its batch should be auto-pulled from Operation A
|
||||
jc_b = frappe.get_doc(
|
||||
"Job Card",
|
||||
frappe.db.get_value(
|
||||
"Job Card", {"work_order": work_order.name, "operation": "Auto Pull Op B"}, "name"
|
||||
),
|
||||
)
|
||||
jc_b.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2024-02-01 08:00:00",
|
||||
"to_time": "2024-02-01 09:00:00",
|
||||
"completed_qty": jc_b.for_quantity,
|
||||
},
|
||||
)
|
||||
jc_b.submit()
|
||||
me_b = frappe.get_doc(jc_b.make_stock_entry_for_semi_fg_item())
|
||||
|
||||
sfg_consume_row = next(r for r in me_b.items if r.item_code == sfg and r.s_warehouse)
|
||||
self.assertTrue(
|
||||
sfg_consume_row.serial_and_batch_bundle,
|
||||
"Previous operation's batch was not auto-pulled into the semi-finished consumption row",
|
||||
)
|
||||
consumed_batches = get_batches_from_bundle(sfg_consume_row.serial_and_batch_bundle)
|
||||
self.assertEqual(set(consumed_batches.keys()), set(produced_batches.keys()))
|
||||
|
||||
def test_semi_fg_auto_pull_with_uom_conversion(self):
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.services.manufacturing import (
|
||||
set_previous_operation_serial_batch,
|
||||
)
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
|
||||
frappe.db.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||
frappe.db.set_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order", 0)
|
||||
warehouse = "Stores - _TC"
|
||||
|
||||
rm1 = make_item("UOM Pull RM 1", {"is_stock_item": 1}).name
|
||||
rm2 = make_item("UOM Pull RM 2", {"is_stock_item": 1}).name
|
||||
fg1 = make_item("UOM Pull FG 1", {"is_stock_item": 1}).name
|
||||
sfg = make_item(
|
||||
"UOM Pull SFG 1",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "UP-SFG-.#####",
|
||||
"uoms": [{"uom": "Box", "conversion_factor": 5}],
|
||||
},
|
||||
).name
|
||||
|
||||
sfg_bom = frappe.new_doc("BOM", company="_Test Company", item=sfg, quantity=1)
|
||||
sfg_bom.append("items", {"item_code": rm1, "qty": 1})
|
||||
sfg_bom.insert()
|
||||
sfg_bom.submit()
|
||||
|
||||
fg_bom = frappe.new_doc(
|
||||
"BOM",
|
||||
company="_Test Company",
|
||||
item=fg1,
|
||||
quantity=1,
|
||||
with_operations=1,
|
||||
track_semi_finished_goods=1,
|
||||
)
|
||||
fg_bom.append("items", {"item_code": rm2, "qty": 1})
|
||||
|
||||
operation1 = {
|
||||
"operation": "UOM Pull Op A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"finished_good": sfg,
|
||||
"bom_no": sfg_bom.name,
|
||||
"finished_good_qty": 1,
|
||||
"sequence_id": 1,
|
||||
"time_in_mins": 60,
|
||||
"source_warehouse": warehouse,
|
||||
"fg_warehouse": warehouse,
|
||||
"skip_material_transfer": 1,
|
||||
}
|
||||
operation2 = {
|
||||
"operation": "UOM Pull Op B",
|
||||
"workstation": "_Test Workstation A",
|
||||
"finished_good": fg1,
|
||||
"finished_good_qty": 1,
|
||||
"is_final_finished_good": 1,
|
||||
"sequence_id": 2,
|
||||
"time_in_mins": 60,
|
||||
"source_warehouse": warehouse,
|
||||
"fg_warehouse": warehouse,
|
||||
"skip_material_transfer": 1,
|
||||
}
|
||||
|
||||
make_workstation(operation1)
|
||||
make_operation(operation1)
|
||||
make_operation(operation2)
|
||||
|
||||
fg_bom.append("operations", operation1)
|
||||
fg_bom.append("operations", operation2)
|
||||
fg_bom.append("items", {"item_code": sfg, "qty": 1, "uom": "Nos", "operation_row_id": 2})
|
||||
fg_bom.insert()
|
||||
fg_bom.submit()
|
||||
|
||||
work_order = make_wo_order_test_record(
|
||||
item=fg1,
|
||||
qty=5,
|
||||
source_warehouse=warehouse,
|
||||
fg_warehouse=warehouse,
|
||||
bom_no=fg_bom.name,
|
||||
skip_transfer=1,
|
||||
do_not_save=True,
|
||||
)
|
||||
work_order.operations[0].time_in_mins = 60
|
||||
work_order.operations[1].time_in_mins = 60
|
||||
work_order.save()
|
||||
work_order.submit()
|
||||
|
||||
make_stock_entry(item_code=rm1, target=warehouse, qty=10, basic_rate=100)
|
||||
make_stock_entry(item_code=sfg, target=warehouse, qty=5, basic_rate=100, posting_date="2024-01-01")
|
||||
|
||||
jc_a = frappe.get_doc(
|
||||
"Job Card",
|
||||
frappe.db.get_value(
|
||||
"Job Card", {"work_order": work_order.name, "operation": "UOM Pull Op A"}, "name"
|
||||
),
|
||||
)
|
||||
jc_a.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2024-02-01 08:00:00",
|
||||
"to_time": "2024-02-01 09:00:00",
|
||||
"completed_qty": jc_a.for_quantity,
|
||||
},
|
||||
)
|
||||
jc_a.submit()
|
||||
me_a = frappe.get_doc(jc_a.make_stock_entry_for_semi_fg_item())
|
||||
me_a.submit()
|
||||
me_a.reload()
|
||||
|
||||
sfg_fg_row = next(r for r in me_a.items if r.is_finished_item and r.item_code == sfg)
|
||||
produced_batches = get_batches_from_bundle(sfg_fg_row.serial_and_batch_bundle)
|
||||
|
||||
se = frappe.new_doc("Stock Entry")
|
||||
se.company = "_Test Company"
|
||||
se.purpose = "Material Transfer"
|
||||
se.work_order = work_order.name
|
||||
se.set_stock_entry_type()
|
||||
row = se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": sfg,
|
||||
"qty": 1,
|
||||
"uom": "Box",
|
||||
"conversion_factor": 5,
|
||||
"s_warehouse": warehouse,
|
||||
"t_warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
)
|
||||
set_previous_operation_serial_batch(se, row)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
self.assertEqual(
|
||||
abs(frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty")),
|
||||
5.0,
|
||||
)
|
||||
|
||||
se.save()
|
||||
se.submit()
|
||||
se.reload()
|
||||
|
||||
row = se.items[0]
|
||||
consumed_batches = get_batches_from_bundle(row.serial_and_batch_bundle)
|
||||
self.assertEqual(set(consumed_batches.keys()), set(produced_batches.keys()))
|
||||
self.assertEqual(abs(sum(consumed_batches.values())), 5.0)
|
||||
self.assertEqual(flt(manufacturing_entry.items[2].basic_rate, 3), 5.556)
|
||||
|
||||
def test_secondary_items_without_sfg(self):
|
||||
for row in frappe.get_doc("BOM", self.work_order.bom_no).items:
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"sub_operations",
|
||||
"total_operation_time",
|
||||
"section_break_4",
|
||||
"description",
|
||||
"work_instruction"
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -44,12 +43,6 @@
|
||||
"in_preview": 1,
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"description": "Shown to operators on the Shop Floor. Supports rich text and embedded images for step-by-step guidance.",
|
||||
"fieldname": "work_instruction",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Work Instructions"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sub_operations_section",
|
||||
|
||||
@@ -25,7 +25,6 @@ class Operation(Document):
|
||||
quality_inspection_template: DF.Link | None
|
||||
sub_operations: DF.Table[SubOperation]
|
||||
total_operation_time: DF.Float
|
||||
work_instruction: DF.TextEditor | None
|
||||
workstation: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
|
||||
@@ -169,29 +169,6 @@ class OperationsService:
|
||||
|
||||
self.doc.set("operations", operations)
|
||||
self.calculate_time()
|
||||
self.set_operation_warehouses()
|
||||
|
||||
def set_operation_warehouses(self):
|
||||
"""For semi-finished goods tracking, default each operation's warehouses from the Work
|
||||
Order and chain them: the first operation pulls from the WO source warehouse and every
|
||||
later operation pulls from the previous operation's output; intermediate outputs go to the
|
||||
WIP warehouse while the final operation outputs to the WO finished goods warehouse.
|
||||
|
||||
Only empty fields are filled, so values configured on the BOM/operation are preserved."""
|
||||
if not self.doc.track_semi_finished_goods or not self.doc.operations:
|
||||
return
|
||||
|
||||
operations = self.doc.operations
|
||||
last_idx = len(operations) - 1
|
||||
for idx, op in enumerate(operations):
|
||||
if not op.source_warehouse:
|
||||
op.source_warehouse = self.doc.source_warehouse
|
||||
|
||||
if not op.fg_warehouse:
|
||||
op.fg_warehouse = self.doc.fg_warehouse if idx == last_idx else self.doc.source_warehouse
|
||||
|
||||
if not op.wip_warehouse:
|
||||
op.wip_warehouse = self.doc.wip_warehouse
|
||||
|
||||
def _collect_bom_operations(self):
|
||||
operations = []
|
||||
|
||||
@@ -691,28 +691,6 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
ste.save()
|
||||
self.assertEqual(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC")
|
||||
|
||||
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 0})
|
||||
def test_cost_center_for_manufacture_falls_back_to_item_group_default(self):
|
||||
# "_Test Item Group" is master data with buying_cost_center already set to
|
||||
# "_Test Cost Center 2 - _TC" for "_Test Company"; only the FG item and its
|
||||
# BOM need to be created, since no existing item in that group has one.
|
||||
fg_item = make_item(
|
||||
"_Test FG Item For Item Group Cost Center",
|
||||
{"is_stock_item": 1, "item_group": "_Test Item Group", "include_item_in_manufacturing": 1},
|
||||
)
|
||||
|
||||
if not frappe.db.exists("BOM", {"item": fg_item.name, "is_active": 1, "is_default": 1}):
|
||||
make_bom(item=fg_item.name, raw_materials=["_Test Item"])
|
||||
|
||||
wo_order = make_wo_order_test_record(
|
||||
production_item=fg_item.name, skip_transfer=1, source_warehouse="_Test Warehouse - _TC"
|
||||
)
|
||||
ste = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", wo_order.qty))
|
||||
ste.insert()
|
||||
|
||||
fg_row = next(d for d in ste.items if d.is_finished_item)
|
||||
self.assertEqual(fg_row.cost_center, "_Test Cost Center 2 - _TC")
|
||||
|
||||
def test_operation_time_with_batch_size(self):
|
||||
fg_item = "Test Batch Size Item For BOM"
|
||||
rm1 = "Test Batch Size Item RM 1 For BOM"
|
||||
|
||||
@@ -203,15 +203,6 @@ frappe.ui.form.on("Work Order", {
|
||||
}
|
||||
}
|
||||
|
||||
let pending_ops = frm.doc?.operations?.filter((op) => op.completed_qty < frm.doc.qty);
|
||||
// Jump to the operator Shop Floor view, pre-filtered to this work order.
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status !== "Closed" && pending_ops && pending_ops.length > 0) {
|
||||
frm.add_custom_button(__("Operator Dashboard"), () => {
|
||||
frappe.route_options = { work_order: frm.doc.name };
|
||||
frappe.set_route("shop-floor");
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Completed") {
|
||||
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
|
||||
frm.add_custom_button(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user