]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: allow specifying default currency for Monetary custom field (#7381)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 6 Aug 2024 00:02:03 +0000 (17:02 -0700)
committerGitHub <noreply@github.com>
Tue, 6 Aug 2024 00:02:03 +0000 (17:02 -0700)
13 files changed:
src-ui/messages.xlf
src-ui/src/app/components/common/custom-field-display/custom-field-display.component.spec.ts
src-ui/src/app/components/common/custom-field-display/custom-field-display.component.ts
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts
src-ui/src/app/components/common/input/monetary/monetary.component.spec.ts
src-ui/src/app/components/common/input/monetary/monetary.component.ts
src-ui/src/app/components/common/input/text/text.component.html
src-ui/src/app/components/common/input/text/text.component.ts
src-ui/src/app/components/document-detail/document-detail.component.html
src-ui/src/app/data/custom-field.ts
src/documents/serialisers.py
src/documents/tests/test_api_custom_fields.py

index 34096d3a276335ef5c0e63961721eecadb77986d..8a5a8db6098ed91ec71a4a02d69a7e7e5f35521f 100644 (file)
         </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">
         </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>
         </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>
         </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>
         </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>
         </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>
           <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">
         <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>
         <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">
index 8706cfac1fe8f9bc56cad464b279603311fdd257..ea60034e493dd59d99c63bc54fd61bc472270a6a 100644 (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')
   })
index 7c97c660acf05ef42c80990dafe55259a97a26df..f565c95e2eb685df1e93c24abd56d1801cefad57 100644 (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 &&
index bc893d53ab57a6a2993cb3a9654bb9ecd61983e4..953f66659880f707c615090e65b8d40afd0dabfb 100644 (file)
             }
           </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>
index db6c7e2181752688d2f6cb1aa60430b7da670e10..48e5e53bbd8a856c127921ccbdae9c02ee7cd9da 100644 (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),
       }),
     })
   }
index e0872be448c407edfb40c3757f6a7b527fd56eb4..7a06d842e83ed7f7d8c6eeb5f83a71c282049ad0 100644 (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')
index 256dc8c18ffc2933ed86487c13c15c634876bd03..c90042860657d0b1b79cb7832c9ef26eb0d625a6 100644 (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 {
index b46ab23b7a662d70b43be6832814d2ef68fee10b..29e5698ad06ed716b2d4e39cc5ff832013ac111e 100644 (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>
         }
index a546e2e39684f5f232160e51e688ded79d11da12..594f5f1d6e5b078d6b04f7f236f151a4de66ec1f 100644 (file)
@@ -18,6 +18,9 @@ export class TextComponent extends AbstractInputComponent<string> {
   @Input()
   autocomplete: string
 
+  @Input()
+  placeholder: string = ''
+
   constructor() {
     super()
   }
index 84ab680b147c6c48059659c455feb765d49cfb2e..124f4811d5fac76b334858b977ed08a40c1d72a2 100644 (file)
                     @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"
index a60c5ac2acb2b8a56b2d7d30aace46f5ff04e3a9..7e52d07856038777a89875a60417f288d1f90b2a 100644 (file)
@@ -57,5 +57,6 @@ export interface CustomField extends ObjectWithId {
   created?: Date
   extra_data?: {
     select_options?: string[]
+    default_currency?: string
   }
 }
index 38f6cc4f9d953be4fe71648c4c2db31e57e1dd4f..0c0813aa4fae9dfe8f52ac72155dc83e25ef1468 100644 (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)
 
 
index edebf7f3cba077ab38e89f50236f2205a6b564ba..6ffe1468189c6e21b78ee6e70d107eb400440d8f 100644 (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: