Compare commits

..

42 Commits

Author SHA1 Message Date
Nabin Hait
e0ea8eee1a test: cover untested Payment Entry field validations 2026-07-03 16:15:24 +05:30
Nabin Hait
23e3dd94c0 Merge pull request #56831 from frappe/chore/test-process-payment-reconciliation
fix: Process Payment Reconciliation drops bank/cash and cost center filters
2026-07-03 14:31:17 +05:30
Nabin Hait
6255d99fda Merge pull request #56827 from frappe/chore/test-subscription-plan
fix: Subscription Plan Monthly Rate under-bills across a year boundary
2026-07-03 14:30:25 +05:30
Nabin Hait
81d5eac0ea Merge pull request #56824 from frappe/chore/test-journal-entry-template
fix: Journal Entry Template rows must belong to its company
2026-07-03 14:29:28 +05:30
Nabin Hait
8632019d2a Merge pull request #56830 from frappe/chore/test-account-closing-balance
test: add coverage for Account Closing Balance
2026-07-03 14:22:40 +05:30
Nabin Hait
c9960b4d51 fix: carry bank/cash account and cost center into Payment Reconciliation 2026-07-03 14:16:46 +05:30
Nabin Hait
2cc02e61d9 fix: validate Journal Entry Template rows belong to its company 2026-07-03 14:15:19 +05:30
Nabin Hait
6fc28edde9 Merge pull request #56828 from frappe/chore/test-cashier-closing
test: add coverage for Cashier Closing
2026-07-03 14:08:55 +05:30
Nabin Hait
196730c535 fix: bill all months across a year boundary in Monthly Rate plans 2026-07-03 14:08:20 +05:30
Nabin Hait
4d39f698bd Merge pull request #56823 from frappe/chore/test-party-link
test: add coverage for Party Link
2026-07-03 14:07:04 +05:30
Nabin Hait
0a7abe7144 Merge pull request #56822 from frappe/chore/test-mode-of-payment
test: add coverage for Mode of Payment
2026-07-03 14:06:50 +05:30
Nabin Hait
5888cdf3a0 Merge pull request #56821 from frappe/chore/test-item-tax-template
test: add coverage for Item Tax Template
2026-07-03 14:04:03 +05:30
Nabin Hait
a1f413e8a8 Merge pull request #56820 from frappe/chore/test-monthly-distribution
test: add coverage for Monthly Distribution
2026-07-03 14:03:36 +05:30
Nabin Hait
cd167bdd40 Merge pull request #56819 from frappe/chore/test-bank-guarantee
test: add coverage for Bank Guarantee
2026-07-03 14:02:53 +05:30
Mihir Kandoi
5d48c44bbb Merge pull request #56826 from frappe/fix/stock-ledger-invariant-check-report
fix: FIFO queue checks and incorrect entries filter in stock ledger reports
2026-07-03 13:19:29 +05:30
rohitwaghchaure
ecc8ec672b fix: replay immutable SLE qty for serial/batch bundle valuation (#56814) 2026-07-03 12:15:07 +05:30
Mihir Kandoi
3b1e57966e test: drop redundant cleanup, db rolls back after each test 2026-07-03 12:12:46 +05:30
Nabin Hait
974571aba7 test: guard account lookups and cover dropped pr_instance filters 2026-07-03 12:08:11 +05:30
Nabin Hait
7d917e497a test: assert account-currency sums carry through the merge 2026-07-03 12:06:56 +05:30
Nabin Hait
e041e33860 test: reload invoice for outstanding and cover equal-time boundary 2026-07-03 12:06:20 +05:30
Nabin Hait
832b5a56bf test: lock current cross-year monthly-rate underbilling value 2026-07-03 12:05:31 +05:30
Nabin Hait
abded56174 test: guard account lookup and lock missing company-check behaviour 2026-07-03 12:04:42 +05:30
Nabin Hait
6f866545b9 test: complete supplier-primary assertions and lock uniqueness gap 2026-07-03 12:03:56 +05:30
Nabin Hait
147e1539dc test: guard account lookup and lock dead POS guard behaviour 2026-07-03 12:02:50 +05:30
Nabin Hait
f58ea8e17d test: guard account lookup and lock current tax-rate behaviour 2026-07-03 12:01:47 +05:30
Nabin Hait
9980d47524 test: lock current end-date behaviour and assert persisted state 2026-07-03 12:00:45 +05:30
Nabin Hait
97794b7ded test: add coverage for Process Payment Reconciliation 2026-07-03 11:41:21 +05:30
Mihir Kandoi
ef5f47fafd fix: address review comments
- restore mutated SLE after test via addCleanup
- explicit return False in has_difference
- comment the fifo_stock_diff guard for non-queue predecessors
2026-07-03 11:39:51 +05:30
Nabin Hait
c51edbd88e test: add coverage for Account Closing Balance 2026-07-03 11:39:50 +05:30
Nabin Hait
5c87e2e398 test: add coverage for Cashier Closing 2026-07-03 11:34:27 +05:30
Nabin Hait
3167e8ba77 test: add coverage for Subscription Plan 2026-07-03 11:31:30 +05:30
Mihir Kandoi
94ab09e4a3 fix: FIFO queue checks and incorrect entries filter in stock ledger reports
- 'Show Incorrect Entries' always returned an empty result (regression
  from #43619); now returns entries from one row before the first
  incorrect one
- FIFO queue columns were computed for serialized/batched SLEs that
  don't maintain a stock queue, showing false differences; left empty
  for such rows
- compare value/valuation differences at currency precision, qty at
  float precision
2026-07-03 11:29:44 +05:30
Nabin Hait
83d821d8c4 test: add coverage for Journal Entry Template 2026-07-03 11:28:56 +05:30
Nabin Hait
22dc51a57a test: add coverage for Party Link 2026-07-03 11:25:32 +05:30
Nabin Hait
df54382727 test: add coverage for Mode of Payment 2026-07-03 11:23:08 +05:30
Nabin Hait
3e9843059e test: add coverage for Item Tax Template 2026-07-03 11:19:56 +05:30
Nabin Hait
ccd2aae481 test: add coverage for Monthly Distribution 2026-07-03 11:18:11 +05:30
Nabin Hait
41000ea109 test: add coverage for Bank Guarantee 2026-07-03 11:14:54 +05:30
Khushi Rawat
344f58b98a Merge pull request #56811 from khushi8112/fix/letterhead-footer-print-formats
fix: render letter head footer in print formats
2026-07-03 02:43:26 +05:30
Khushi Rawat
c9145c5ece Merge branch 'develop' into fix/letterhead-footer-print-formats 2026-07-03 02:27:58 +05:30
khushi8112
2d0c0a8c09 fix: add page numbers to print format footer 2026-07-03 02:26:52 +05:30
khushi8112
e60a467972 fix: render letter head footer in print formats 2026-07-03 02:16:59 +05:30
36 changed files with 861 additions and 181 deletions

View File

@@ -1,10 +1,59 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
aggregate_with_last_account_closing_balance,
generate_key,
)
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):
pass
"""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)

View File

@@ -1,8 +1,76 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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))

View File

@@ -1,8 +1,67 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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)

View File

@@ -1,8 +1,62 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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)

View File

@@ -45,6 +45,20 @@ 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):
"""

