Skip to content

fix(pwa): multicurrency support in expense & advance forms#4083

Open
iamkhanraheel wants to merge 13 commits intofrappe:developfrom
iamkhanraheel:fix/v16_changes_pwa
Open

fix(pwa): multicurrency support in expense & advance forms#4083
iamkhanraheel wants to merge 13 commits intofrappe:developfrom
iamkhanraheel:fix/v16_changes_pwa

Conversation

@iamkhanraheel
Copy link
Copy Markdown
Collaborator

@iamkhanraheel iamkhanraheel commented Feb 5, 2026

Changes done

  • Updated Expense Claim form and tables to support transaction/base currency values and improved advance allocation handling
  • Compute base currency values when transaction amounts or exchange rates change.
expenseClaim.PWA.mov

  • Added a useCurrencyConversion composable in which
    • updateCurrencyLabels func helps to dynamically update currency field labels to reflect transaction and company base currency
    • updateBaseFieldsAmount func helps to convert base field amount to base currency
image
  • Removed employee currency fetch from salary strucutre since the currency field is added in the Employee Advance & also removed the exchange rate fetch as the field is removed from the form

  • Removed employee advance fetch & it's api as currency filter is added in the field to show account as per the currency selected

  • Improved advances resources as it's api is also updated & updated the logic to get data & make it selected as per the sanctioned amount

  • Updated Link component to reload options when filters change -- Contribution by @ruchamahabal

Closes: #3986

Summary by CodeRabbit

  • New Features

    • Currency column added to Expense Claim list for clearer visibility.
  • Improvements

    • Reactive currency conversion across expense forms and tables with live label and base/transaction amount synchronization.
    • Better exchange-rate handling and visibility, plus retrieval of currency precision for totals.
    • Dropdowns now refresh options when filters change for up-to-date selections.
    • Advances and allocations recalculated more accurately in response to currency/exchange changes.
  • Chores

    • Removed an unused advance-account API endpoint.

@iamkhanraheel iamkhanraheel changed the title fix: Update currency & exchange rate data as per desk in PWA fix(pwa): multicurrency support in expense & advance forms Mar 5, 2026
@iamkhanraheel iamkhanraheel marked this pull request as ready for review March 6, 2026 11:12
@iamkhanraheel
Copy link
Copy Markdown
Collaborator Author

@Mergifyio rebase

@mergify
Copy link
Copy Markdown
Contributor

mergify bot commented Mar 6, 2026

rebase

✅ Branch has been successfully rebased

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces two composables, updateCurrencyLabels and updateBaseFieldsAmount, and a currencyPrecision resource. Multiple frontend components were updated to rely on document currency and the new composables, adding reactive watchers and label/amount conversion logic (ExpenseClaimItem, ExpensesTable, Link, roster Link, employee_advance Form, expense_claim Form, expense_claim List). It also removes the get_advance_account API function from hrms/api/__init__.py.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Minor out-of-scope changes detected: Link.vue updates in both frontend and roster components extend beyond multicurrency requirements, and removal of get_advance_account API is not explicitly linked to the issue. Clarify whether Link.vue filter reactivity and get_advance_account removal are intentional refactorings or separate improvements that should be tracked separately.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(pwa): multicurrency support in expense & advance forms' clearly and concisely summarizes the main change, addressing multicurrency support in PWA forms.
Linked Issues check ✅ Passed The PR successfully implements multicurrency support by adding updateCurrencyLabels and updateBaseFieldsAmount composables, updating field labels in Expense Claim and Employee Advance forms to reflect both transaction and base currencies.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/views/expense_claim/Form.vue (1)

193-200: ⚠️ Potential issue | 🟡 Minor

Condition may not trigger for new documents where docstatus is undefined.

For newly created documents, expenseClaim.value.docstatus may be undefined rather than 0. The strict equality check === 0 would evaluate to false, preventing the advances reload.

