Enhancement: allow specifying default currency for Monetary custom field (#7381)

This commit is contained in:
shamoon 2024-08-05 17:02:03 -07:00 committed by GitHub
parent 0ee85aae21
commit 15554322dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 168 additions and 29 deletions

View File

@ -525,7 +525,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">347</context>
<context context-type="linenumber">348</context>
</context-group>
</trans-unit>
<trans-unit id="3768927257183755959" datatype="html">
@ -544,7 +544,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">36</context>
<context context-type="linenumber">41</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@ -584,7 +584,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">339</context>
<context context-type="linenumber">340</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -718,7 +718,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">356</context>
<context context-type="linenumber">357</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -1080,7 +1080,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">315</context>
<context context-type="linenumber">316</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@ -1628,7 +1628,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">35</context>
<context context-type="linenumber">40</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@ -3456,6 +3456,27 @@
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="2739003406164860877" datatype="html">
<source>Default Currency</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="7615210738790237590" datatype="html">
<source>3-character currency code</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="607636736207886379" datatype="html">
<source>Use locale</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">33</context>
</context-group>
</trans-unit>
<trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source>
<context-group purpose="location">
@ -5874,14 +5895,14 @@
<source>Content</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">211</context>
<context context-type="linenumber">212</context>
</context-group>
</trans-unit>
<trans-unit id="218403386307979629" datatype="html">
<source>Metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">220</context>
<context context-type="linenumber">221</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@ -5892,119 +5913,119 @@
<source>Date modified</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">227</context>
<context context-type="linenumber">228</context>
</context-group>
</trans-unit>
<trans-unit id="6392918669949841614" datatype="html">
<source>Date added</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">231</context>
<context context-type="linenumber">232</context>
</context-group>
</trans-unit>
<trans-unit id="146828917013192897" datatype="html">
<source>Media filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">236</context>
</context-group>
</trans-unit>
<trans-unit id="4500855521601039868" datatype="html">
<source>Original filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">239</context>
<context context-type="linenumber">240</context>
</context-group>
</trans-unit>
<trans-unit id="7985558498848210210" datatype="html">
<source>Original MD5 checksum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">243</context>
<context context-type="linenumber">244</context>
</context-group>
</trans-unit>
<trans-unit id="5888243105821763422" datatype="html">
<source>Original file size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">247</context>
<context context-type="linenumber">248</context>
</context-group>
</trans-unit>
<trans-unit id="2696647325713149563" datatype="html">
<source>Original mime type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">251</context>
<context context-type="linenumber">252</context>
</context-group>
</trans-unit>
<trans-unit id="342875990758166588" datatype="html">
<source>Archive MD5 checksum</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">257</context>
</context-group>
</trans-unit>
<trans-unit id="6033581412811562084" datatype="html">
<source>Archive file size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">262</context>
<context context-type="linenumber">263</context>
</context-group>
</trans-unit>
<trans-unit id="6992781481378431874" datatype="html">
<source>Original document metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">271</context>
<context context-type="linenumber">272</context>
</context-group>
</trans-unit>
<trans-unit id="2846565152091361585" datatype="html">
<source>Archived document metadata</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">274</context>
<context context-type="linenumber">275</context>
</context-group>
</trans-unit>
<trans-unit id="1295614462098694869" datatype="html">
<source>Preview</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">281</context>
<context context-type="linenumber">282</context>
</context-group>
</trans-unit>
<trans-unit id="7206723502037428235" datatype="html">
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge text-bg-secondary ms-1&quot;&gt;"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">293,296</context>
<context context-type="linenumber">294,297</context>
</context-group>
</trans-unit>
<trans-unit id="186236568870281953" datatype="html">
<source>History</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">304</context>
<context context-type="linenumber">305</context>
</context-group>
</trans-unit>
<trans-unit id="5129524307369213584" datatype="html">
<source>Save &amp; next</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">341</context>
<context context-type="linenumber">342</context>
</context-group>
</trans-unit>
<trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">344</context>
<context context-type="linenumber">345</context>
</context-group>
</trans-unit>
<trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
<context context-type="linenumber">395</context>
<context context-type="linenumber">396</context>
</context-group>
</trans-unit>
<trans-unit id="2218903673684131427" datatype="html">

View File

@ -20,6 +20,12 @@ const customFields: CustomField[] = [
select_options: ['Option 1', 'Option 2', 'Option 3'],
},
},
{
id: 5,
name: 'Field 5',
data_type: CustomFieldDataType.Monetary,
extra_data: { default_currency: 'JPY' },
},
]
const document: Document = {
id: 1,
@ -112,6 +118,18 @@ describe('CustomFieldDisplayComponent', () => {
expect(component.value).toEqual(100)
})
it('should respect explicit default currency', () => {
component['defaultCurrencyCode'] = 'EUR' // mock default locale injection
component.fieldId = 5
component.document = {
id: 1,
title: 'Doc 1',
custom_fields: [{ field: 5, document: 1, created: null, value: '100' }],
}
expect(component.currency).toEqual('JPY')
expect(component.value).toEqual(100)
})
it('should show select value', () => {
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
})

View File

@ -90,7 +90,9 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
)?.value
if (this.value && this.field.data_type === CustomFieldDataType.Monetary) {
this.currency =
this.value.match(/([A-Z]{3})/)?.[0] ?? this.defaultCurrencyCode
this.value.match(/([A-Z]{3})/)?.[0] ??
this.field.extra_data?.default_currency ??
this.defaultCurrencyCode
this.value = parseFloat(this.value.replace(this.currency, ''))
} else if (
this.value?.length &&

View File

@ -28,6 +28,11 @@
}
</div>
}
@case (CustomFieldDataType.Monetary) {
<div class="my-3">
<pngx-input-text i18n-title title="Default Currency" hint="3-character currency code" i18n-hint formControlName="default_currency" placeholder="Use locale" i18n-placeholder autocomplete="off"></pngx-input-text>
</div>
}
}
</div>
</div>

