diff --git a/frontend/src/components/ExpenseAdvancesTable.vue b/frontend/src/components/ExpenseAdvancesTable.vue index 918d894384..16da1208e0 100644 --- a/frontend/src/components/ExpenseAdvancesTable.vue +++ b/frontend/src/components/ExpenseAdvancesTable.vue @@ -21,7 +21,7 @@
@@ -34,7 +34,7 @@
{{ __("{0}: {1}", [ __("Unclaimed Amount"), - formatCurrency(advance.unclaimed_amount, currency), + formatCurrency(advance.unclaimed_amount, expenseClaim.currency), ]) }}
@@ -74,17 +74,13 @@ const props = defineProps({ type: Object, required: true, }, - currency: { - type: String, - required: true, - }, isReadOnly: { type: Boolean, default: false, }, }) -const currencySymbol = computed(() => getCurrencySymbol(props.currency)) +const currencySymbol = computed(() => getCurrencySymbol(props.expenseClaim.currency)) function toggleAdvanceSelection(advance) { if (props.isReadOnly) return diff --git a/frontend/src/components/ExpenseClaimItem.vue b/frontend/src/components/ExpenseClaimItem.vue index cceb4c7762..cf9e960100 100644 --- a/frontend/src/components/ExpenseClaimItem.vue +++ b/frontend/src/components/ExpenseClaimItem.vue @@ -14,7 +14,7 @@ {{ claimDates }} · - {{ formatCurrency(props.doc.total_claimed_amount, currency) }} + {{ formatCurrency(props.doc.total_claimed_amount, props.doc.currency) }} @@ -33,7 +33,6 @@ import { computed, inject } from "vue" import ListItem from "@/components/ListItem.vue" import ExpenseIcon from "@/components/icons/ExpenseIcon.vue" -import { getCompanyCurrency } from "@/data/currencies" import { formatCurrency } from "@/utils/formatters" const dayjs = inject("$dayjs") @@ -99,7 +98,6 @@ const claimDates = computed(() => { } }) -const currency = computed(() => getCompanyCurrency(props.doc.company)) const approvalStatus = computed(() => { return props.doc.approval_status === "Draft" ? "Pending" : props.doc.approval_status diff --git a/frontend/src/components/ExpenseItems.vue b/frontend/src/components/ExpenseItems.vue index fbe22fd0ca..29ae4d0a2a 100644 --- a/frontend/src/components/ExpenseItems.vue +++ b/frontend/src/components/ExpenseItems.vue @@ -21,7 +21,7 @@ {{ __("{0}: {1}", [ __("Sanctioned"), - formatCurrency(item.sanctioned_amount || 0, currency), + formatCurrency(item.sanctioned_amount || 0, doc.currency), ]) }} @@ -33,7 +33,7 @@ - {{ formatCurrency(item.amount, currency) }} + {{ formatCurrency(item.amount, doc.currency) }} @@ -42,9 +42,8 @@ diff --git a/frontend/src/components/ExpenseTaxesTable.vue b/frontend/src/components/ExpenseTaxesTable.vue index 1bef2bc2a8..841d38ff11 100644 --- a/frontend/src/components/ExpenseTaxesTable.vue +++ b/frontend/src/components/ExpenseTaxesTable.vue @@ -4,7 +4,7 @@

{{ __("Taxes & Charges") }}

- {{ formatCurrency(expenseClaim.total_taxes_and_charges, currency) }} + {{ formatCurrency(expenseClaim.total_taxes_and_charges, expenseClaim.currency) }}
- {{ formatCurrency(item.total, currency) }} + {{ formatCurrency(item.total, expenseClaim.currency) }}
@@ -134,16 +134,13 @@ import EmptyState from "@/components/EmptyState.vue" import CustomIonModal from "@/components/CustomIonModal.vue" import { formatCurrency } from "@/utils/formatters" +import { useCurrencyConversion } from "@/composables/useCurrencyConversion" const props = defineProps({ expenseClaim: { type: Object, required: true, }, - currency: { - type: String, - required: true, - }, isReadOnly: { type: Boolean, default: false, @@ -215,6 +212,13 @@ const taxesTableFields = createResource({ }) taxesTableFields.reload() +const expenseClaimRef = computed(() => props.expenseClaim) +useCurrencyConversion( + taxesTableFields, + expenseClaimRef, + ["tax_amount", "total"] +) + const modalTitle = computed(() => { if (props.isReadOnly) return __("Expense Tax") diff --git a/frontend/src/components/ExpensesTable.vue b/frontend/src/components/ExpensesTable.vue index c12746d5cd..80b62eb921 100644 --- a/frontend/src/components/ExpensesTable.vue +++ b/frontend/src/components/ExpensesTable.vue @@ -4,7 +4,7 @@

{{ __("Expenses") }}

- {{ formatCurrency(expenseClaim.total_claimed_amount, currency) }} + {{ formatCurrency(expenseClaim.total_claimed_amount, expenseClaim.currency) }}
- {{ formatCurrency(item.amount, currency) }} + {{ formatCurrency(item.amount, expenseClaim.currency) }}
@@ -75,7 +75,7 @@
-
+
!excludeFields.includes(field.fieldname)) }, }) expensesTableFields.reload() +const expenseClaimRef = computed(() => props.expenseClaim) +useCurrencyConversion( + expensesTableFields, + expenseClaimRef, + ["amount", "sanctioned_amount"] +) + const modalTitle = computed(() => { if (props.isReadOnly) return __("Expense Item") diff --git a/frontend/src/components/FormView.vue b/frontend/src/components/FormView.vue index d94fc2b91f..779904d88c 100644 --- a/frontend/src/components/FormView.vue +++ b/frontend/src/components/FormView.vue @@ -386,7 +386,7 @@ const props = defineProps({ default: false, }, }) -const emit = defineEmits(["validateForm", "update:modelValue"]) +const emit = defineEmits(["validateForm", "update:modelValue", "formReloaded"]) const router = useRouter() const { downloadPDF } = useDownloadPDF() @@ -553,7 +553,6 @@ const docList = createListResource({ const documentResource = createDocumentResource({ doctype: props.doctype, name: props.id, - fields: "*", setValue: { onSuccess() { toast({ @@ -674,7 +673,7 @@ async function handleDocUpdate(action) { } else if (action == "cancel") { params.docstatus = 2 } - + await documentResource.setValue.submit(params) await documentResource.get.promise resetForm() @@ -720,6 +719,7 @@ function resetForm() { nextTick(() => { isFormDirty.value = false isFormUpdated.value = true + emit("formReloaded") }) } function handleDownload() { diff --git a/frontend/src/components/Link.vue b/frontend/src/components/Link.vue index 5ccea6c5b3..bd9387612d 100644 --- a/frontend/src/components/Link.vue +++ b/frontend/src/components/Link.vue @@ -43,8 +43,11 @@ const searchText = ref("") const value = computed({ get: () => props.modelValue, set: (val) => { - const newVal = (val && typeof val === "object" && val.value !== undefined) ? val.value : val - emit("update:modelValue", newVal || "") + if (typeof val === "string") { + emit("update:modelValue", val) + } else { + emit("update:modelValue", val?.value || "") + } }, }) @@ -72,28 +75,30 @@ const reloadOptions = (searchTextVal) => { params: { txt: searchTextVal, doctype: props.doctype, - filters: props.filters + filters: props.filters, }, }) options.reload() } const handleQueryUpdate = debounce((newQuery) => { - const val = newQuery || "" - - if (val === "" && props.modelValue) return - - if (searchText.value === val) return - searchText.value = val - reloadOptions(val) + const val = newQuery || "" + if (searchText.value === val) return + searchText.value = val + reloadOptions(val) }, 300) watch( () => props.doctype, () => { if (!props.doctype || props.doctype === options.doctype) return - reloadOptions(props.modelValue) + reloadOptions("") }, { immediate: true } ) + +watch( + () => props.filters, + () => reloadOptions(''), +) diff --git a/frontend/src/components/ListView.vue b/frontend/src/components/ListView.vue index 77c9179d6b..e3dfc17dff 100644 --- a/frontend/src/components/ListView.vue +++ b/frontend/src/components/ListView.vue @@ -284,7 +284,7 @@ const documents = createResource({ const createPermission = createResource({ url: "frappe.client.has_permission", - params: { doctype: props.doctype, docname: null, perm_type: "create" }, + params: { doctype: props.doctype, docname: "", perm_type: "create" }, auto: true, }) diff --git a/frontend/src/composables/useCurrencyConversion.js b/frontend/src/composables/useCurrencyConversion.js new file mode 100644 index 0000000000..060c2e1bcc --- /dev/null +++ b/frontend/src/composables/useCurrencyConversion.js @@ -0,0 +1,41 @@ +import { watch } from "vue" + +export function useCurrencyConversion(formFields, docRef, fieldsToConvert = []) { + /** + * Accepts a formFields resource, a doc ref and an array of fieldnames which are currency fields and need to have the currency in their label + * Watches and updates the labels of the currency fields to include the currency in labels + */ + const currencyFields = new Set([...fieldsToConvert]) + + const updateLabels = () => { + formFields.data?.forEach((field) => { + if (!field?.fieldname) return + if (!currencyFields.has(field.fieldname)) return + + if (!field._original_label && field.label) { + field._original_label = field.label.replace(/\([^\)]*\)/g, "").trim() + } + if (currencyFields.has(field.fieldname)) { + field.label = `${field._original_label} (${docRef.value.currency})` + } + }) + } + + watch( + () => docRef.value?.currency, + () => { + updateLabels() + }, + { immediate: true } + ) + + watch( + () => formFields.data, + () => { + updateLabels() + }, + { deep: true, immediate: true } + ) + + return { updateLabels } +} \ No newline at end of file diff --git a/frontend/src/data/currencies.js b/frontend/src/data/currencies.js index e95d3f99b1..25533df808 100644 --- a/frontend/src/data/currencies.js +++ b/frontend/src/data/currencies.js @@ -21,3 +21,13 @@ export function getCompanyCurrencySymbol(company) { export function getCurrencySymbol(currency) { return currencySymbols?.data?.[currency] } + +export const currencyPrecision = createResource({ + url: "frappe.client.get_single_value", + params: { + doctype: "System Settings", + field: "currency_precision" + }, + auto: true, + initialData: 2 +}); \ No newline at end of file diff --git a/frontend/src/views/employee_advance/Form.vue b/frontend/src/views/employee_advance/Form.vue index 8697eff316..be6ec1f040 100644 --- a/frontend/src/views/employee_advance/Form.vue +++ b/frontend/src/views/employee_advance/Form.vue @@ -18,11 +18,10 @@ \ No newline at end of file diff --git a/frontend/src/views/expense_claim/Form.vue b/frontend/src/views/expense_claim/Form.vue index 661e473d3b..e5f70eb9b2 100644 --- a/frontend/src/views/expense_claim/Form.vue +++ b/frontend/src/views/expense_claim/Form.vue @@ -13,12 +13,12 @@ :showAttachmentView="true" @validateForm="validateForm" :showDownloadPDFButton="true" + @formReloaded="onFormReloaded" > @@ -59,8 +56,8 @@ import FormView from "@/components/FormView.vue" import ExpensesTable from "@/components/ExpensesTable.vue" import ExpenseTaxesTable from "@/components/ExpenseTaxesTable.vue" import ExpenseAdvancesTable from "@/components/ExpenseAdvancesTable.vue" - import { getCompanyCurrency } from "@/data/currencies" +import { useCurrencyConversion } from "@/composables/useCurrencyConversion" const dayjs = inject("$dayjs") @@ -90,9 +87,10 @@ const tabs = [ const expenseClaim = ref({ employee: currEmployee, company: employeeCompany, + doctype: "Expense Claim", }) -const currency = computed(() => getCompanyCurrency(expenseClaim.value.company)) +const companyCurrency = computed(() => getCompanyCurrency(expenseClaim.value.company)) // get form fields const formFields = createResource({ @@ -108,46 +106,70 @@ const formFields = createResource({ }, onSuccess(_data) { expenseApproverDetails.reload() + if (!expenseClaim.value.currency) { + employeeCurrency.reload() + } companyDetails.reload() }, }) formFields.reload() -// resources +useCurrencyConversion( + formFields, + expenseClaim, + [ + "total_sanctioned_amount", + "total_taxes_and_charges", + "total_advance_amount", + "grand_total", + "total_claimed_amount" + ] +) + +// resources & helper functions const advances = createResource({ url: "hrms.hr.doctype.expense_claim.expense_claim.get_advances", - params: { employee: currEmployee.value }, - auto: true, + makeParams() { + return { expense_claim: expenseClaim.value } + }, onSuccess(data) { - // set advances - if (props.id) { - expenseClaim.value.advances?.map((advance) => (advance.selected = true)) - } else { - expenseClaim.value.advances = [] - } - - return data.forEach((advance) => { - if ( - props.id && - expenseClaim.value.advances?.some( - (entry) => entry.employee_advance === advance.name - ) - ) - return - - expenseClaim.value.advances?.push({ - employee_advance: advance.name, - purpose: advance.purpose, - posting_date: advance.posting_date, - advance_account: advance.advance_account, - advance_paid: advance.paid_amount, - unclaimed_amount: advance.paid_amount - advance.claimed_amount, - allocated_amount: 0, - }) - }) + selectAllocatedAdvances() + addUnallocatedAdvances(data) }, }) +function selectAllocatedAdvances() { + if (props.id) { + expenseClaim.value?.advances?.map((advance) => (advance.selected = true)) + } else { + expenseClaim.value.advances = [] + } +} + +function addUnallocatedAdvances(data) { + // only show advances for selection in a draft claim + const isDraft = expenseClaim.value?.docstatus == 0 || !expenseClaim.value?.docstatus + if (!isDraft) return + + const allocatedAdvances = new Set( + expenseClaim.value?.advances?.map((advance) => advance.employee_advance) + ) + + return data.forEach((advance) => { + if (props.id && allocatedAdvances.has(advance.employee_advance)) return + + expenseClaim.value?.advances?.push({ + ...advance, + selected: false, + allocated_amount: 0, + }) + }) +} + +function onFormReloaded() { + advances.reload() +} + const expenseApproverDetails = createResource({ url: "hrms.api.get_expense_approval_details", params: { employee: currEmployee.value }, @@ -156,6 +178,22 @@ const expenseApproverDetails = createResource({ }, }) +const employeeCurrency = createResource({ + url: "frappe.client.get_value", + makeParams() { + return { + doctype: "Employee", + fieldname: ["salary_currency"], + filters: { name: currEmployee.value }, + }; + }, + onSuccess(data) { + if (data?.salary_currency) { + expenseClaim.value.currency = data.salary_currency; + } + } +}); + const companyDetails = createResource({ url: "hrms.api.get_company_cost_center_and_expense_account", params: { company: expenseClaim.value.company }, @@ -166,6 +204,13 @@ const companyDetails = createResource({ }, }) +const exchangeRate = createResource({ + url: "erpnext.setup.utils.get_exchange_rate", + onSuccess(data) { + expenseClaim.value.exchange_rate = data + }, +}) + // form scripts watch( () => expenseClaim.value.employee, @@ -176,8 +221,10 @@ watch( } currEmployee.value = employee_id expenseApproverDetails.fetch({ employee: currEmployee.value }) - } + employeeCurrency.fetch() + }, ) + watch( () => expenseClaim.value.company, (company) => { @@ -185,13 +232,18 @@ watch( companyDetails.fetch({ company: employeeCompany.value }) } ) + watch( - () => props.id && expenseClaim.value.expenses, - (_) => { - if (expenseClaim.value.docstatus === 0) { - advances.reload() - } - } + () => expenseClaim.value.currency, + () => setExchangeRate() +) + +watch( + () => expenseClaim.value.name, + () => { + advances.reload() + }, + { immediate: true } ) watch( @@ -235,7 +287,12 @@ function getFilteredFields(fields) { if (!props.id) excludeFields.push(...extraFields) - return fields.filter((field) => !excludeFields.includes(field.fieldname)) + return fields.filter((field) => { + if (excludeFields.includes(field.fieldname)) return false + + if (field.fieldname?.startsWith("base_")) return false + return true + }) } function applyFilters(field) { @@ -245,6 +302,7 @@ function applyFilters(field) { account_type: "Payable", company: expenseClaim.value.company, is_group: 0, + account_currency: expenseClaim.value.currency, } } else if (field.fieldname === "cost_center") { field.linkFilters = { @@ -364,6 +422,8 @@ function allocateAdvanceAmount() { let amount_to_be_allocated = parseFloat(expenseClaim.value.total_sanctioned_amount) + parseFloat(expenseClaim.value.total_taxes_and_charges) + + if (!amount_to_be_allocated) return let total_advance_amount = 0 expenseClaim?.value?.advances?.forEach((advance) => { @@ -387,8 +447,8 @@ function calculateTotalAdvance() { let total_advance_amount = 0 expenseClaim?.value?.advances?.forEach((advance) => { - if (advance.selected) { - total_advance_amount += parseFloat(advance.allocated_amount) + if (advance.selected || parseFloat(advance.allocated_amount) > 0) { + total_advance_amount += parseFloat(advance.allocated_amount || 0) } }) expenseClaim.value.total_advance_amount = total_advance_amount @@ -413,4 +473,16 @@ function validateForm() { }) } +function setExchangeRate() { + if (!expenseClaim.value.currency || !formFields.data) return + const exchange_rate_field = formFields.data?.find( + (field) => field.fieldname === "exchange_rate" + ) + + exchangeRate.fetch({ + from_currency: expenseClaim.value.currency, + to_currency: companyCurrency.value, + }) + if (exchange_rate_field) exchange_rate_field.hidden = 0 +} \ No newline at end of file diff --git a/frontend/src/views/expense_claim/List.vue b/frontend/src/views/expense_claim/List.vue index 0edc3aa7e8..07e656a92e 100644 --- a/frontend/src/views/expense_claim/List.vue +++ b/frontend/src/views/expense_claim/List.vue @@ -21,6 +21,7 @@ const EXPENSE_CLAIM_FIELDS = [ "`tabExpense Claim`.name", "`tabExpense Claim`.employee", "`tabExpense Claim`.employee_name", + "`tabExpense Claim`.currency", "`tabExpense Claim`.approval_status", "`tabExpense Claim`.status", "`tabExpense Claim`.expense_approver", @@ -28,7 +29,7 @@ const EXPENSE_CLAIM_FIELDS = [ "`tabExpense Claim`.posting_date", "`tabExpense Claim`.company", "`tabExpense Claim Detail`.expense_type", - "count(`tabExpense Claim Detail`.expense_type) as total_expenses", + {"COUNT": "`tabExpense Claim Detail`.expense_type", "as":"total_expenses"} ] const FILTER_CONFIG = [ diff --git a/hrms/api/__init__.py b/hrms/api/__init__.py index 92e2a08ef8..5414b6f9ac 100644 --- a/hrms/api/__init__.py +++ b/hrms/api/__init__.py @@ -495,6 +495,7 @@ def get_expense_claims( "`tabExpense Claim`.posting_date", "`tabExpense Claim`.employee", "`tabExpense Claim`.employee_name", + "`tabExpense Claim`.currency", "`tabExpense Claim`.approval_status", "`tabExpense Claim`.status", "`tabExpense Claim`.expense_approver", @@ -642,11 +643,6 @@ def get_employee_advance_balance(employee: str) -> list[dict]: return advances -@frappe.whitelist() -def get_advance_account(company: str) -> str | None: - return frappe.db.get_value("Company", company, "default_employee_advance_account", cache=True) - - # Company @frappe.whitelist() def get_company_currencies() -> dict: diff --git a/hrms/hr/doctype/expense_claim/expense_claim.js b/hrms/hr/doctype/expense_claim/expense_claim.js index 7bfe6935d2..6c6d98112e 100644 --- a/hrms/hr/doctype/expense_claim/expense_claim.js +++ b/hrms/hr/doctype/expense_claim/expense_claim.js @@ -141,9 +141,9 @@ frappe.ui.form.on("Expense Claim", { }, currency: function (frm) { + frm.trigger("set_exchange_rate"); frm.trigger("update_fields_label"); frm.trigger("update_child_fields_label"); - frm.trigger("set_exchange_rate"); }, set_exchange_rate: function (frm) { @@ -459,7 +459,6 @@ frappe.ui.form.on("Expense Claim", { }, get_advances: function (frm) { - frappe.model.clear_table(frm.doc, "advances"); if (frm.doc.employee) { return frappe.call({ method: "hrms.hr.doctype.expense_claim.expense_claim.get_advances", @@ -467,6 +466,7 @@ frappe.ui.form.on("Expense Claim", { expense_claim: frm.doc, }, callback: function (r, rt) { + frappe.model.clear_table(frm.doc, "advances"); if (r.message) { $.each(r.message, function (i, d) { var row = frappe.model.add_child( diff --git a/roster/src/components/Link.vue b/roster/src/components/Link.vue index 3f6056dfe6..d473338dcc 100644 --- a/roster/src/components/Link.vue +++ b/roster/src/components/Link.vue @@ -51,9 +51,11 @@ const searchText = ref(""); const value = computed({ get: () => props.modelValue, set: (val) => { - const newVal = val && typeof val === "object" && val.value !== undefined ? val.value : val; - console.log(newVal); - emit("update:modelValue", newVal || ""); + if (typeof val === "string") { + emit("update:modelValue", val); + } else { + emit("update:modelValue", val?.value || ""); + } }, }); @@ -88,9 +90,6 @@ const reloadOptions = (searchTextVal) => { const handleQueryUpdate = debounce((newQuery) => { const val = newQuery || ""; - - if (val === "" && props.modelValue) return; - if (searchText.value === val) return; searchText.value = val; reloadOptions(val); @@ -104,4 +103,9 @@ watch( }, { immediate: true }, ); + +watch( + () => props.filters, + () => reloadOptions(""), +);