diff --git a/src-ui/src/app/components/common/input/number/number.component.html b/src-ui/src/app/components/common/input/number/number.component.html index 0628187f1..39aa6d4f5 100644 --- a/src-ui/src/app/components/common/input/number/number.component.html +++ b/src-ui/src/app/components/common/input/number/number.component.html @@ -10,7 +10,7 @@
- +
diff --git a/src-ui/src/app/components/common/input/number/number.component.spec.ts b/src-ui/src/app/components/common/input/number/number.component.spec.ts index dfe1673db..b6a281e6f 100644 --- a/src-ui/src/app/components/common/input/number/number.component.spec.ts +++ b/src-ui/src/app/components/common/input/number/number.component.spec.ts @@ -46,4 +46,18 @@ describe('NumberComponent', () => { component.nextAsn() expect(component.value).toEqual(1002) }) + + it('should support float & monetary values', () => { + component.writeValue(11.13) + expect(component.value).toEqual(11) + component.step = 0.01 + component.writeValue(11.1) + expect(component.value).toEqual('11.10') + component.step = 0.1 + component.writeValue(12.3456) + expect(component.value).toEqual(12.3456) + // float (step = .1) doesnt force 2 decimals + component.writeValue(11.1) + expect(component.value).toEqual(11.1) + }) }) diff --git a/src-ui/src/app/components/common/input/number/number.component.ts b/src-ui/src/app/components/common/input/number/number.component.ts index 682cd8036..0b113a4de 100644 --- a/src-ui/src/app/components/common/input/number/number.component.ts +++ b/src-ui/src/app/components/common/input/number/number.component.ts @@ -19,6 +19,9 @@ export class NumberComponent extends AbstractInputComponent { @Input() showAdd: boolean = true + @Input() + step: number = 1 + constructor(private documentService: DocumentService) { super() } @@ -32,4 +35,10 @@ export class NumberComponent extends AbstractInputComponent { this.onChange(this.value) }) } + + writeValue(newValue: any): void { + if (this.step === 1) newValue = parseInt(newValue, 10) + if (this.step === 0.01) newValue = parseFloat(newValue).toFixed(2) + super.writeValue(newValue) + } } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index adfa90887..b3465e28a 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -117,6 +117,8 @@ + +
diff --git a/src-ui/src/app/data/paperless-custom-field.ts b/src-ui/src/app/data/paperless-custom-field.ts index 87ac5e241..6890cbf90 100644 --- a/src-ui/src/app/data/paperless-custom-field.ts +++ b/src-ui/src/app/data/paperless-custom-field.ts @@ -6,6 +6,8 @@ export enum PaperlessCustomFieldDataType { Date = 'date', Boolean = 'boolean', Integer = 'integer', + Float = 'float', + Monetary = 'monetary', } export const DATA_TYPE_LABELS = [ @@ -19,8 +21,16 @@ export const DATA_TYPE_LABELS = [ }, { id: PaperlessCustomFieldDataType.Integer, + name: $localize`Integer`, + }, + { + id: PaperlessCustomFieldDataType.Float, name: $localize`Number`, }, + { + id: PaperlessCustomFieldDataType.Monetary, + name: $localize`Monetary`, + }, { id: PaperlessCustomFieldDataType.String, name: $localize`String`, diff --git a/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py b/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py index 2aab0ea8e..eb644b50f 100644 --- a/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py +++ b/src/documents/migrations/1040_customfield_customfieldinstance_and_more.py @@ -43,6 +43,8 @@ class Migration(migrations.Migration): ("date", "Date"), ("boolean", "Boolean"), ("integer", "Integer"), + ("float", "Float"), + ("monetary", "Monetary"), ], editable=False, max_length=50, @@ -82,6 +84,11 @@ class Migration(migrations.Migration): ("value_url", models.URLField(null=True)), ("value_date", models.DateField(null=True)), ("value_int", models.IntegerField(null=True)), + ("value_float", models.FloatField(null=True)), + ( + "value_monetary", + models.DecimalField(decimal_places=2, max_digits=12, null=True), + ), ( "document", models.ForeignKey( diff --git a/src/documents/models.py b/src/documents/models.py index 743f2ec1c..04a3230e6 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -888,6 +888,8 @@ class CustomField(models.Model): DATE = ("date", _("Date")) BOOL = ("boolean"), _("Boolean") INT = ("integer", _("Integer")) + FLOAT = ("float", _("Float")) + MONETARY = ("monetary", _("Monetary")) created = models.DateTimeField( _("created"), @@ -962,6 +964,10 @@ class CustomFieldInstance(models.Model): value_int = models.IntegerField(null=True) + value_float = models.FloatField(null=True) + + value_monetary = models.DecimalField(null=True, decimal_places=2, max_digits=12) + class Meta: ordering = ("created",) verbose_name = _("custom field instance") @@ -992,6 +998,10 @@ class CustomFieldInstance(models.Model): return self.value_bool elif self.field.data_type == CustomField.FieldDataType.INT: return self.value_int + elif self.field.data_type == CustomField.FieldDataType.FLOAT: + return self.value_float + elif self.field.data_type == CustomField.FieldDataType.MONETARY: + return self.value_monetary raise NotImplementedError(self.field.data_type) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 49a429e8f..c91b97430 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -437,6 +437,8 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer): CustomField.FieldDataType.DATE: "value_date", CustomField.FieldDataType.BOOL: "value_bool", CustomField.FieldDataType.INT: "value_int", + CustomField.FieldDataType.FLOAT: "value_float", + CustomField.FieldDataType.MONETARY: "value_monetary", } # An instance is attached to a document document: Document = validated_data["document"] diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py index 7d21f1ffe..274b3ae9c 100644 --- a/src/documents/tests/test_api_custom_fields.py +++ b/src/documents/tests/test_api_custom_fields.py @@ -34,6 +34,7 @@ class TestCustomField(DirectoriesMixin, APITestCase): ("date", "Invoiced Date"), ("integer", "Invoice #"), ("boolean", "Is Active"), + ("float", "Total Paid"), ]: resp = self.client.post( self.ENDPOINT, @@ -87,6 +88,14 @@ class TestCustomField(DirectoriesMixin, APITestCase): name="Test Custom Field Url", data_type=CustomField.FieldDataType.URL, ) + custom_field_float = CustomField.objects.create( + name="Test Custom Field Float", + data_type=CustomField.FieldDataType.FLOAT, + ) + custom_field_monetary = CustomField.objects.create( + name="Test Custom Field Monetary", + data_type=CustomField.FieldDataType.MONETARY, + ) date_value = date.today() @@ -114,6 +123,14 @@ class TestCustomField(DirectoriesMixin, APITestCase): "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, + }, ], }, format="json", @@ -131,11 +148,13 @@ class TestCustomField(DirectoriesMixin, APITestCase): {"field": custom_field_int.id, "value": 3}, {"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}, ], ) doc.refresh_from_db() - self.assertEqual(len(doc.custom_fields.all()), 5) + self.assertEqual(len(doc.custom_fields.all()), 7) def test_change_custom_field_instance_value(self): """