Merge branch 'main-hotfix' into mergify/bp/main-hotfix/pr-1084

This commit is contained in:
Shariq Ansari 2025-07-28 18:17:45 +05:30 committed by GitHub
commit 97d9866c0e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 157 additions and 103 deletions

View File

@ -7,10 +7,8 @@ from frappe.desk.form.assign_to import add as assign
from frappe.model.document import Document
from crm.fcrm.doctype.crm_service_level_agreement.utils import get_sla
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import (
add_status_change_log,
)
from crm.utils import get_exchange_rate
from crm.fcrm.doctype.crm_status_change_log.crm_status_change_log import add_status_change_log
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMDeal(Document):
@ -177,7 +175,7 @@ class CRMDeal(Document):
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
exchange_rate = get_exchange_rate(self.currency, system_currency)
self.db_set("exchange_rate", exchange_rate)

View File

@ -4,7 +4,7 @@
import frappe
from frappe.model.document import Document
from crm.utils import get_exchange_rate
from crm.fcrm.doctype.fcrm_settings.fcrm_settings import get_exchange_rate
class CRMOrganization(Document):
@ -16,7 +16,7 @@ class CRMOrganization(Document):
system_currency = frappe.db.get_single_value("FCRM Settings", "currency") or "USD"
exchange_rate = 1
if self.currency and self.currency != system_currency:
exchange_rate = get_exchange_rate(self.currency, system_currency, frappe.utils.nowdate())
exchange_rate = get_exchange_rate(self.currency, system_currency)
self.db_set("exchange_rate", exchange_rate)

View File

