* fix: use one formula environment in validator and engine
* test: cover shared formula environment
* fix: use distinct dummy values when test-evaluating formulas
* fix: reject line references that can't be used in a formula
* fix: drop the undefined-reference check
* fix: normalise line references before validating
* fix: normalise formulas on save instead of during validation
* fix: escape validation messages where they are rendered
* fix: strip the formula in the engine instead of relying on the validator
* fix: ignore division by zero when test-evaluating formulas
* fix(stock): use stored posting_datetime for repost boundary
get_stock_ledger_entries re-derived posting_datetime from posting_date and
posting_time on every call, discarding the stored value its callers pass in.
when a row's stored posting_datetime differs from that pair, the replay window
is built from the wrong instant: the row falls outside the range filter and is
never recomputed, while get_previous_sle still selects it as the opening
balance and reuses its stale qty_after_transaction. every later entry inherits
the error, leaving bin qty adrift from the sum of its ledger.
derive the boundary only when the caller has not supplied one.
* fix(stock): match current voucher sle on stored posting_datetime
get_sle_against_current_voucher selected rows with an equality check against a
posting_datetime re-derived from posting_date and posting_time. a row whose
stored posting_datetime differs from that pair matches nothing, so reposting
the voucher silently processes zero entries and the row can never be corrected
through its own voucher.
read the timestamp from the stored row when the sle is known, and derive it
only as a fallback.
* test(stock): cover repost with diverged posting_datetime
add a repack scenario whose incoming entry stores a posting_datetime one
microsecond before its own posting_time. asserts the voucher lookup still
finds that entry, and that reposting replays it instead of reusing its stale
qty_after_transaction, which otherwise left bin qty at 115 against 615 of
recorded movements.
* fix(stock): consume batch slots newest first for LIFO items
The valuation method decided which end of the queue an issue consumed
from, but only for stock carrying no batch or serial number. Batch slots
were always consumed from the head, so a LIFO item reported its oldest
stock as still on hand when it had been issued.
Slots of one batch valued batchwise share a date, so the direction of
the walk cannot change what they report. Slots pooled across batches
carry the date of the batch that filled them, and there the wrong stock
aged.
Pass the valuation method through to the batch walk and read the queue
from the tail for a LIFO item, as the untagged walk already does.
* test(stock): cover LIFO consumption of pooled batch slots
Issue against the newer of two pooled batches on a LIFO item and assert
the September slot is consumed rather than the January one.
* fix(stock): scope stock ageing batch and serial age to the warehouse
The first inward posting date of a batch or serial number was cached
under the identity alone, so the age of a row depended on which stock
ledger entries the filters let the report scan.
A batch received into WH A and transferred to WH B aged from the WH A
receipt in an unfiltered run, but from the transfer date once a
warehouse filter was applied. Same stock, same warehouse, same to date,
two different ages.
Key the cache on the warehouse as well. Repeated receipts of one batch
into one warehouse still age from the first of them, and a transfer now
restarts the clock in the destination warehouse, as it already does for
stock that carries no batch or serial number.
* test(stock): cover warehouse scoped batch age in stock ageing
A batch received into one warehouse and transferred to another aged
from the first receipt in an unfiltered run and from the transfer once
the warehouse filter narrowed the scan. Assert both runs report the
transfer date.
* test(stock): cover warehouse scoped serial age in stock ageing
The cached date is keyed on the warehouse for serial numbers as well as
batches, and only the batch half was covered. Assert a serial
transferred between warehouses ages from the transfer in both a full
and a warehouse filtered scan.
Insert the batch fixture with ignore_if_duplicate instead of checking
for it first.
* test: stop four tests from passing without running
Three advisory-lock tests return early on MariaDB:
if frappe.db.db_type != "postgres":
return
A bare return reports the test as passed, so the MariaDB CI job shows
green for a test it never ran. skipTest reports it as skipped.
test_stock_reco_with_opening_stock_with_diff_inventory returned early
when the custom "Plant" DocType already existed. DocType creation is
DDL and survives the test transaction, so the test ran once on a fresh
site and silently did nothing on every run after that. Create the
DocType only when it is missing and let the test run either way.
Its closing loop also asserted inside an if/elif over the ledger rows,
which verified nothing if the dimension came back unset. Compare the
whole {plant: qty} mapping instead.
* test: give the job card validator tests a real job card
Both tests looked for a submitted Job Card left behind by another test
and returned when they did not find one:
jc_name = frappe.db.get_value("Job Card", {"docstatus": 1})
if not jc_name:
return # skip if no job cards in test data
Run in isolation they asserted nothing and still reported a pass, and
they were the only coverage for validate_job_card_fg_item and
validate_job_card_item.
Move them to test_job_card.py, where the Work Order and BOM fixtures
that produce Job Cards already live, and build the Job Card in the test.
The finished-good case needs a card that carries one, so it goes through
a track_semi_finished_goods BOM. Both now assert on the message text, and
both fail if the validator body is removed.
Four tests in test_stock_entry.py each cover only an early-return guard:
def test_validate_job_card_item_skips_when_no_job_card(self):
se = frappe.new_doc("Stock Entry")
se.job_card = None
se.validate_job_card_item() # must not raise
That exercises `if not self.job_card: return` and nothing else. The
mismatch tests next to them already cover the behaviour these validators
actually implement.
Three tests in test_payment_request.py assert against their own mock.
_is_v2_gateway delegates to payments.utils.is_v2_gateway; all three mock
that delegate to return False and then assert the result is False, for
inputs (None, "", "NonExistentGateway12345") that take an identical code
path. The mock decides the outcome, so the assertion holds regardless of
what ERPNext does. The three tests covering the real branches --
delegation, a False delegate, and the exception fallback -- are kept.
Rapid successive edits to Net Purchase Amount could fire overlapping
set_finance_book calls; if an older request's response arrived after a
newer one, it could overwrite Finance Books with values computed from
a stale amount. Now the callback only applies a response if the fields
it was based on still match the form's current values.