Proposed fix
-		if (!props.id && expenseClaim.value.docstatus === 0) {
+		if (!props.id && !expenseClaim.value.docstatus) {
 			advances.reload()
 		}

Or use (expenseClaim.value.docstatus ?? 0) === 0 for explicit intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 193 - 200, The watch
callback's condition uses strict equality expenseClaim.value.docstatus === 0
which fails when docstatus is undefined for new documents; update the condition
inside the watcher (the function referencing expenseClaim, props.id and calling
advances.reload) to treat undefined as 0 (e.g., use
(expenseClaim.value.docstatus ?? 0) === 0) so advances.reload() runs for new
drafts when props.id is falsy.
🧹 Nitpick comments (8)
roster/src/components/Link.vue (2)

32-35: Default object prop should use a factory function.

Using a static {} as the default value means all component instances share the same object reference. This can cause unexpected mutation issues.

Suggested fix
 	filters: {
 		type: Object,
-		default: {},
+		default: () => ({}),
 	},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@roster/src/components/Link.vue` around lines 32 - 35, The props definition
for filters in Link.vue uses a shared object literal as the default which can
cause shared-state mutations; change the filters prop to use a factory function
that returns a new object (i.e., replace default: {} with default: () => ({}))
so each component instance gets its own object; update the props block where
filters is declared to use this factory-style default.

107-111: Consider adding deep: true for nested filter changes.

The watcher on props.filters only detects reference changes by default. If the parent component mutates filter properties in place rather than replacing the entire object, this watcher won't trigger. If filters are always replaced with new objects, this is fine—otherwise, add { deep: true }.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@roster/src/components/Link.vue` around lines 107 - 111, The watcher on
props.filters only observes top-level reference changes; update the watch call
that currently reads watch(() => props.filters, () => reloadOptions(""), ) to
pass a third options argument with { deep: true } so nested mutations trigger
reloadOptions (i.e. watch(() => props.filters, () => reloadOptions(""), { deep:
true })). Keep the existing callback and getter (props.filters) and only add the
options object; if you prefer not to use deep watching consider switching to
toRef(props, 'filters') and watching that if filters are always replaced.
frontend/src/composables/useCurrencyConversion.js (1)

10-51: Composable creates a watcher without cleanup.

The updateCurrencyLabels function creates a watch internally. In Vue 3, watchers created inside setup() are automatically stopped when the component unmounts, but this composable could be called outside that context. If the composable is called dynamically or multiple times, consider returning a cleanup function or using watchEffect with explicit stop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/composables/useCurrencyConversion.js` around lines 10 - 51, The
composable updateCurrencyLabels creates a Vue watch that isn't cleaned up;
capture the watcher stop handle returned by watch(...) and return a cleanup
function (e.g., stop or stopWatcher) from updateCurrencyLabels so callers can
stop the watcher when needed, or alternatively call onScopeDispose inside
updateCurrencyLabels if you want automatic cleanup when used inside setup;
ensure you reference the existing watch call, the returned stop handle, and the
helper functions/refs (fetchCompanyCurrency, companyCurrency, updateLabels) when
wiring the stop logic.
frontend/src/components/ExpensesTable.vue (2)

277-290: Mutating prop data directly.

The watch mutates props.expenseClaim.expenses rows directly via updateBaseFieldsAmount. While this works due to Vue's reactivity, mutating props is generally discouraged as it can make data flow harder to trace. Consider emitting an event to update the parent instead, or document this as intentional two-way binding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ExpensesTable.vue` around lines 277 - 290, The watch
on props.expenseClaim.exchange_rate is mutating prop objects directly by calling
updateBaseFieldsAmount on items from props.expenseClaim.expenses; instead change
the behavior to avoid direct prop mutation—create a shallow/deep copy of
expenseClaim.expenses, apply updateBaseFieldsAmount to the copied rows, then
emit an event (e.g., 'update-expenses' or 'update:expenseClaim') with the
updated array so the parent updates the prop. Alternatively, if two-way binding
is intended, explicitly use v-model/update:expenseClaim and document it;
reference the watch callback, updateBaseFieldsAmount, and
props.expenseClaim.expenses when implementing the change.

264-275: Guard condition may not prevent execution on empty object.

expenseItem.value is initialized to {} (line 166), which is truthy. The check if (expenseItem.value) on line 267 will pass even when the item is empty, potentially calling updateBaseFieldsAmount with an empty doc before user interaction.

Suggested fix
 watch(
 	() => [expenseItem.value.amount, expenseItem.value.sanctioned_amount],
 	() => {
-		if (expenseItem.value) {
+		if (expenseItem.value.amount !== undefined || expenseItem.value.sanctioned_amount !== undefined) {
 			updateBaseFieldsAmount({
 				doc: expenseItem.value, 
 				fields: ['amount', 'sanctioned_amount'], 
 				exchangeRate: props.expenseClaim.exchange_rate,
 			});
 		}
 	}
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/ExpensesTable.vue` around lines 264 - 275, The watch
currently calls updateBaseFieldsAmount even when expenseItem.value is an empty
object because expenseItem is initialized to {} (truthy); change the guard in
the watcher to verify the item is populated (e.g., check a stable identifier or
required fields like expenseItem.value.amount !== undefined ||
expenseItem.value.sanctioned_amount !== undefined or
Object.keys(expenseItem.value).length > 0) before calling
updateBaseFieldsAmount, keeping the same parameters (doc: expenseItem.value,
fields: ['amount','sanctioned_amount'], exchangeRate:
props.expenseClaim.exchange_rate).
frontend/src/components/Link.vue (1)

28-31: Default object prop should use a factory function.

Same issue as in roster/src/components/Link.vue—using a static {} as the default value means all component instances share the same object reference.

Suggested fix
 	filters: {
 		type: Object,
-		default: {},
+		default: () => ({}),
 	},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/Link.vue` around lines 28 - 31, The filters prop in
the Link.vue component is using a shared static object as its default which can
lead to shared mutable state; change the prop definition for filters (the
filters prop inside the component options) to use a factory function that
returns a new object (e.g., default: () => ({})) so each component instance gets
its own object; locate the filters prop in the Link.vue component props/options
and replace the static {} default with a function returning an empty object.
frontend/src/views/expense_claim/Form.vue (2)

266-290: { deep: true } is unnecessary for this watcher.

The watched value is an array of primitives returned from the getter function. Vue already detects changes because the getter produces a new array reference on each evaluation. The deep option adds overhead without benefit here.

Suggested simplification
-    { deep: true }
+    { immediate: true }

Or remove the options object entirely if immediate evaluation isn't needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 266 - 290, The
watcher on expenseClaim that observes an array of primitive fields (defined in
the inline getter) is using the unnecessary { deep: true } option; remove the
deep option (or the entire options object) from the watch call that wraps the
getter so Vue won't perform needless deep observation—update the watch
invocation around the getter that references expenseClaim.value.* and calls
updateBaseFieldsAmount (and keeps fieldsToConvert and exchangeRate as-is) to
pass no deep option.

158-163: Consider adding error handling for exchange rate fetch.

If the exchange rate API call fails, expenseClaim.value.exchange_rate remains undefined, which could cause calculation issues in updateBaseFieldsAmount downstream. Consider adding an onError handler or a fallback:

Suggested improvement
 const exchangeRate = createResource({
 	url: "erpnext.setup.utils.get_exchange_rate",
 	onSuccess(data) {
 		expenseClaim.value.exchange_rate = data
 	},
+	onError(error) {
+		console.error("Failed to fetch exchange rate:", error)
+		// Fallback to 1 or show user notification
+		expenseClaim.value.exchange_rate = 1
+	},
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 158 - 163, The
exchange rate fetch via createResource (exchangeRate) lacks error handling: add
an onError handler on the same createResource call (alongside onSuccess) to set
a safe fallback on expenseClaim.value.exchange_rate (e.g., 1 or a previously
cached value), log or surface the error for debugging, and ensure you
trigger/update updateBaseFieldsAmount after setting the fallback so downstream
calculations are not left with undefined values; reference the createResource
export named exchangeRate, its onSuccess/onError handlers, the expenseClaim
reactive object, and the updateBaseFieldsAmount function when implementing this.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/ExpenseClaimItem.vue`:
- Line 17: The API is not returning the currency field so props.doc.currency is
undefined; update the get_expense_claims function to include the expense claim
currency by adding "tabExpense Claim.currency" (or its equivalent column
reference) to the fields list returned by get_expense_claims in
hrms/api/__init__.py so formatCurrency receives a valid currency value; ensure
the field name matches the table alias used in the query and update any
field-selection arrays or SQL SELECT clause inside get_expense_claims
accordingly.

In `@frontend/src/composables/useCurrencyConversion.js`:
- Line 56: The variable name excahnge_rate is misspelled; rename it to
exchange_rate in useCurrencyConversion (update the declaration const
exchange_rate = flt(exchangeRate || doc.exchange_rate || 1, 9)) and update any
subsequent references within that module or function (e.g., places using
excahnge_rate) to the corrected symbol to avoid undefined variable bugs.

In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 497-513: The setExchangeRate function wrongly treats undefined
companyCurrency as equal to currency, so add an explicit guard for
companyCurrency.value (or expenseClaim.value.company) before comparing: if
companyCurrency.value is falsy, return early (do not set exchange_rate or hide
the field) so currencies can initialize; keep the existing logic that sets
exchange_rate = 1 and toggles exchange_rate_field.hidden only when
companyCurrency.value is defined and currency.value === companyCurrency.value,
otherwise call exchangeRate.fetch(...) and unhide the field (references:
setExchangeRate, expenseClaim, companyCurrency, currency, exchangeRate.fetch,
exchange_rate_field).
- Around line 119-138: The advances resource captures params at creation so
advances.reload() uses stale expense_claim; replace the reload call with a fetch
that supplies the current params (e.g., call advances.fetch({ params: {
expense_claim: expenseClaim.value } })) so the latest expenseClaim is sent, or
alternatively make the params dynamic if createResource supports a params
function or remove auto: true and explicitly call advances.fetch(...) after
expenseClaim is populated; update references to advances.reload() to use
advances.fetch with current expenseClaim.value.

---

Outside diff comments:
In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 193-200: The watch callback's condition uses strict equality
expenseClaim.value.docstatus === 0 which fails when docstatus is undefined for
new documents; update the condition inside the watcher (the function referencing
expenseClaim, props.id and calling advances.reload) to treat undefined as 0
(e.g., use (expenseClaim.value.docstatus ?? 0) === 0) so advances.reload() runs
for new drafts when props.id is falsy.

---

Nitpick comments:
In `@frontend/src/components/ExpensesTable.vue`:
- Around line 277-290: The watch on props.expenseClaim.exchange_rate is mutating
prop objects directly by calling updateBaseFieldsAmount on items from
props.expenseClaim.expenses; instead change the behavior to avoid direct prop
mutation—create a shallow/deep copy of expenseClaim.expenses, apply
updateBaseFieldsAmount to the copied rows, then emit an event (e.g.,
'update-expenses' or 'update:expenseClaim') with the updated array so the parent
updates the prop. Alternatively, if two-way binding is intended, explicitly use
v-model/update:expenseClaim and document it; reference the watch callback,
updateBaseFieldsAmount, and props.expenseClaim.expenses when implementing the
change.
- Around line 264-275: The watch currently calls updateBaseFieldsAmount even
when expenseItem.value is an empty object because expenseItem is initialized to
{} (truthy); change the guard in the watcher to verify the item is populated
(e.g., check a stable identifier or required fields like
expenseItem.value.amount !== undefined || expenseItem.value.sanctioned_amount
!== undefined or Object.keys(expenseItem.value).length > 0) before calling
updateBaseFieldsAmount, keeping the same parameters (doc: expenseItem.value,
fields: ['amount','sanctioned_amount'], exchangeRate:
props.expenseClaim.exchange_rate).

In `@frontend/src/components/Link.vue`:
- Around line 28-31: The filters prop in the Link.vue component is using a
shared static object as its default which can lead to shared mutable state;
change the prop definition for filters (the filters prop inside the component
options) to use a factory function that returns a new object (e.g., default: ()
=> ({})) so each component instance gets its own object; locate the filters prop
in the Link.vue component props/options and replace the static {} default with a
function returning an empty object.

In `@frontend/src/composables/useCurrencyConversion.js`:
- Around line 10-51: The composable updateCurrencyLabels creates a Vue watch
that isn't cleaned up; capture the watcher stop handle returned by watch(...)
and return a cleanup function (e.g., stop or stopWatcher) from
updateCurrencyLabels so callers can stop the watcher when needed, or
alternatively call onScopeDispose inside updateCurrencyLabels if you want
automatic cleanup when used inside setup; ensure you reference the existing
watch call, the returned stop handle, and the helper functions/refs
(fetchCompanyCurrency, companyCurrency, updateLabels) when wiring the stop
logic.

In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 266-290: The watcher on expenseClaim that observes an array of
primitive fields (defined in the inline getter) is using the unnecessary { deep:
true } option; remove the deep option (or the entire options object) from the
watch call that wraps the getter so Vue won't perform needless deep
observation—update the watch invocation around the getter that references
expenseClaim.value.* and calls updateBaseFieldsAmount (and keeps fieldsToConvert
and exchangeRate as-is) to pass no deep option.
- Around line 158-163: The exchange rate fetch via createResource (exchangeRate)
lacks error handling: add an onError handler on the same createResource call
(alongside onSuccess) to set a safe fallback on expenseClaim.value.exchange_rate
(e.g., 1 or a previously cached value), log or surface the error for debugging,
and ensure you trigger/update updateBaseFieldsAmount after setting the fallback
so downstream calculations are not left with undefined values; reference the
createResource export named exchangeRate, its onSuccess/onError handlers, the
expenseClaim reactive object, and the updateBaseFieldsAmount function when
implementing this.

In `@roster/src/components/Link.vue`:
- Around line 32-35: The props definition for filters in Link.vue uses a shared
object literal as the default which can cause shared-state mutations; change the
filters prop to use a factory function that returns a new object (i.e., replace
default: {} with default: () => ({})) so each component instance gets its own
object; update the props block where filters is declared to use this
factory-style default.
- Around line 107-111: The watcher on props.filters only observes top-level
reference changes; update the watch call that currently reads watch(() =>
props.filters, () => reloadOptions(""), ) to pass a third options argument with
{ deep: true } so nested mutations trigger reloadOptions (i.e. watch(() =>
props.filters, () => reloadOptions(""), { deep: true })). Keep the existing
callback and getter (props.filters) and only add the options object; if you
prefer not to use deep watching consider switching to toRef(props, 'filters')
and watching that if filters are always replaced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 645f7d54-7c23-4e09-9116-5c0b7ab04397

📥 Commits

Reviewing files that changed from the base of the PR and between 9479a67 and cde96b3.

📒 Files selected for processing (10)
  • frontend/src/components/ExpenseClaimItem.vue
  • frontend/src/components/ExpensesTable.vue
  • frontend/src/components/Link.vue
  • frontend/src/composables/useCurrencyConversion.js
  • frontend/src/data/currencies.js
  • frontend/src/views/employee_advance/Form.vue
  • frontend/src/views/expense_claim/Form.vue
  • frontend/src/views/expense_claim/List.vue
  • hrms/api/__init__.py
  • roster/src/components/Link.vue
💤 Files with no reviewable changes (1)
  • hrms/api/init.py

@iamkhanraheel
Copy link
Copy Markdown
Collaborator Author

@Mergifyio rebase

@mergify
Copy link
Copy Markdown
Contributor

mergify bot commented Mar 6, 2026

rebase

☑️ Nothing to do, the required conditions are not met

Details
  • any of:
    • #commits-behind > 0 [📌 rebase requirement]
    • -linear-history [📌 rebase requirement]
  • -closed [📌 rebase requirement]
  • -conflict [📌 rebase requirement]
  • queue-position = -1 [📌 rebase requirement]

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/src/views/expense_claim/Form.vue (1)

119-138: ⚠️ Potential issue | 🟠 Major

Stale params when reloading advances resource.

The params object captures expenseClaim.value at resource creation time. When advances.reload() is called (line 200), it reuses these stale initial params rather than the current expenseClaim.value. At creation time, fields like employee may not yet be populated.

Use .fetch() with current params instead of .reload():

-			advances.reload()
+			advances.fetch({ expense_claim: expenseClaim.value })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 119 - 138, The
advances resource captured a stale params object at creation (params: {
expense_claim: expenseClaim.value }) so calling advances.reload() reuses the old
snapshot; instead, when you need fresh data (where reload() is currently
invoked) call advances.fetch and pass the current params object built from
expenseClaim.value (i.e., request the resource with params:{ expense_claim:
expenseClaim.value }) so the request uses the up-to-date expenseClaim fields;
update the call site that uses advances.reload() to use advances.fetch with the
current params and keep existing onSuccess handling (e.g., the logic that sets
expenseClaim.value.advances and calls calculateTotalAdvance()).
🧹 Nitpick comments (4)
frontend/src/composables/useCurrencyConversion.js (2)

14-17: Unnecessary async/await on synchronous function.

getCompanyCurrency returns a plain value synchronously (per currencies.js), not a Promise. The async/await here is misleading and unnecessary overhead.

Proposed fix
-	const fetchCompanyCurrency = async () => {
+	const fetchCompanyCurrency = () => {
 		if (!doc.company) return
-		companyCurrency.value = await getCompanyCurrency(doc.company)
+		companyCurrency.value = getCompanyCurrency(doc.company)
 	}

Also update the watcher callback:

 	watch(
 		() => [doc.company, doc.currency],
-		async () => {
-			await fetchCompanyCurrency()
+		() => {
+			fetchCompanyCurrency()
 			updateLabels()
 		},
 		{ immediate: true }
 	)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/composables/useCurrencyConversion.js` around lines 14 - 17, The
fetchCompanyCurrency function is marked async and uses await even though
getCompanyCurrency returns a synchronous value; remove the async keyword and the
await and assign companyCurrency.value directly from
getCompanyCurrency(doc.company). Also update any watcher callback that calls
fetchCompanyCurrency (the reactive watcher in this composable) to call the
now-synchronous fetchCompanyCurrency directly (no need to handle a Promise).

41-50: Watcher created inside function call may leak.

Each invocation of updateCurrencyLabels registers a new watch() without cleanup. If the caller's outer watcher (e.g., in Form.vue) fires multiple times, watchers accumulate. Consider capturing and returning the stop handle, or restructuring so the caller manages the watch lifecycle.

Possible restructure
+	const stopWatch = watch(
 		() => [doc.company, doc.currency],
-		async () => {
-			await fetchCompanyCurrency()
+		() => {
+			fetchCompanyCurrency()
 			updateLabels()
 		},
 		{ immediate: true }
 	)

-	return { updateLabels, companyCurrency }
+	return { updateLabels, companyCurrency, stopWatch }

The caller can then call stopWatch() before re-invoking or on unmount.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/composables/useCurrencyConversion.js` around lines 41 - 50, The
watcher created by watch(...) inside this composable is registered every time
the caller invokes the function and never cleaned up; capture the stop handle
(const stop = watch(...)) and expose it (e.g., return stopWatch or stop from the
composable) or move the watch out so the caller manages lifecycle; specifically,
change the code that defines the watch around fetchCompanyCurrency and
updateLabels to store the stop handle and return a stop function alongside
updateLabels and companyCurrency so the caller can call stop() before
re-invoking or on unmount.
frontend/src/views/expense_claim/Form.vue (2)

269-293: { deep: true } is unnecessary for primitive values.

The watch dependencies are all primitive number values. deep: true adds overhead for deep comparison but provides no benefit here since primitives are compared by value anyway.

Proposed fix
-    { deep: true }
+    { immediate: true }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 269 - 293, The
watcher is using { deep: true } unnecessarily for primitive numeric
dependencies; in the watch call that observes
expenseClaim.value.total_sanctioned_amount, total_advance_amount, grand_total,
total_claimed_amount, total_taxes_and_charges and exchange_rate, remove the {
deep: true } option so Vue will use the default shallow comparison for these
primitive values; keep the rest of the handler intact (the fieldsToConvert array
and the call to updateBaseFieldsAmount with doc: expenseClaim.value and
exchangeRate: expenseClaim.value.exchange_rate).

242-267: Redundant watcher setup may cause accumulation.

This watch calls updateCurrencyLabels each time currency changes, but updateCurrencyLabels internally sets up its own watch on doc.currency. Each invocation creates a new internal watcher without cleanup, leading to multiple watchers tracking the same properties.

Consider calling updateCurrencyLabels once (on mount or initial data load) and relying on its internal watcher, or removing the internal watcher from the composable and keeping the external control here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 242 - 267, The
external watch in Form.vue is creating duplicate watchers because
updateCurrencyLabels itself registers a watch on doc.currency; remove the
surrounding watch and instead call updateCurrencyLabels once when
formFields.data and expenseClaim.value.currency are available (e.g., in mounted
or a small computed/nextTick helper) so the composable's internal watcher is the
sole watcher; target the existing updateCurrencyLabels invocation and replace
the watch(...) block with a one-time guarded call that checks for fields and
currency before calling updateCurrencyLabels.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 500-517: In setExchangeRate, guard against companyCurrency.value
being undefined and fix the hide/unhide logic: first return early if
currency.value or companyCurrency.value or formFields.data are missing; then
locate the exchange_rate_field (from formFields.data.find(...)) and only set
exchange_rate_field.hidden = 1 when currency.value === companyCurrency.value and
set expenseClaim.value.exchange_rate = 1; otherwise, if
expenseClaim.value.exchange_rate is falsy, call exchangeRate.fetch({
from_currency: currency.value, to_currency: companyCurrency.value }) and only
then set exchange_rate_field.hidden = 0. Ensure all checks reference
expenseClaim, currency, companyCurrency, exchange_rate_field and
exchangeRate.fetch in that function.

---

Duplicate comments:
In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 119-138: The advances resource captured a stale params object at
creation (params: { expense_claim: expenseClaim.value }) so calling
advances.reload() reuses the old snapshot; instead, when you need fresh data
(where reload() is currently invoked) call advances.fetch and pass the current
params object built from expenseClaim.value (i.e., request the resource with
params:{ expense_claim: expenseClaim.value }) so the request uses the up-to-date
expenseClaim fields; update the call site that uses advances.reload() to use
advances.fetch with the current params and keep existing onSuccess handling
(e.g., the logic that sets expenseClaim.value.advances and calls
calculateTotalAdvance()).

---

Nitpick comments:
In `@frontend/src/composables/useCurrencyConversion.js`:
- Around line 14-17: The fetchCompanyCurrency function is marked async and uses
await even though getCompanyCurrency returns a synchronous value; remove the
async keyword and the await and assign companyCurrency.value directly from
getCompanyCurrency(doc.company). Also update any watcher callback that calls
fetchCompanyCurrency (the reactive watcher in this composable) to call the
now-synchronous fetchCompanyCurrency directly (no need to handle a Promise).
- Around line 41-50: The watcher created by watch(...) inside this composable is
registered every time the caller invokes the function and never cleaned up;
capture the stop handle (const stop = watch(...)) and expose it (e.g., return
stopWatch or stop from the composable) or move the watch out so the caller
manages lifecycle; specifically, change the code that defines the watch around
fetchCompanyCurrency and updateLabels to store the stop handle and return a stop
function alongside updateLabels and companyCurrency so the caller can call
stop() before re-invoking or on unmount.

In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 269-293: The watcher is using { deep: true } unnecessarily for
primitive numeric dependencies; in the watch call that observes
expenseClaim.value.total_sanctioned_amount, total_advance_amount, grand_total,
total_claimed_amount, total_taxes_and_charges and exchange_rate, remove the {
deep: true } option so Vue will use the default shallow comparison for these
primitive values; keep the rest of the handler intact (the fieldsToConvert array
and the call to updateBaseFieldsAmount with doc: expenseClaim.value and
exchangeRate: expenseClaim.value.exchange_rate).
- Around line 242-267: The external watch in Form.vue is creating duplicate
watchers because updateCurrencyLabels itself registers a watch on doc.currency;
remove the surrounding watch and instead call updateCurrencyLabels once when
formFields.data and expenseClaim.value.currency are available (e.g., in mounted
or a small computed/nextTick helper) so the composable's internal watcher is the
sole watcher; target the existing updateCurrencyLabels invocation and replace
the watch(...) block with a one-time guarded call that checks for fields and
currency before calling updateCurrencyLabels.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4850c360-6a66-409d-ad36-dbbdbb8b3b8b

📥 Commits

Reviewing files that changed from the base of the PR and between cde96b3 and f7ef13f.

📒 Files selected for processing (2)
  • frontend/src/composables/useCurrencyConversion.js
  • frontend/src/views/expense_claim/Form.vue

@github-actions
Copy link
Copy Markdown

This pull request is being marked as inactive because of no recent activity.
If your PR hasn't been reviewed, it's likely because it doesn't fullfill the contribution guidelines. Please read them carefully and fix the pull request. When you are sure all items are checked, please ping relevant codeowner in the comment. Be nice, they have a lot on their plate too.

It will be closed in 3 days if no further activity occurs.
Thank you for contributing!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/src/views/expense_claim/Form.vue (3)

111-114: ⚠️ Potential issue | 🟠 Major

Avoid reloading the whole schema on currency changes.

formFields.reload() retriggers formFields.onSuccess(), which reloads company details and writes cost_center / payable_account back to defaults. A currency toggle can therefore wipe a user-selected payable account, and it also replaces the field objects that setExchangeRate() just mutated.

Also applies to: 151-155, 185-195

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 111 - 114, The
onSuccess handler is causing full schema reloads (via formFields.reload()) which
triggers formFields.onSuccess() and resets company defaults
(cost_center/payable_account) and overwrites mutations made by
setExchangeRate(); instead, stop reloading the entire formFields on currency
toggle — remove or avoid calling formFields.reload() in the currency-change path
and instead update only the specific fields changed (call a targeted
update/patch or mutate the particular field objects and then trigger a minimal
reactive refresh). Ensure expenseApproverDetails.reload() and
companyDetails.reload() are only invoked where truly necessary and that
setExchangeRate() mutations are preserved by updating the same field objects
(cost_center, payable_account) rather than replacing them.

177-195: ⚠️ Potential issue | 🟠 Major

Refresh the exchange rate whenever the currency pair changes.

This only reacts to expenseClaim.value.currency, and setExchangeRate() only fetches when expenseClaim.value.exchange_rate is falsy. After the first lookup, changing USD → EUR or changing company keeps the old rate, so the base-field recalculation below uses the previous pair.

Also applies to: 269-293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 177 - 195, The
currency watcher (and the company watcher flow) doesn't force a new lookup
because setExchangeRate() only fetches when expenseClaim.value.exchange_rate is
falsy; change the flow so that changing the currency pair always refreshes the
rate: add a force parameter to setExchangeRate(force = false) and make
setExchangeRate bypass the "only when falsy" guard when force is true, then
update the watcher for expenseClaim.value.currency (and the company-related
watcher that can affect the pair) to call setExchangeRate(true) and then call
formFields.reload() after the forced fetch resolves; this ensures functions
named setExchangeRate, the currency watcher, and the company watcher refresh the
exchange rate whenever the pair changes.

444-450: ⚠️ Potential issue | 🟠 Major

Zero totals must clear any existing advance allocation.

If the user removes all expenses/taxes after an advance was allocated, Line 450 returns before resetting allocated_amount, selected, and total_advance_amount. The form can keep stale advances and a wrong grand_total.

Suggested fix
 function allocateAdvanceAmount() {
 	// allocate reqd advance amount
 	let amount_to_be_allocated =
 		parseFloat(expenseClaim.value.total_sanctioned_amount) +
 		parseFloat(expenseClaim.value.total_taxes_and_charges)

-	if (!amount_to_be_allocated) return
+	if (!amount_to_be_allocated) {
+		expenseClaim?.value?.advances?.forEach((advance) => {
+			advance.allocated_amount = 0
+			advance.selected = false
+		})
+		expenseClaim.value.total_advance_amount = 0
+		calculateGrandTotal()
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 444 - 450, The
allocateAdvanceAmount function exits early when amount_to_be_allocated is falsy,
leaving stale advance state; update it so when amount_to_be_allocated is 0 or
invalid you explicitly clear/reset advance-related state (set allocated_amount
to 0, clear selected allocation(s) and set total_advance_amount to 0) before
returning. Locate allocateAdvanceAmount and add a guard that normalizes/parses
totals (avoid NaN), then if amount_to_be_allocated === 0 or
isNaN(amount_to_be_allocated) perform the resets for allocated_amount, selected,
and total_advance_amount and then return.
♻️ Duplicate comments (1)
frontend/src/views/expense_claim/Form.vue (1)

500-517: ⚠️ Potential issue | 🟠 Major

exchange_rate is still forced visible in every path.

When transaction and company currencies match, Lines 506-509 hide the field, but Line 516 immediately unhides it again. Please also guard against a missing base currency before comparing or fetching.

Suggested fix
 function setExchangeRate() {
-	if (!expenseClaim.value.currency || !formFields.data) return
+	if (!expenseClaim.value.currency || !formFields.data || !companyCurrency.value) return
 	const exchange_rate_field = formFields.data?.find(
 		(field) => field.fieldname === "exchange_rate"
 	)

 	if (currency.value === companyCurrency.value) {
 		expenseClaim.value.exchange_rate = 1
 		if (exchange_rate_field) exchange_rate_field.hidden = 1
+		return
 	}
+	if (exchange_rate_field) exchange_rate_field.hidden = 0
 	if (!expenseClaim.value.exchange_rate) {
 		exchangeRate.fetch({
 			from_currency: currency.value,
 			to_currency: companyCurrency.value,
 		})
 	}
-	if (exchange_rate_field) exchange_rate_field.hidden = 0
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 500 - 517, In
setExchangeRate(), avoid unconditionally setting exchange_rate_field.hidden = 0
at the end which re-shows the field in every path; instead make visibility
changes conditional: if currency.value and companyCurrency.value are present,
set expenseClaim.value.exchange_rate = 1 and hide exchange_rate_field when
currency.value === companyCurrency.value, otherwise if
expenseClaim.value.exchange_rate is falsy call exchangeRate.fetch({
from_currency: currency.value, to_currency: companyCurrency.value }) and show
the field; also add guards at the top to return early if either currency.value
or companyCurrency.value is missing to prevent invalid comparisons/fetches. Use
the existing symbols: setExchangeRate, expenseClaim.value, formFields.data,
exchange_rate_field, currency.value, companyCurrency.value, and
exchangeRate.fetch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 242-267: You are registering updateCurrencyLabels repeatedly via a
watcher on formFields.data and expenseClaim.value.currency; remove this watcher
and instead call updateCurrencyLabels only once to initialize labels when
formFields.data becomes available (or invoke it from onMounted/after
formFields.reload), letting the composable's own watch(() => [doc.company,
doc.currency], ...) handle subsequent updates; locate the current watch block
using updateCurrencyLabels, formFields.data and expenseClaim.value.currency and
replace it with a single initialization call (guarded by if (!fields ||
!currency) return) or trigger the composable's exposed refresh method rather
than re-watching.
- Around line 213-231: The watcher on expenseClaim.value should be changed to
deeply watch the advances array so in-place mutations (like
advances[i].allocated_amount or advances[i].selected from ExpenseAdvancesTable)
trigger re-checking; update the watcher to observe expenseClaim.value.advances
with { deep: true, immediate: true } and keep the same body that flips
advance.selected and calls calculateTotalAdvance() when needed; also audit
similar watchers/functions that rely on selected (e.g., the other watchers
referenced around validateForm and the blocks at lines ~469-479 and ~488-494)
and convert them to deep watchers or watch the advances array directly so
validateForm’s filtering logic stays consistent with calculateTotalAdvance().

---

Outside diff comments:
In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 111-114: The onSuccess handler is causing full schema reloads (via
formFields.reload()) which triggers formFields.onSuccess() and resets company
defaults (cost_center/payable_account) and overwrites mutations made by
setExchangeRate(); instead, stop reloading the entire formFields on currency
toggle — remove or avoid calling formFields.reload() in the currency-change path
and instead update only the specific fields changed (call a targeted
update/patch or mutate the particular field objects and then trigger a minimal
reactive refresh). Ensure expenseApproverDetails.reload() and
companyDetails.reload() are only invoked where truly necessary and that
setExchangeRate() mutations are preserved by updating the same field objects
(cost_center, payable_account) rather than replacing them.
- Around line 177-195: The currency watcher (and the company watcher flow)
doesn't force a new lookup because setExchangeRate() only fetches when
expenseClaim.value.exchange_rate is falsy; change the flow so that changing the
currency pair always refreshes the rate: add a force parameter to
setExchangeRate(force = false) and make setExchangeRate bypass the "only when
falsy" guard when force is true, then update the watcher for
expenseClaim.value.currency (and the company-related watcher that can affect the
pair) to call setExchangeRate(true) and then call formFields.reload() after the
forced fetch resolves; this ensures functions named setExchangeRate, the
currency watcher, and the company watcher refresh the exchange rate whenever the
pair changes.
- Around line 444-450: The allocateAdvanceAmount function exits early when
amount_to_be_allocated is falsy, leaving stale advance state; update it so when
amount_to_be_allocated is 0 or invalid you explicitly clear/reset
advance-related state (set allocated_amount to 0, clear selected allocation(s)
and set total_advance_amount to 0) before returning. Locate
allocateAdvanceAmount and add a guard that normalizes/parses totals (avoid NaN),
then if amount_to_be_allocated === 0 or isNaN(amount_to_be_allocated) perform
the resets for allocated_amount, selected, and total_advance_amount and then
return.

---

Duplicate comments:
In `@frontend/src/views/expense_claim/Form.vue`:
- Around line 500-517: In setExchangeRate(), avoid unconditionally setting
exchange_rate_field.hidden = 0 at the end which re-shows the field in every
path; instead make visibility changes conditional: if currency.value and
companyCurrency.value are present, set expenseClaim.value.exchange_rate = 1 and
hide exchange_rate_field when currency.value === companyCurrency.value,
otherwise if expenseClaim.value.exchange_rate is falsy call exchangeRate.fetch({
from_currency: currency.value, to_currency: companyCurrency.value }) and show
the field; also add guards at the top to return early if either currency.value
or companyCurrency.value is missing to prevent invalid comparisons/fetches. Use
the existing symbols: setExchangeRate, expenseClaim.value, formFields.data,
exchange_rate_field, currency.value, companyCurrency.value, and
exchangeRate.fetch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fb212c6d-cd27-4ec8-af8f-4095c001cf8b

📥 Commits

Reviewing files that changed from the base of the PR and between f7ef13f and 1164ff8.

📒 Files selected for processing (2)
  • frontend/src/components/ExpensesTable.vue
  • frontend/src/views/expense_claim/Form.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/ExpensesTable.vue

Comment on lines +213 to +231
watch(
() => expenseClaim.value,
(newDoc) => {
if (newDoc?.advances?.length > 0) {
let needsRecalc = false
newDoc.advances.forEach(advance => {
// Reapply the "selected" flag if money is allocated
if (parseFloat(advance.allocated_amount || 0) > 0 && !advance.selected) {
advance.selected = true
needsRecalc = true
}
})
if (needsRecalc) {
calculateTotalAdvance()
}
}
},
{ immediate: true }
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd 'ExpenseAdvancesTable\.vue$' -x sh -c '
  echo "== {} =="
  rg -n -C3 "allocated_amount|selected|v-model:expenseClaim|emit\\(|expenseClaim" "{}"
'

Repository: frappe/hrms

Length of output: 1246


🏁 Script executed:

#!/bin/bash
# Check Form.vue at the mentioned line ranges
fd 'Form\.vue$' -path '*/expense_claim/*' -x sh -c '
  echo "=== Lines 469-479 ==="
  sed -n "469,479p" "{}"
  echo ""
  echo "=== Lines 488-494 ==="
  sed -n "488,494p" "{}"
'

Repository: frappe/hrms

Length of output: 223


🏁 Script executed:

#!/bin/bash
# Find Form.vue in expense_claim directory
fd 'Form\.vue' | grep expense_claim

# Read the specific lines from Form.vue
file=$(fd 'Form\.vue' | grep expense_claim)
if [ -n "$file" ]; then
  echo "=== Lines 469-479 ==="
  sed -n '469,479p' "$file"
  echo ""
  echo "=== Lines 488-494 ==="
  sed -n '488,494p' "$file"
fi

Repository: frappe/hrms

Length of output: 768


🏁 Script executed:

#!/bin/bash
# Find all watch declarations in Form.vue
rg -n "watch\(" frontend/src/views/expense_claim/Form.vue | head -20

Repository: frappe/hrms

Length of output: 153


🏁 Script executed:

#!/bin/bash
# Get more context around the watch statements
rg -n "watch\(" -A 5 frontend/src/views/expense_claim/Form.vue | head -100

Repository: frappe/hrms

Length of output: 1679


🏁 Script executed:

#!/bin/bash
# Get the full watcher at line 205-210 with more context
sed -n '205,232p' frontend/src/views/expense_claim/Form.vue

Repository: frappe/hrms

Length of output: 602


🏁 Script executed:

#!/bin/bash
# Get context around calculateTotalAdvance and validateForm to understand the flow
echo "=== calculateTotalAdvance (around 469) ==="
sed -n '469,480p' frontend/src/views/expense_claim/Form.vue

echo ""
echo "=== validateForm (around 488) ==="
sed -n '488,500p' frontend/src/views/expense_claim/Form.vue

echo ""
echo "=== Check if there are other watchers watching expenseClaim value (shallow) ==="
rg -n "watch\(\s*\(\)\s*=>\s*expenseClaim\.value," frontend/src/views/expense_claim/Form.vue

Repository: frappe/hrms

Length of output: 988


🏁 Script executed:

#!/bin/bash
# Check the exact structure of the watchers - is there any other shallow watch on expenseClaim.value?
echo "=== All watch calls with their complete structure ==="
sed -n '205,280p' frontend/src/views/expense_claim/Form.vue | grep -A 6 "watch("

Repository: frappe/hrms

Length of output: 1013


Change this shallow watcher to deeply watch the advances array so it re-applies the selected flag when allocated_amount is mutated.

watch(() => expenseClaim.value, ...) only triggers when the claim object reference changes. When ExpenseAdvancesTable mutates advances[i].allocated_amount or selected in place, this callback never re-applies the selected flag. Meanwhile, calculateTotalAdvance() counts allocations where either selected is true OR allocated_amount > 0, so unsaved amounts get included in the total. When validateForm() later filters advances to only those with selected: true, the count becomes inaccurate.

Change to: watch(() => expenseClaim.value.advances, ..., { deep: true, immediate: true })

This applies to other watchers or functions that depend on the selected flag state at lines 469-479 and 488-494.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 213 - 231, The
watcher on expenseClaim.value should be changed to deeply watch the advances
array so in-place mutations (like advances[i].allocated_amount or
advances[i].selected from ExpenseAdvancesTable) trigger re-checking; update the
watcher to observe expenseClaim.value.advances with { deep: true, immediate:
true } and keep the same body that flips advance.selected and calls
calculateTotalAdvance() when needed; also audit similar watchers/functions that
rely on selected (e.g., the other watchers referenced around validateForm and
the blocks at lines ~469-479 and ~488-494) and convert them to deep watchers or
watch the advances array directly so validateForm’s filtering logic stays
consistent with calculateTotalAdvance().

Comment on lines +242 to +267
watch(
() => [formFields.data, expenseClaim.value.currency],
([fields, currency]) => {
if (!fields || !currency) return

updateCurrencyLabels({
formFields: fields,
doc: expenseClaim.value,
baseFields: [
"base_total_sanctioned_amount",
"base_total_taxes_and_charges",
"base_total_advance_amount",
"base_grand_total",
"base_total_claimed_amount"
],
transactionFields: [
"total_sanctioned_amount",
"total_taxes_and_charges",
"total_advance_amount",
"grand_total",
"total_claimed_amount"
],
})
},
{ immediate: true }
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

updateCurrencyLabels() is being subscribed over and over.

The composable already installs its own watch(() => [doc.company, doc.currency], ...). Calling it from another watcher here registers a new watcher every time formFields.data or currency changes, so currency toggles / formFields.reload() will stack extra company-currency fetches and label mutations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/expense_claim/Form.vue` around lines 242 - 267, You are
registering updateCurrencyLabels repeatedly via a watcher on formFields.data and
expenseClaim.value.currency; remove this watcher and instead call
updateCurrencyLabels only once to initialize labels when formFields.data becomes
available (or invoke it from onMounted/after formFields.reload), letting the
composable's own watch(() => [doc.company, doc.currency], ...) handle subsequent
updates; locate the current watch block using updateCurrencyLabels,
formFields.data and expenseClaim.value.currency and replace it with a single
initialization call (guarded by if (!fields || !currency) return) or trigger the
composable's exposed refresh method rather than re-watching.

"`tabExpense Claim`.name",
"`tabExpense Claim`.employee",
"`tabExpense Claim`.employee_name",
"`tabExpense Claim`.currency",
Copy link
Copy Markdown
Member

@ruchamahabal ruchamahabal Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currency shows up in list view but not in recent expenses

Before:

Image

After:

Image

() => setExchangeRate()
(currency) => {
if (!currency) return
formFields.reload()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to reload formfields when currency changes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update fields label for all base currency (multicurrency) in advance & claim in PWA

2 participants