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):
"""