+
!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(""),
+);