@ -8,7 +8,12 @@
"defaults_tab",
"restore_defaults",
"enable_forecasting",
"currency_tab",
"currency",
"exchange_rate_provider_section",
"service_provider",
"column_break_vqck",
"access_key",
"branding_tab",
"brand_name",
"brand_logo",
@ -72,13 +77,42 @@
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "currency_tab",
"fieldtype": "Tab Break",
"label": "Currency"
},
{
"fieldname": "exchange_rate_provider_section",
"fieldtype": "Section Break",
"label": "Exchange Rate Provider"
},
{
"default": "frankfurter.app",
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host",
"reqd": 1
},
{
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key",
"mandatory_depends_on": "eval:doc.service_provider == 'exchangerate.host';"
},
{
"fieldname": "column_break_vqck",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-07-13 11:58:34.857638",
"modified_by": "Administrator",
"modified": "2025-07-28 17:04:24.585768",
"modified_by": "shariq@frappe.io",
"module": "FCRM",
"name": "FCRM Settings",
"owner": "Administrator",

View File

@ -2,6 +2,7 @@
# For license information, please see license.txt
import frappe
import requests
from frappe import _
from frappe.custom.doctype.property_setter.property_setter import delete_property_setter, make_property_setter
from frappe.model.document import Document
@ -132,3 +133,76 @@ def get_forecasting_script():
this.doc.probability = status.probability
}
}"""
def get_exchange_rate(from_currency, to_currency, date=None):
if not date:
date = "latest"
api_used = "frankfurter"
api_endpoint = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
res = requests.get(api_endpoint, timeout=5)
if res.ok:
data = res.json()
return data["rates"][to_currency]
# Fallback to exchangerate.host if Frankfurter API fails
settings = FCRMSettings("FCRM Settings")
if settings and settings.service_provider == "exchangerate.host":
api_used = "exchangerate.host"
if not settings.access_key:
frappe.throw(
_("Access Key is required for Service Provider: {0}").format(
frappe.bold(settings.service_provider)
)
)
params = {
"access_key": settings.access_key,
"from": from_currency,
"to": to_currency,
"amount": 1,
}
if date != "latest":
params["date"] = date
api_endpoint = "https://api.exchangerate.host/convert"
res = requests.get(api_endpoint, params=params, timeout=5)
if res.ok:
data = res.json()
return data["result"]
frappe.log_error(
title="Exchange Rate Fetch Error",
message=f"Failed to fetch exchange rate from {from_currency} to {to_currency} using {api_used} API.",
)
if api_used == "frankfurter":
user = frappe.session.user
is_manager = (
"System Manager" in frappe.get_roles(user)
or "Sales Manager" in frappe.get_roles(user)
or user == "Administrator"
)
if not is_manager:
frappe.throw(
_(
"Ask your manager to set up the Exchange Rate Provider, as default provider does not support currency conversion for {0} to {1}."
).format(from_currency, to_currency)
)
else:
frappe.throw(
_(
"Setup the Exchange Rate Provider as 'Exchangerate Host' in settings, as default provider does not support currency conversion for {0} to {1}."
).format(from_currency, to_currency)
)
frappe.throw(
_(
"Failed to fetch exchange rate from {0} to {1} on {2}. Please check your internet connection or try again later."
).format(from_currency, to_currency, date)
)

View File

@ -267,24 +267,3 @@ def sales_user_only(fn):
return fn(*args, **kwargs)
return wrapper
def get_exchange_rate(from_currency, to_currency, date=None):
if not date:
date = "latest"
url = f"https://api.frankfurter.app/{date}?from={from_currency}&to={to_currency}"
for _i in range(3):
response = requests.get(url)
if response.status_code == 200:
data = response.json()
rate = data["rates"].get(to_currency)
if rate:
return rate
frappe.log_error(
f"Failed to fetch exchange rate from {from_currency} to {to_currency} on {date}",
title="Exchange Rate Fetch Error",
)
return 1.0 # Default exchange rate if API call fails or no rate found

View File

@ -62,6 +62,7 @@ declare module 'vue' {
CountUpTimer: typeof import('./src/components/CountUpTimer.vue')['default']
CreateDocumentModal: typeof import('./src/components/Modals/CreateDocumentModal.vue')['default']
CRMLogo: typeof import('./src/components/Icons/CRMLogo.vue')['default']
CurrencySettings: typeof import('./src/components/Settings/General/CurrencySettings.vue')['default']
CustomActions: typeof import('./src/components/CustomActions.vue')['default']
DashboardGrid: typeof import('./src/components/Dashboard/DashboardGrid.vue')['default']
DashboardIcon: typeof import('./src/components/Icons/DashboardIcon.vue')['default']

View File

@ -21,7 +21,7 @@
<div class="text-p-sm text-ink-gray-5 truncate">
{{
__(
'Makes "Close Date" and "Deal Value" mandatory for deal value forecasting',
'Makes "Expected Closure Date" and "Expected Deal Value" mandatory for deal value forecasting',
)
}}
</div>
@ -35,37 +35,6 @@
</div>
</div>
<div class="h-px border-t mx-2 border-outline-gray-modals" />
<div
class="flex items-center justify-between gap-8 p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
>
<div class="flex flex-col">
<div class="text-p-base font-medium text-ink-gray-7 truncate">
{{ __('Currency') }}
</div>
<div class="text-p-sm text-ink-gray-5">
{{
__(
'CRM currency for all monetary values. Once set, cannot be edited.',
)
}}
</div>
</div>
<div>
<div v-if="settings.doc.currency" class="text-base text-ink-gray-8">
{{ settings.doc.currency }}
</div>
<Link
v-else
class="form-control flex-1 truncate w-40"
:value="settings.doc.currency"
doctype="Currency"
@change="(v) => setCurrency(v)"
:placeholder="__('Select currency')"
placement="bottom-end"
/>
</div>
</div>
<div class="h-px border-t mx-2 border-outline-gray-modals" />
<template v-for="(setting, i) in settingsList" :key="setting.name">
<li
class="flex items-center justify-between p-3 cursor-pointer hover:bg-surface-menu-bar rounded"
@ -93,27 +62,21 @@
</template>
<script setup>
import Link from '@/components/Controls/Link.vue'
import { getSettings } from '@/stores/settings'
import { globalStore } from '@/stores/global'
import { Switch, toast } from 'frappe-ui'
const emit = defineEmits(['updateStep'])
const { _settings: settings } = getSettings()
const { $dialog } = globalStore()
const settingsList = [
{
<<<<<<< HEAD
=======
name: 'currency-settings',
label: 'Currency & Exchange rate provider',
description:
'Configure the currency and exchange rate provider for your CRM',
},
{
>>>>>>> 4c7a40d8 (fix: fixed labels)
name: 'brand-settings',
label: 'Brand settings',
description: 'Configure your brand name, logo and favicon',
@ -139,31 +102,4 @@ function toggleForecasting(value) {
},
})
}
function setCurrency(value) {
$dialog({
title: __('Set currency'),
message: __(
'Are you sure you want to set the currency as {0}? This cannot be changed later.',
[value],
),
variant: 'solid',
theme: 'blue',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick: (close) => {
settings.doc.currency = value
settings.save.submit(null, {
onSuccess: () => {
toast.success(__('Currency set as {0} successfully', [value]))
close()
},
})
},
},
],
})
}
</script>

View File

@ -4,6 +4,7 @@
<script setup>
import GeneralSettings from './GeneralSettings.vue'
import CurrencySettings from './CurrencySettings.vue'
import BrandSettings from './BrandSettings.vue'
import HomeActions from './HomeActions.vue'
import { ref } from 'vue'
@ -20,6 +21,8 @@ function getComponent(step) {
switch (step) {
case 'general-settings':
return GeneralSettings
case 'currency-settings':
return CurrencySettings
case 'brand-settings':
return BrandSettings
case 'home-actions':

View File

@ -1,4 +1,6 @@
import { getScript } from '@/data/script'
import { globalStore } from '@/stores/global'
import { showSettings, activeSettingsPage } from '@/composables/settings'
import { runSequentially, parseAssignees } from '@/utils'
import { createDocumentResource, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue'
@ -24,7 +26,8 @@ export function useDocument(doctype, docname) {
toast.success(__('Document updated successfully'))
},
onError: (err) => {
let errorMessage = __('Error updating document')
triggerOnError(err)
if (err.exc_type == 'MandatoryError') {
const fieldName = err.messages
.map((msg) => {
@ -32,9 +35,18 @@ export function useDocument(doctype, docname) {
return arr[arr.length - 1].trim()
})
.join(', ')
errorMessage = __('Mandatory field error: {0}', [fieldName])
toast.error(__('Mandatory field error: {0}', [fieldName]))
return
}
toast.error(errorMessage)
err.messages?.forEach((msg) => {
toast.error(msg)
})
if (err.messages?.length === 0) {
toast.error(__('An error occurred while updating the document'))
}
console.error(err)
},
},
@ -76,8 +88,21 @@ export function useDocument(doctype, docname) {
controllersCache[doctype][docname || ''] = {}
const { makeCall } = globalStore()
let helpers = {}
helpers.crm = {
makePhoneCall: makeCall,
openSettings: (page) => {
showSettings.value = true
activeSettingsPage.value = page
},
}
const controllersArray = await setupScript(
documentsCache[doctype][docname || ''],
helpers,
)
if (!controllersArray || controllersArray.length === 0) return
@ -133,6 +158,13 @@ export function useDocument(doctype, docname) {
await trigger(handler)
}
async function triggerOnError() {
const handler = async function () {
await (this.onError?.() || this.on_error?.())
}
await trigger(handler)
}
async function triggerOnRefresh() {
const handler = async function () {
await this.refresh?.()
@ -234,6 +266,7 @@ export function useDocument(doctype, docname) {
triggerOnLoad,
triggerOnBeforeCreate,
triggerOnSave,
triggerOnError,
triggerOnRefresh,
triggerOnChange,
triggerOnRowAdd,

View File

@ -38,7 +38,7 @@ export function getScript(doctype, view = 'Form') {
let scriptDefs = doctypeScripts[doctype]
if (!scriptDefs || Object.keys(scriptDefs).length === 0) return null
const { $dialog, $socket, makeCall } = globalStore()
const { $dialog, $socket } = globalStore()
helpers.createDialog = $dialog
helpers.toast = toast
@ -51,10 +51,6 @@ export function getScript(doctype, view = 'Form') {
throw new Error(message || __('An error occurred'))
}
helpers.crm = {
makePhoneCall: makeCall,
}
return setupMultipleFormControllers(scriptDefs, document, helpers)
}