View File

@@ -1,9 +1,45 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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)

View File

@@ -5,9 +5,59 @@ import frappe
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
class TestModeofPayment(ERPNextTestSuite):
pass
"""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)
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):

View File

@@ -1,8 +1,67 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. 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):
pass
"""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

View File

@@ -1,9 +1,67 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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))

View File

@@ -2317,3 +2317,65 @@ 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()

View File

@@ -106,6 +106,8 @@ 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",

View File

@@ -1,11 +1,73 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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")

View File

@@ -79,7 +79,9 @@ def get_plan_rate(
start_date = getdate(start_date)
end_date = getdate(end_date)
no_of_months = relativedelta.relativedelta(end_date, start_date).months + 1
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
cost = plan.cost * no_of_months
# Adjust cost if start or end date is not month start or end

View File

@@ -1,8 +1,54 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, 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):
pass
"""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)

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

View File

@@ -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-03 21:32\n"
"PO-Revision-Date: 2026-07-01 20:39\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 "Historija Stanja na osnovu Šarže"
msgstr "Istorija 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 Historija Personala"
msgstr "Eksterna radna istorija 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 "Eksterna Radna Historija Personala"
msgstr "Interna radna istorija 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 RadnaHstorija"
msgstr "Eksterna Radna Istorija"
#: 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 Historija"
msgstr "Porodična Istorija"
#. 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 "Historija u Poduzeću"
msgstr "Istorija 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 Historija"
msgstr "Interna Radna Istorija"
#. 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 "Historija Prodaje po Artiklu"
msgstr "Istorija 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 Historija Prodaje"
msgstr "Mjesečna Istorija 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 Historija Transakcije"
msgstr "Godišnja Istorija 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."

View File

@@ -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-03 21:32\n"
"PO-Revision-Date: 2026-07-01 20:39\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 "Povijest Stanja na temelju Šarže"
msgstr "Istorija 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 "Obrazovanje Osoblja"
#. Name of a DocType
#: erpnext/setup/doctype/employee_external_work_history/employee_external_work_history.json
msgid "Employee External Work History"
msgstr "Vanjska radna povijest Osoblja"
msgstr "Eksterna 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 "Unutarnja radna povijest Osoblja"
msgstr "Interna 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 "Vanjska Radna Povijest"
msgstr "Eksterna Radna Istorija"
#: 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 "Unutarnja Radna Povijest"
msgstr "Interna Radna Istorija"
#. 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 "Povijest Prodaje po Artiklu"
msgstr "Istorija 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 Povijest Prodaje"
msgstr "Mjesečna Istorija 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 Povijest Transakcija"
msgstr "Godišnja Istorija 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."

View File

@@ -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-03 21:32\n"
"PO-Revision-Date: 2026-07-01 20:39\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å återbeställning nivå. Vänligen åtgärda dessa problem:"
msgstr "Fel uppstod för vissa artiklar när Material Begäran skapades baserat på bestä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 (återbeställning)"
msgstr "Skapas automatiskt (ombestä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 Återbeställning"
msgstr "Automatisk Ombestä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 "Partibaserad Saldo Historik"
msgstr "Saldo Historik per Parti"
#: 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 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."
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."
#: 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 "Kategoribaserad Tillgång Värde"
msgstr "Tillgång Värde per Kategori"
#: 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 \"Kundbaserad Rabatt\""
msgstr "Kund erfordras för \"Kund 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 "Kundbaserad Artikel Pris"
msgstr "Artikel Pris per Kund"
#: 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 "Kundbaserad Rabatt"
msgstr "Rabatt per Kund"
#. 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 "Dimension baserad Bokföring Saldo Rapport"
msgstr "Bokföring Saldo Rapport per Dimension"
#. 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 Partibaserad Värdering"
msgstr "Använd inte Parti baserad 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 Återbeställning"
msgstr "Aktivera Automatisk Ombestä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 Artikelbaserad Lager Konto"
msgstr "Aktivera Lager Konto per Artikel"
#. 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 återbeställning nivå"
msgstr "Följande Material Begäran skapades automatiskt baserat på Artikel bestä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 återbeställning kontroll är angiven på grupp lager nivå blir tillgänglig kvantitet summa av planerad kvantitet för alla underordnade lager."
msgstr "Om ombestä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 Återbeställning"
msgstr "Felaktig vald (grupp) Lager för Ombestä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 "Artikel Grupp baserad Rabatt"
msgstr "Rabatt per Artikel Grupp"
#. 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 Återbeställning"
msgstr "Artikel Ombestä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 "Artikelbaserad Förbrukning"
msgstr "Artikelvis Förbrukning"
#. Name of a DocType
#: erpnext/accounts/doctype/item_wise_tax_detail/item_wise_tax_detail.json
msgid "Item Wise Tax Detail"
msgstr "Artikelbaserad Moms Detalj"
msgstr "Moms Detalj per Artikel"
#. 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 "Artikelbaserad Moms Detalj"
#: erpnext/stock/doctype/delivery_note/delivery_note.json
#: erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
msgid "Item Wise Tax Details"
msgstr "Artikelbaserade Moms Detaljer"
msgstr "Artikel 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 "Artikelbaserade Moms Detaljer stämmer inte med Moms och Avgifter på följande rader:"
msgstr "Artikel Moms Detaljer stämmer inte överens 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 "Artikelbaserad Prislista Pris "
msgstr "Prislista Pris per Artikel"
#. Name of a report
#. Label of a Link in the Buying Workspace
@@ -27976,14 +27976,14 @@ msgstr "Artikelbaserad Prislista Pris "
#: erpnext/buying/workspace/buying/buying.json
#: erpnext/workspace_sidebar/buying.json
msgid "Item-wise Purchase History"
msgstr "Artikelbaserad Inköp Historik"
msgstr "Inköp Historik per Artikel"
#. 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 "Artikelbaserad Inköp Register"
msgstr "Inköp Register per Artikel"
#. Name of a report
#. Label of a Link in the Selling Workspace
@@ -27992,19 +27992,19 @@ msgstr "Artikelbaserad Inköp Register"
#: erpnext/selling/workspace/selling/selling.json
#: erpnext/workspace_sidebar/selling.json
msgid "Item-wise Sales History"
msgstr "Artikelbaserad Försäljning Historik"
msgstr "Försäljning Historik per Artikel"
#. 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 "Artikelbaserad Försäljning Register"
msgstr "Försäljning Register per Artikel"
#. Label of a Workspace Sidebar Item
#: erpnext/workspace_sidebar/financial_reports.json
msgid "Item-wise sales Register"
msgstr "Artikelbaserad Försäljning Register"
msgstr "Försäljning Register per Artikel"
#: 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 "Artikelbaserad Rabatt"
msgstr "Rabatt per Artikel"
#. Name of a report
#. Label of a Link in the Stock Workspace
@@ -28120,7 +28120,7 @@ msgstr "Artikelbaserad Rabatt"
#: erpnext/stock/workspace/stock/stock.json
#: erpnext/workspace_sidebar/stock.json
msgid "Itemwise Recommended Reorder Level"
msgstr "Artikelbaserad Rekommenderad Återbeställning Nivå"
msgstr "Rekommenderad Ombeställning Nivå per Artikel"
#. 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 "Projektbaserad Lager Spårning"
msgstr "Lager Spårning per Projekt"
#. Name of a report
#: erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.json
msgid "Project wise Stock Tracking "
msgstr "Projektbaserad Lager Spårning "
msgstr "Lager Spårning per Projekt"
#: erpnext/controllers/trends.py:457
msgid "Project-wise data is not available for Quotation"
msgstr "Projektbaserad data är inte tillgängligt för Försäljning Offert"
msgstr "Data per Projekt finns 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 "Kvantitetbaserad Diagram"
msgstr "Kvantitet 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 återbeställning nivå"
msgstr "Skapa Material Begäran när Lager når ombestä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 "Återbeställning Nivå"
msgstr "Ombestä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 "Återbeställning Kvantitet"
msgstr "Ombestä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 "Återbeställning Nivå"
msgstr "Ombestä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 "Återbeställning Kvantitet"
msgstr "Ombestä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 "Återbeställning Nivå Baserad på Lager"
msgstr "Ombestä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 Återbeställning Kvantitet"
msgstr "Rad # {0}: Ange Ombestä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 "Säljarebaserad Transaktion Översikt"
msgstr "Transaktion Översikt per Säljare"
#. 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 Artikel Grupp baserad Budget för detta Distrikt. Inkludera även säsongvariationer genom att ange Fördelning."
msgstr "Ange Budget per Artikel Grupp för detta Distrikt. Man kan även inkludera 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 Dimensionbaserad Lager"
msgstr "Visa Lager per Dimension"
#: 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 Lagerbaserad Lager Värde"
msgstr "Visa Lager Värde per Lager"
#: 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 "Distriktbaserad Försäljning"
msgstr "Försäljning per Distrikt"
#. 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 serie nummer, annars blir det Dubbel Post."
msgstr "Prefix {0} '{1}' finns redan. Ändra serienummer, annars blir det dubblett 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 "Verifikatbaserad Saldo"
msgstr "Saldo per Verifikat"
#. 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 "Lagerbaserad Lager Saldo"
msgstr "Lager Saldo per Lager"
#. 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 "Lagerbaserad Artikel Saldo, Ålder och Värde"
msgstr "Artikel Saldo Ålder och Värde per Lager"
#: 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 återbeställning i Lager Inställningar för att behålla återbeställning nivåer."
msgstr "Du måste aktivera automatisk ombeställning i lager inställningar för att behålla ombestä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 Återbeställning Fel"
msgstr "[Viktigt] [System] Automatisk Ombeställning Fel"
#: erpnext/controllers/status_updater.py:306
msgid "`Allow Negative rates for Items`"

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

View File

@@ -20,6 +20,7 @@ SLE_FIELDS = (
"outgoing_rate",
"stock_queue",
"batch_no",
"serial_no",
"stock_value",
"stock_value_difference",
"valuation_rate",
@@ -52,16 +53,16 @@ def add_invariant_check_fields(sles, filters):
balance_qty = 0.0
balance_stock_value = 0.0
incorrect_idx = 0
precision = frappe.get_precision("Stock Ledger Entry", "actual_qty")
incorrect_idx = None
float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3
currency_precision = (
cint(frappe.db.get_single_value("System Settings", "currency_precision")) or float_precision
)
for idx, sle in enumerate(sles):
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
fifo_qty = 0.0
fifo_value = 0.0
for qty, rate in queue:
fifo_qty += qty
fifo_value += qty * rate
if sle.batch_no:
sle.use_batchwise_valuation = frappe.db.get_value(
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
)
if sle.actual_qty < 0:
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
@@ -77,57 +78,67 @@ def add_invariant_check_fields(sles, filters):
if balance_qty is None:
balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
sle.balance_value_by_qty = (
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
)
sle.expected_qty_after_transaction = balance_qty
sle.stock_value_from_diff = balance_stock_value
# set difference fields
sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
sle.fifo_value_diff = sle.stock_value - fifo_value
sle.fifo_valuation_diff = (
sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
)
sle.valuation_diff = (
sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
)
sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value
if not incorrect_idx and filters.get("show_incorrect_entries"):
if is_sle_has_correct_data(sle, precision):
continue
else:
incorrect_idx = idx
if maintains_fifo_queue(sle):
add_fifo_fields(sle, sles[idx - 1] if idx else None)
if idx > 0:
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
if sle.batch_no:
sle.use_batchwise_valuation = frappe.db.get_value(
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
)
if incorrect_idx is None and not is_sle_has_correct_data(sle, float_precision, currency_precision):
incorrect_idx = idx
if filters.get("show_incorrect_entries"):
if incorrect_idx > 0:
sles = sles[cint(incorrect_idx) - 1 :]
return []
if incorrect_idx is None:
return []
return sles[max(incorrect_idx - 1, 0) :]
return sles
def is_sle_has_correct_data(sle, precision):
if flt(sle.difference_in_qty, precision) != 0.0 or flt(sle.diff_value_diff, precision) != 0:
print(flt(sle.difference_in_qty, precision), flt(sle.diff_value_diff, precision))
return False
def maintains_fifo_queue(sle):
# no queue is maintained for serialized/batchwise-valued stock
return not (
sle.serial_and_batch_bundle or sle.serial_no or (sle.batch_no and sle.use_batchwise_valuation)
)
return True
def add_fifo_fields(sle, prev_sle):
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
fifo_qty = 0.0
fifo_value = 0.0
for qty, rate in queue:
fifo_qty += qty
fifo_value += qty * rate
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
sle.fifo_value_diff = sle.stock_value - fifo_value
sle.fifo_valuation_diff = (
sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
)
# prev row may not maintain a queue; H and H - F stay blank across the gap
if prev_sle and prev_sle.fifo_stock_value is not None:
sle.fifo_stock_diff = sle.fifo_stock_value - prev_sle.fifo_stock_value
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
def is_sle_has_correct_data(sle, float_precision, currency_precision):
return (
flt(sle.difference_in_qty, float_precision) == 0.0
and flt(sle.diff_value_diff, currency_precision) == 0.0
)
def get_columns():

View File

@@ -42,3 +42,35 @@ class TestStockLedgerInvariantCheck(ERPNextTestSuite):
data = self.run_report(item_code=item)
self.assertEqual(data[-1].qty_after_transaction, 11)
def test_show_incorrect_entries(self):
item = self.make_movements()
self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), [])
sle = frappe.get_last_doc(
"Stock Ledger Entry", {"item_code": item, "warehouse": WAREHOUSE, "is_cancelled": 0}
)
frappe.db.set_value(
"Stock Ledger Entry", sle.name, "qty_after_transaction", sle.qty_after_transaction + 5
)
data = self.run_report(item_code=item, show_incorrect_entries=1)
self.assertEqual(len(data), 2) # incorrect entry + one before it for context
self.assertEqual(data[-1].name, sle.name)
def test_batch_item_skips_fifo_queue_checks(self):
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
properties={"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "SLIC-BAT-.####"}
).name
make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100)
data = self.run_report(item_code=item)
self.assertTrue(data)
for row in data:
self.assertIsNone(row.fifo_qty_diff)
self.assertIsNone(row.fifo_value_diff)
self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), [])

View File

@@ -205,7 +205,10 @@ def get_data(filters=None):
data = []
if item_warehouse_map:
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3
currency_precision = (
cint(frappe.db.get_single_value("System Settings", "currency_precision")) or float_precision
)
for item_warehouse in item_warehouse_map:
report_data = stock_ledger_invariant_check(item_warehouse)
@@ -215,7 +218,11 @@ def get_data(filters=None):
for row in report_data:
if has_difference(
row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method
row,
float_precision,
currency_precision,
filters.difference_in,
item_warehouse.valuation_method or valuation_method,
):
row.update(
{
@@ -261,23 +268,26 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict:
return query.run(as_dict=1)
def has_difference(row, precision, difference_in, valuation_method):
def has_difference(row, float_precision, currency_precision, difference_in, valuation_method):
if valuation_method == "Moving Average":
qty_diff = flt(row.difference_in_qty, precision)
value_diff = flt(row.diff_value_diff, precision)
valuation_diff = flt(row.valuation_diff, precision)
qty_diff = flt(row.difference_in_qty, float_precision)
value_diff = flt(row.diff_value_diff, currency_precision)
valuation_diff = flt(row.valuation_diff, currency_precision)
else:
qty_diff = flt(row.difference_in_qty, precision)
value_diff = flt(row.diff_value_diff, precision)
qty_diff = flt(row.difference_in_qty, float_precision)
value_diff = flt(row.diff_value_diff, currency_precision)
if row.stock_queue and json.loads(row.stock_queue):
value_diff = value_diff or (
flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision)
flt(row.fifo_value_diff, currency_precision)
or flt(row.fifo_difference_diff, currency_precision)
)
qty_diff = qty_diff or flt(row.fifo_qty_diff, precision)
qty_diff = qty_diff or flt(row.fifo_qty_diff, float_precision)
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
valuation_diff = flt(row.valuation_diff, currency_precision) or flt(
row.fifo_valuation_diff, currency_precision
)
if difference_in == "Qty" and qty_diff:
return True
@@ -287,3 +297,5 @@ def has_difference(row, precision, difference_in, valuation_method):
return True
elif difference_in not in ["Qty", "Value", "Valuation"] and (qty_diff or value_diff or valuation_diff):
return True
return False

View File

@@ -1204,7 +1204,11 @@ class update_entries_after:
self.wh_data.stock_queue = json.loads(stock_queue[0]) if stock_queue else []
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
self.wh_data.qty_after_transaction += flt(doc.total_qty, self.flt_precision)
# Replay the immutable qty recorded on the SLE at submission, not the bundle's recomputed
# total_qty. A valuation repost must never rewrite physical quantities; if the bundle's child
# rows were edited after submission, doc.total_qty would silently corrupt qty_after_transaction
# (and every downstream balance). sle.actual_qty is the frozen movement for this entry.
self.wh_data.qty_after_transaction += flt(sle.actual_qty, self.flt_precision)
if flt(self.wh_data.qty_after_transaction, self.flt_precision):
self.wh_data.valuation_rate = flt(self.wh_data.stock_value, self.flt_precision) / flt(
self.wh_data.qty_after_transaction, self.flt_precision