View File

@ -90,6 +90,7 @@ export class CustomFieldEditDialogComponent
data_type: new FormControl(null),
extra_data: new FormGroup({
select_options: new FormArray([new FormControl(null)]),
default_currency: new FormControl(null),
}),
})
}

View File

@ -52,6 +52,11 @@ describe('MonetaryComponent', () => {
expect(component.defaultCurrencyCode).toEqual('BRL')
})
it('should support setting a default currency code', () => {
component.defaultCurrency = 'EUR'
expect(component.defaultCurrencyCode).toEqual('EUR')
})
it('should parse monetary value correctly', () => {
expect(component['parseMonetaryValue']('123.4')).toEqual('123.4')
expect(component['parseMonetaryValue']('123.4', true)).toEqual('123.40')

View File

@ -1,4 +1,4 @@
import { Component, forwardRef, Inject, LOCALE_ID } from '@angular/core'
import { Component, forwardRef, Inject, Input, LOCALE_ID } from '@angular/core'
import { NG_VALUE_ACCESSOR } from '@angular/forms'
import { AbstractInputComponent } from '../abstract-input'
import { getLocaleCurrencyCode } from '@angular/common'
@ -29,11 +29,16 @@ export class MonetaryComponent extends AbstractInputComponent<string> {
defaultCurrencyCode: string
@Input()
set defaultCurrency(currency: string) {
if (currency) this.defaultCurrencyCode = currency
}
constructor(@Inject(LOCALE_ID) currentLocale: string) {
super()
this.currency = this.defaultCurrencyCode =
getLocaleCurrencyCode(currentLocale)
this.defaultCurrency ?? getLocaleCurrencyCode(currentLocale)
}
writeValue(newValue: any): void {

View File

@ -11,7 +11,7 @@
}
</div>
<div class="position-relative" [class.col-md-9]="horizontal">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete" [placeholder]="placeholder">
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}

View File

@ -18,6 +18,9 @@ export class TextComponent extends AbstractInputComponent<string> {
@Input()
autocomplete: string
@Input()
placeholder: string = ''
constructor() {
super()
}

View File

@ -157,6 +157,7 @@
@case (CustomFieldDataType.Monetary) {
<pngx-input-monetary formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
[defaultCurrency]="getCustomFieldFromInstance(fieldInstance)?.extra_data?.default_currency"
[removable]="userIsOwner"
(removed)="removeField(fieldInstance)"
[horizontal]="true"

View File

@ -57,5 +57,6 @@ export interface CustomField extends ObjectWithId {
created?: Date
extra_data?: {
select_options?: string[]
default_currency?: string
}
}

View File

@ -507,6 +507,23 @@ class CustomFieldSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(
{"error": "extra_data.select_options must be a valid list"},
)
elif (
"data_type" in attrs
and attrs["data_type"] == CustomField.FieldDataType.MONETARY
and "extra_data" in attrs
and "default_currency" in attrs["extra_data"]
and attrs["extra_data"]["default_currency"] is not None
and (
not isinstance(attrs["extra_data"]["default_currency"], str)
or (
len(attrs["extra_data"]["default_currency"]) > 0
and len(attrs["extra_data"]["default_currency"]) != 3
)
)
):
raise serializers.ValidationError(
{"error": "extra_data.default_currency must be a 3-character string"},
)
return super().validate(attrs)

View File

@ -137,6 +137,66 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_custom_field_monetary_validation(self):
"""
GIVEN:
- Custom field does not exist
WHEN:
- API request to create custom field with invalid default currency option
- API request to create custom field with valid default currency option
THEN:
- HTTP 400 is returned
- HTTP 201 is returned
"""
# not a string
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "monetary",
"name": "Monetary Field",
"extra_data": {
"default_currency": 123,
},
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
# not a 3-letter currency code
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "monetary",
"name": "Monetary Field",
"extra_data": {
"default_currency": "EU",
},
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
# valid currency code
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "monetary",
"name": "Monetary Field",
"extra_data": {
"default_currency": "EUR",
},
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
def test_create_custom_field_instance(self):
"""
GIVEN: