]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: better monetary field with currency code (#5858)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 27 Feb 2024 16:26:06 +0000 (08:26 -0800)
committerGitHub <noreply@github.com>
Tue, 27 Feb 2024 16:26:06 +0000 (16:26 +0000)
13 files changed:
docs/usage.md
src-ui/messages.xlf
src-ui/src/app/app.module.ts
src-ui/src/app/components/common/input/monetary/monetary.component.html [new file with mode: 0644]
src-ui/src/app/components/common/input/monetary/monetary.component.scss [new file with mode: 0644]
src-ui/src/app/components/common/input/monetary/monetary.component.spec.ts [new file with mode: 0644]
src-ui/src/app/components/common/input/monetary/monetary.component.ts [new file with mode: 0644]
src-ui/src/app/components/document-detail/document-detail.component.html
src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py [new file with mode: 0644]
src/documents/models.py
src/documents/serialisers.py
src/documents/tests/test_api_custom_fields.py
src/locale/en_US/LC_MESSAGES/django.po

index b396168449a1e1c78bdfc52d5f3faf7822956413..b1b8c4cd1802231d196a3f6127b3da162a287e6f 100644 (file)
@@ -407,7 +407,7 @@ The following custom field types are supported:
 - `URL`: a valid url
 - `Integer`: integer number e.g. 12
 - `Number`: float number e.g. 12.3456
-- `Monetary`: float number with exactly two decimals, e.g. 12.30
+- `Monetary`: [ISO 4217 currency code](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) and a number with exactly two decimals, e.g. USD12.30
 - `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
 
 ## Share Links
index b102e956728be9ea9b019fd2c855135af1f934b0..ca23684f8b2855e3e07d2e8f7cb64a48cae327f5 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">315</context>
+          <context context-type="linenumber">313</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3768927257183755959" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
-          <context context-type="linenumber">307</context>
+          <context context-type="linenumber">305</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">324</context>
+          <context context-type="linenumber">322</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">283</context>
+          <context context-type="linenumber">281</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
           <context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context>
           <context context-type="linenumber">21</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/input/monetary/monetary.component.html</context>
+          <context context-type="linenumber">9</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context>
           <context context-type="linenumber">9</context>
         <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">190</context>
+          <context context-type="linenumber">188</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">199</context>
+          <context context-type="linenumber">197</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">206</context>
+          <context context-type="linenumber">204</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">210</context>
+          <context context-type="linenumber">208</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">214</context>
+          <context context-type="linenumber">212</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">218</context>
+          <context context-type="linenumber">216</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">222</context>
+          <context context-type="linenumber">220</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">226</context>
+          <context context-type="linenumber">224</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">230</context>
+          <context context-type="linenumber">228</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">235</context>
+          <context context-type="linenumber">233</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">241</context>
+          <context context-type="linenumber">239</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">250</context>
+          <context context-type="linenumber">248</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">253</context>
+          <context context-type="linenumber">251</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">260</context>
+          <context context-type="linenumber">258</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">272,275</context>
+          <context context-type="linenumber">270,273</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">309</context>
+          <context context-type="linenumber">307</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">312</context>
+          <context context-type="linenumber">310</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">363</context>
+          <context context-type="linenumber">361</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2218903673684131427" datatype="html">
index f23dcc2c388b48dd412ae44a59867cb052d34207..69213846f2516374667e2b865efe92ff2e7e7e62 100644 (file)
@@ -113,6 +113,7 @@ import { ConfigComponent } from './components/admin/config/config.component'
 import { FileComponent } from './components/common/input/file/file.component'
 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
+import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
 import {
   archive,
   arrowCounterclockwise,
@@ -443,6 +444,7 @@ function initializeApp(settings: SettingsService) {
     ConfigComponent,
     FileComponent,
     ConfirmButtonComponent,
+    MonetaryComponent,
   ],
   imports: [
     BrowserModule,
diff --git a/src-ui/src/app/components/common/input/monetary/monetary.component.html b/src-ui/src/app/components/common/input/monetary/monetary.component.html
new file mode 100644 (file)
index 0000000..ff3d7b9
--- /dev/null
@@ -0,0 +1,27 @@
+<div class="mb-3" [class.pb-3]="error">
+  <div class="row">
+    <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
+      @if (title) {
+        <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
+      }
+      @if (removable) {
+        <button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
+          <i-bs name="x"></i-bs>&nbsp;<ng-container i18n>Remove</ng-container>
+        </button>
+      }
+    </div>
+    <div class="position-relative" [class.col-md-9]="horizontal">
+      <div class="input-group" [class.is-invalid]="error">
+        <span class="input-group-text fw-bold bg-light">{{monetaryValue | currency: currencyCode }}</span>
+        <input #currencyField class="form-control text-muted mw-60" tabindex="0" [(ngModel)]="currencyCode" maxlength="3" [class.is-invalid]="error" (change)="onChange(value)" [disabled]="disabled">
+        <input #inputField type="number" tabindex="0" class="form-control text-muted" step=".01" [id]="inputId" [(ngModel)]="monetaryValue" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
+      </div>
+      <div class="invalid-feedback position-absolute top-100">
+        {{error}}
+      </div>
+      @if (hint) {
+        <small class="form-text text-muted">{{hint}}</small>
+      }
+    </div>
+  </div>
+</div>
diff --git a/src-ui/src/app/components/common/input/monetary/monetary.component.scss b/src-ui/src/app/components/common/input/monetary/monetary.component.scss
new file mode 100644 (file)
index 0000000..f4fe1fa
--- /dev/null
@@ -0,0 +1,11 @@
+.input-group-text {
+    font-size: inherit;
+}
+
+.text-muted:focus-within {
+    color: var(--bs-body-color) !important;
+}
+
+.mw-60 {
+    max-width: 60px;
+}
diff --git a/src-ui/src/app/components/common/input/monetary/monetary.component.spec.ts b/src-ui/src/app/components/common/input/monetary/monetary.component.spec.ts
new file mode 100644 (file)
index 0000000..0a8608a
--- /dev/null
@@ -0,0 +1,59 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+  FormsModule,
+  NG_VALUE_ACCESSOR,
+  ReactiveFormsModule,
+} from '@angular/forms'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { CurrencyPipe } from '@angular/common'
+import { MonetaryComponent } from './monetary.component'
+
+describe('MonetaryComponent', () => {
+  let component: MonetaryComponent
+  let fixture: ComponentFixture<MonetaryComponent>
+  let input: HTMLInputElement
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [MonetaryComponent],
+      providers: [CurrencyPipe],
+      imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
+    }).compileComponents()
+
+    fixture = TestBed.createComponent(MonetaryComponent)
+    fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+    component = fixture.componentInstance
+    fixture.detectChanges()
+    input = component.inputField.nativeElement
+  })
+
+  it('should set the currency code correctly', () => {
+    expect(component.currencyCode).toEqual('USD') // default
+    component.currencyCode = 'EUR'
+    expect(component.currencyCode).toEqual('EUR')
+
+    component.value = 'G123.4'
+    jest
+      .spyOn(document, 'activeElement', 'get')
+      .mockReturnValue(component.currencyField.nativeElement)
+    expect(component.currencyCode).toEqual('G')
+  })
+
+  it('should parse monetary value only when out of focus', () => {
+    component.monetaryValue = 10.5
+    jest.spyOn(document, 'activeElement', 'get').mockReturnValue(null)
+    expect(component.monetaryValue).toEqual('10.50')
+
+    component.value = 'GBP123.4'
+    jest
+      .spyOn(document, 'activeElement', 'get')
+      .mockReturnValue(component.inputField.nativeElement)
+    expect(component.monetaryValue).toEqual('123.4')
+  })
+
+  it('should report value including currency code and monetary value', () => {
+    component.currencyCode = 'EUR'
+    component.monetaryValue = 10.5
+    expect(component.value).toEqual('EUR10.50')
+  })
+})
diff --git a/src-ui/src/app/components/common/input/monetary/monetary.component.ts b/src-ui/src/app/components/common/input/monetary/monetary.component.ts
new file mode 100644 (file)
index 0000000..a7957b4
--- /dev/null
@@ -0,0 +1,59 @@
+import {
+  Component,
+  DEFAULT_CURRENCY_CODE,
+  ElementRef,
+  forwardRef,
+  Inject,
+  ViewChild,
+} from '@angular/core'
+import { NG_VALUE_ACCESSOR } from '@angular/forms'
+import { AbstractInputComponent } from '../abstract-input'
+
+@Component({
+  providers: [
+    {
+      provide: NG_VALUE_ACCESSOR,
+      useExisting: forwardRef(() => MonetaryComponent),
+      multi: true,
+    },
+  ],
+  selector: 'pngx-input-monetary',
+  templateUrl: './monetary.component.html',
+  styleUrls: ['./monetary.component.scss'],
+})
+export class MonetaryComponent extends AbstractInputComponent<string> {
+  @ViewChild('currencyField')
+  currencyField: ElementRef
+
+  constructor(
+    @Inject(DEFAULT_CURRENCY_CODE) public defaultCurrencyCode: string
+  ) {
+    super()
+  }
+
+  get currencyCode(): string {
+    const focused = document.activeElement === this.currencyField?.nativeElement
+    if (focused && this.value) return this.value.match(/^([A-Z]{0,3})/)?.[0]
+    return (
+      this.value
+        ?.toString()
+        .toUpperCase()
+        .match(/^([A-Z]{1,3})/)?.[0] ?? this.defaultCurrencyCode
+    )
+  }
+
+  set currencyCode(value: string) {
+    this.value = value + this.monetaryValue?.toString()
+  }
+
+  get monetaryValue(): string {
+    if (!this.value) return null
+    const focused = document.activeElement === this.inputField?.nativeElement
+    const val = parseFloat(this.value.toString().replace(/[^0-9.,]+/g, ''))
+    return focused ? val.toString() : val.toFixed(2)
+  }
+
+  set monetaryValue(value: number) {
+    this.value = this.currencyCode + value.toFixed(2)
+  }
+}
index a3890f961d78b220d8252438ec371f9fbefa98ea..79cc58f534788030d2cf76c3200b69ab6d16b3ac 100644 (file)
                       [error]="getCustomFieldError(i)"></pngx-input-number>
                     }
                     @case (CustomFieldDataType.Monetary) {
-                      <pngx-input-number formControlName="value"
+                      <pngx-input-monetary formControlName="value"
                       [title]="getCustomFieldFromInstance(fieldInstance)?.name"
                       [removable]="userIsOwner"
                       (removed)="removeField(fieldInstance)"
                       [horizontal]="true"
-                      [showAdd]="false"
-                      [step]=".01"
-                      [error]="getCustomFieldError(i)"></pngx-input-number>
+                      [error]="getCustomFieldError(i)"></pngx-input-monetary>
                     }
                     @case (CustomFieldDataType.Boolean) {
                       <pngx-input-check formControlName="value"
diff --git a/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py b/src/documents/migrations/1045_alter_customfieldinstance_value_monetary.py
new file mode 100644 (file)
index 0000000..9689bbd
--- /dev/null
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.10 on 2024-02-22 03:52
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("documents", "1044_workflow_workflowaction_workflowtrigger_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="customfieldinstance",
+            name="value_monetary",
+            field=models.CharField(max_length=128, null=True),
+        ),
+    ]
index f2e122abc996b3a2008a5baee20f9f6d34be8fbf..6dc24c801ac1b1c4796a9c40c934fbf3ceb65b5f 100644 (file)
@@ -838,7 +838,7 @@ class CustomFieldInstance(models.Model):
 
     value_float = models.FloatField(null=True)
 
-    value_monetary = models.DecimalField(null=True, decimal_places=2, max_digits=12)
+    value_monetary = models.CharField(null=True, max_length=128)
 
     value_document_ids = models.JSONField(null=True)
 
index 8918918468c800cd4b2ab794015869caa2b25329..f4f92a1c91fc3d7ebd2ee939dd504df3a91e19d4 100644 (file)
@@ -12,6 +12,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.core.validators import DecimalValidator
 from django.core.validators import MaxLengthValidator
+from django.core.validators import RegexValidator
 from django.core.validators import integer_validator
 from django.utils import timezone
 from django.utils.crypto import get_random_string
@@ -528,9 +529,17 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
             elif field.data_type == CustomField.FieldDataType.INT:
                 integer_validator(data["value"])
             elif field.data_type == CustomField.FieldDataType.MONETARY:
-                DecimalValidator(max_digits=12, decimal_places=2)(
-                    Decimal(str(data["value"])),
-                )
+                try:
+                    # First try to validate as a number from legacy format
+                    DecimalValidator(max_digits=12, decimal_places=2)(
+                        Decimal(str(data["value"])),
+                    )
+                except Exception:
+                    # If that fails, try to validate as a monetary string
+                    RegexValidator(
+                        regex=r"^[A-Z][A-Z][A-Z]\d+(\.\d{2,2})$",
+                        message="Must be a two-decimal number with optional currency code e.g. GBP123.45",
+                    )(data["value"])
             elif field.data_type == CustomField.FieldDataType.STRING:
                 MaxLengthValidator(limit_value=128)(data["value"])
 
index 33124a48c825a83cf40e8f70aeaeecd0c4962076..2885d9071706afb4e32a0fe5ae22c9592f83877d 100644 (file)
@@ -10,7 +10,7 @@ from documents.models import Document
 from documents.tests.utils import DirectoriesMixin
 
 
-class TestCustomField(DirectoriesMixin, APITestCase):
+class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
     ENDPOINT = "/api/custom_fields/"
 
     def setUp(self):
@@ -127,6 +127,10 @@ class TestCustomField(DirectoriesMixin, APITestCase):
             name="Test Custom Field Monetary",
             data_type=CustomField.FieldDataType.MONETARY,
         )
+        custom_field_monetary2 = CustomField.objects.create(
+            name="Test Custom Field Monetary 2",
+            data_type=CustomField.FieldDataType.MONETARY,
+        )
         custom_field_documentlink = CustomField.objects.create(
             name="Test Custom Field Doc Link",
             data_type=CustomField.FieldDataType.DOCUMENTLINK,
@@ -164,7 +168,11 @@ class TestCustomField(DirectoriesMixin, APITestCase):
                     },
                     {
                         "field": custom_field_monetary.id,
-                        "value": 11.10,
+                        "value": "EUR11.10",
+                    },
+                    {
+                        "field": custom_field_monetary2.id,
+                        "value": 11.10,  # Legacy format
                     },
                     {
                         "field": custom_field_documentlink.id,
@@ -188,13 +196,14 @@ class TestCustomField(DirectoriesMixin, APITestCase):
                 {"field": custom_field_boolean.id, "value": True},
                 {"field": custom_field_url.id, "value": "https://example.com"},
                 {"field": custom_field_float.id, "value": 12.3456},
-                {"field": custom_field_monetary.id, "value": 11.10},
+                {"field": custom_field_monetary.id, "value": "EUR11.10"},
+                {"field": custom_field_monetary2.id, "value": "11.1"},
                 {"field": custom_field_documentlink.id, "value": [doc2.id]},
             ],
         )
 
         doc.refresh_from_db()
-        self.assertEqual(len(doc.custom_fields.all()), 8)
+        self.assertEqual(len(doc.custom_fields.all()), 9)
 
     def test_change_custom_field_instance_value(self):
         """
@@ -458,7 +467,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
         GIVEN:
             - Document & custom field exist
         WHEN:
-            - API request to set a field value to something not a valid monetary decimal
+            - API request to set a field value to something not a valid monetary decimal (legacy) or not a new monetary format e.g. USD12.34
         THEN:
             - HTTP 400 is returned
             - No field instance is created or attached to the document
@@ -488,6 +497,54 @@ class TestCustomField(DirectoriesMixin, APITestCase):
             format="json",
         )
 
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+        resp = self.client.patch(
+            f"/api/documents/{doc.id}/",
+            data={
+                "custom_fields": [
+                    {
+                        "field": custom_field_money.id,
+                        # Too few places past decimal
+                        "value": "GBP12.1",
+                    },
+                ],
+            },
+            format="json",
+        )
+
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+        resp = self.client.patch(
+            f"/api/documents/{doc.id}/",
+            data={
+                "custom_fields": [
+                    {
+                        "field": custom_field_money.id,
+                        # Too many places past decimal
+                        "value": "GBP12.123",
+                    },
+                ],
+            },
+            format="json",
+        )
+
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+        resp = self.client.patch(
+            f"/api/documents/{doc.id}/",
+            data={
+                "custom_fields": [
+                    {
+                        "field": custom_field_money.id,
+                        # Not a 3-letter currency code
+                        "value": "G12.12",
+                    },
+                ],
+            },
+            format="json",
+        )
+
         self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(CustomFieldInstance.objects.count(), 0)
         self.assertEqual(len(doc.custom_fields.all()), 0)
index 109940a59e2ce35cb790373272458028c416912a..0689b523ca3093fd2bdf88340a292767b7d23b46 100644 (file)
@@ -777,12 +777,12 @@ msgstr ""
 msgid "Invalid color."
 msgstr ""
 
-#: documents/serialisers.py:1061
+#: documents/serialisers.py:1073
 #, python-format
 msgid "File type %(type)s not supported"
 msgstr ""
 
-#: documents/serialisers.py:1164
+#: documents/serialisers.py:1176
 msgid "Invalid variable detected."
 msgstr ""