Enhancement: better monetary field with currency code (#5858)

This commit is contained in:
shamoon 2024-02-27 08:26:06 -08:00 committed by GitHub
parent 84721b001f
commit bf11dc8d1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 283 additions and 38 deletions

View File

@ -407,7 +407,7 @@ The following custom field types are supported:
- `URL`: a valid url - `URL`: a valid url
- `Integer`: integer number e.g. 12 - `Integer`: integer number e.g. 12
- `Number`: float number e.g. 12.3456 - `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 - `Document Link`: reference(s) to other document(s) displayed as links, automatically creates a symmetrical link in reverse
## Share Links ## Share Links

View File

@ -447,7 +447,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3768927257183755959" datatype="html"> <trans-unit id="3768927257183755959" datatype="html">
@ -506,7 +506,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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>
<context-group purpose="location"> <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 context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
@ -632,7 +632,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -962,7 +962,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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>
<context-group purpose="location"> <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/document-list/bulk-editor/bulk-editor.component.html</context>
@ -3815,6 +3815,10 @@
<context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context> <context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context>
<context context-type="linenumber">21</context> <context context-type="linenumber">21</context>
</context-group> </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-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context> <context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context>
<context context-type="linenumber">9</context> <context context-type="linenumber">9</context>
@ -4824,14 +4828,14 @@
<source>Content</source> <source>Content</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="218403386307979629" datatype="html"> <trans-unit id="218403386307979629" datatype="html">
<source>Metadata</source> <source>Metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts</context>
@ -4842,112 +4846,112 @@
<source>Date modified</source> <source>Date modified</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6392918669949841614" datatype="html"> <trans-unit id="6392918669949841614" datatype="html">
<source>Date added</source> <source>Date added</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="146828917013192897" datatype="html"> <trans-unit id="146828917013192897" datatype="html">
<source>Media filename</source> <source>Media filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4500855521601039868" datatype="html"> <trans-unit id="4500855521601039868" datatype="html">
<source>Original filename</source> <source>Original filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7985558498848210210" datatype="html"> <trans-unit id="7985558498848210210" datatype="html">
<source>Original MD5 checksum</source> <source>Original MD5 checksum</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5888243105821763422" datatype="html"> <trans-unit id="5888243105821763422" datatype="html">
<source>Original file size</source> <source>Original file size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2696647325713149563" datatype="html"> <trans-unit id="2696647325713149563" datatype="html">
<source>Original mime type</source> <source>Original mime type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="342875990758166588" datatype="html"> <trans-unit id="342875990758166588" datatype="html">
<source>Archive MD5 checksum</source> <source>Archive MD5 checksum</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6033581412811562084" datatype="html"> <trans-unit id="6033581412811562084" datatype="html">
<source>Archive file size</source> <source>Archive file size</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6992781481378431874" datatype="html"> <trans-unit id="6992781481378431874" datatype="html">
<source>Original document metadata</source> <source>Original document metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2846565152091361585" datatype="html"> <trans-unit id="2846565152091361585" datatype="html">
<source>Archived document metadata</source> <source>Archived document metadata</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1295614462098694869" datatype="html"> <trans-unit id="1295614462098694869" datatype="html">
<source>Preview</source> <source>Preview</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7206723502037428235" datatype="html"> <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> <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-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5129524307369213584" datatype="html"> <trans-unit id="5129524307369213584" datatype="html">
<source>Save &amp; next</source> <source>Save &amp; next</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4910102545766233758" datatype="html"> <trans-unit id="4910102545766233758" datatype="html">
<source>Save &amp; close</source> <source>Save &amp; close</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8191371354890763172" datatype="html"> <trans-unit id="8191371354890763172" datatype="html">
<source>Enter Password</source> <source>Enter Password</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context> <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> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2218903673684131427" datatype="html"> <trans-unit id="2218903673684131427" datatype="html">

View File

@ -113,6 +113,7 @@ import { ConfigComponent } from './components/admin/config/config.component'
import { FileComponent } from './components/common/input/file/file.component' import { FileComponent } from './components/common/input/file/file.component'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component' import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
import { import {
archive, archive,
arrowCounterclockwise, arrowCounterclockwise,
@ -443,6 +444,7 @@ function initializeApp(settings: SettingsService) {
ConfigComponent, ConfigComponent,
FileComponent, FileComponent,
ConfirmButtonComponent, ConfirmButtonComponent,
MonetaryComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -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>

View File

@ -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;
}

View File

@ -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')
})
})

View File

@ -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)
}
}

View File

@ -142,14 +142,12 @@
[error]="getCustomFieldError(i)"></pngx-input-number> [error]="getCustomFieldError(i)"></pngx-input-number>
} }
@case (CustomFieldDataType.Monetary) { @case (CustomFieldDataType.Monetary) {
<pngx-input-number formControlName="value" <pngx-input-monetary formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name" [title]="getCustomFieldFromInstance(fieldInstance)?.name"
[removable]="userIsOwner" [removable]="userIsOwner"
(removed)="removeField(fieldInstance)" (removed)="removeField(fieldInstance)"
[horizontal]="true" [horizontal]="true"
[showAdd]="false" [error]="getCustomFieldError(i)"></pngx-input-monetary>
[step]=".01"
[error]="getCustomFieldError(i)"></pngx-input-number>
} }
@case (CustomFieldDataType.Boolean) { @case (CustomFieldDataType.Boolean) {
<pngx-input-check formControlName="value" <pngx-input-check formControlName="value"

View File

@ -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),
),
]

View File

@ -838,7 +838,7 @@ class CustomFieldInstance(models.Model):
value_float = models.FloatField(null=True) 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) value_document_ids = models.JSONField(null=True)

View File

@ -12,6 +12,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.validators import DecimalValidator from django.core.validators import DecimalValidator
from django.core.validators import MaxLengthValidator from django.core.validators import MaxLengthValidator
from django.core.validators import RegexValidator
from django.core.validators import integer_validator from django.core.validators import integer_validator
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
@ -528,9 +529,17 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
elif field.data_type == CustomField.FieldDataType.INT: elif field.data_type == CustomField.FieldDataType.INT:
integer_validator(data["value"]) integer_validator(data["value"])
elif field.data_type == CustomField.FieldDataType.MONETARY: elif field.data_type == CustomField.FieldDataType.MONETARY:
DecimalValidator(max_digits=12, decimal_places=2)( try:
Decimal(str(data["value"])), # 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: elif field.data_type == CustomField.FieldDataType.STRING:
MaxLengthValidator(limit_value=128)(data["value"]) MaxLengthValidator(limit_value=128)(data["value"])

View File

@ -10,7 +10,7 @@ from documents.models import Document
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
class TestCustomField(DirectoriesMixin, APITestCase): class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/custom_fields/" ENDPOINT = "/api/custom_fields/"
def setUp(self): def setUp(self):
@ -127,6 +127,10 @@ class TestCustomField(DirectoriesMixin, APITestCase):
name="Test Custom Field Monetary", name="Test Custom Field Monetary",
data_type=CustomField.FieldDataType.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( custom_field_documentlink = CustomField.objects.create(
name="Test Custom Field Doc Link", name="Test Custom Field Doc Link",
data_type=CustomField.FieldDataType.DOCUMENTLINK, data_type=CustomField.FieldDataType.DOCUMENTLINK,
@ -164,7 +168,11 @@ class TestCustomField(DirectoriesMixin, APITestCase):
}, },
{ {
"field": custom_field_monetary.id, "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, "field": custom_field_documentlink.id,
@ -188,13 +196,14 @@ class TestCustomField(DirectoriesMixin, APITestCase):
{"field": custom_field_boolean.id, "value": True}, {"field": custom_field_boolean.id, "value": True},
{"field": custom_field_url.id, "value": "https://example.com"}, {"field": custom_field_url.id, "value": "https://example.com"},
{"field": custom_field_float.id, "value": 12.3456}, {"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]}, {"field": custom_field_documentlink.id, "value": [doc2.id]},
], ],
) )
doc.refresh_from_db() 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): def test_change_custom_field_instance_value(self):
""" """
@ -458,7 +467,7 @@ class TestCustomField(DirectoriesMixin, APITestCase):
GIVEN: GIVEN:
- Document & custom field exist - Document & custom field exist
WHEN: 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: THEN:
- HTTP 400 is returned - HTTP 400 is returned
- No field instance is created or attached to the document - No field instance is created or attached to the document
@ -488,6 +497,54 @@ class TestCustomField(DirectoriesMixin, APITestCase):
format="json", 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(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(CustomFieldInstance.objects.count(), 0) self.assertEqual(CustomFieldInstance.objects.count(), 0)
self.assertEqual(len(doc.custom_fields.all()), 0) self.assertEqual(len(doc.custom_fields.all()), 0)

View File

@ -777,12 +777,12 @@ msgstr ""
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:1061 #: documents/serialisers.py:1073
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:1164 #: documents/serialisers.py:1176
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""