From 667a568f545088b6cfe650ba643c25dd34e29d9c Mon Sep 17 00:00:00 2001 From: Jack Eccles Date: Wed, 24 Sep 2025 20:53:26 -0400 Subject: [PATCH 1/5] Add new fields to Quote object: uuid, order_numbers, metadata - Added uuid field (Optional[str]) to Quote class - Added order_numbers field (List[str]) to Quote class - Added metadata field (Optional[Dict]) to Quote class - Updated QuoteDetailsMapper to handle new fields - Added special handling for order_numbers default value - Maintains backward compatibility with existing code --- paperless/api_mappers/quotes.py | 10 +++++++++- paperless/objects/quotes.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/paperless/api_mappers/quotes.py b/paperless/api_mappers/quotes.py index 81c6e41..d33614b 100644 --- a/paperless/api_mappers/quotes.py +++ b/paperless/api_mappers/quotes.py @@ -191,10 +191,18 @@ def map(cls, resource): 'customer', 'erp_code', 'send_from_facility', - 'rfq_number' + 'rfq_number', + 'uuid', + 'order_numbers', + 'metadata' ] for key in field_keys: mapped_result[key] = resource.get(key, None) + + # Handle special default values for new fields + if 'order_numbers' not in resource or resource['order_numbers'] is None: + mapped_result['order_numbers'] = [] + bool_keys = ['export_controlled', 'is_unviewed_drafted_rfq'] for key in bool_keys: mapped_result[key] = resource.get(key, False) diff --git a/paperless/objects/quotes.py b/paperless/objects/quotes.py index ed75a45..9ae1a6b 100644 --- a/paperless/objects/quotes.py +++ b/paperless/objects/quotes.py @@ -522,6 +522,18 @@ class Quote( attr.validators.instance_of((int, float, object)) ), ) + uuid: Optional[str] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(str)), + default=None + ) + order_numbers: List[str] = attr.ib( + converter=optional_convert(convert_iterable(str)), + default=attr.Factory(list) + ) + metadata: Optional[Dict[str, Union[str, int, float, bool]]] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(dict)), + default=None + ) @classmethod def construct_get_url(cls): From 871e22bd2a63b22ba3089e8531fea5ec99879112 Mon Sep 17 00:00:00 2001 From: Jack Eccles Date: Wed, 8 Oct 2025 14:24:51 -0400 Subject: [PATCH 2/5] accepts Andrew's feedback to get order numbers from dict --- paperless/api_mappers/quotes.py | 2 +- paperless/objects/quotes.py | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/paperless/api_mappers/quotes.py b/paperless/api_mappers/quotes.py index d33614b..86a64fe 100644 --- a/paperless/api_mappers/quotes.py +++ b/paperless/api_mappers/quotes.py @@ -200,7 +200,7 @@ def map(cls, resource): mapped_result[key] = resource.get(key, None) # Handle special default values for new fields - if 'order_numbers' not in resource or resource['order_numbers'] is None: + if resource.get('order_numbers', None) is None: mapped_result['order_numbers'] = [] bool_keys = ['export_controlled', 'is_unviewed_drafted_rfq'] diff --git a/paperless/objects/quotes.py b/paperless/objects/quotes.py index 9ae1a6b..ed75a45 100644 --- a/paperless/objects/quotes.py +++ b/paperless/objects/quotes.py @@ -522,18 +522,6 @@ class Quote( attr.validators.instance_of((int, float, object)) ), ) - uuid: Optional[str] = attr.ib( - validator=attr.validators.optional(attr.validators.instance_of(str)), - default=None - ) - order_numbers: List[str] = attr.ib( - converter=optional_convert(convert_iterable(str)), - default=attr.Factory(list) - ) - metadata: Optional[Dict[str, Union[str, int, float, bool]]] = attr.ib( - validator=attr.validators.optional(attr.validators.instance_of(dict)), - default=None - ) @classmethod def construct_get_url(cls): From 17b00268c2fc4e94e30ff1fecf2e0964be7d44db Mon Sep 17 00:00:00 2001 From: Jack Eccles Date: Wed, 8 Oct 2025 14:26:38 -0400 Subject: [PATCH 3/5] restores erroneosly removed quote attributes --- paperless/objects/quotes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/paperless/objects/quotes.py b/paperless/objects/quotes.py index ed75a45..9ae1a6b 100644 --- a/paperless/objects/quotes.py +++ b/paperless/objects/quotes.py @@ -522,6 +522,18 @@ class Quote( attr.validators.instance_of((int, float, object)) ), ) + uuid: Optional[str] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(str)), + default=None + ) + order_numbers: List[str] = attr.ib( + converter=optional_convert(convert_iterable(str)), + default=attr.Factory(list) + ) + metadata: Optional[Dict[str, Union[str, int, float, bool]]] = attr.ib( + validator=attr.validators.optional(attr.validators.instance_of(dict)), + default=None + ) @classmethod def construct_get_url(cls): From 61cdaab101b561bcba9054476c2d0fd918811d13 Mon Sep 17 00:00:00 2001 From: Jack Eccles Date: Thu, 23 Oct 2025 10:37:50 -0400 Subject: [PATCH 4/5] Fix order_numbers converter to handle lists of strings correctly - Changed order_numbers converter from convert_iterable(str) to a simple lambda - convert_iterable is designed for attrs classes, not primitives like strings - Added converter that handles None values and converts to empty list - Fixed mapper to ensure order_numbers defaults to empty list when None --- paperless/api_mappers/quotes.py | 2 +- paperless/objects/quotes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/paperless/api_mappers/quotes.py b/paperless/api_mappers/quotes.py index 86a64fe..be0d49a 100644 --- a/paperless/api_mappers/quotes.py +++ b/paperless/api_mappers/quotes.py @@ -200,7 +200,7 @@ def map(cls, resource): mapped_result[key] = resource.get(key, None) # Handle special default values for new fields - if resource.get('order_numbers', None) is None: + if mapped_result.get('order_numbers') is None: mapped_result['order_numbers'] = [] bool_keys = ['export_controlled', 'is_unviewed_drafted_rfq'] diff --git a/paperless/objects/quotes.py b/paperless/objects/quotes.py index 9ae1a6b..dacd34a 100644 --- a/paperless/objects/quotes.py +++ b/paperless/objects/quotes.py @@ -527,7 +527,7 @@ class Quote( default=None ) order_numbers: List[str] = attr.ib( - converter=optional_convert(convert_iterable(str)), + converter=lambda x: x if x is not None else [], default=attr.Factory(list) ) metadata: Optional[Dict[str, Union[str, int, float, bool]]] = attr.ib( From b72b044a02266c266d1874e6f92f0c042deb774d Mon Sep 17 00:00:00 2001 From: Jack Eccles Date: Thu, 23 Oct 2025 10:42:13 -0400 Subject: [PATCH 5/5] Add new fields to mock data and test assertions - Added uuid field to quote.json mock data - Added metadata field to quote.json mock data with sample values - Added test assertions to verify new fields are accessible - All 57 tests pass --- tests/unit/mock_data/quote.json | 6 ++++++ tests/unit/test_quotes.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/tests/unit/mock_data/quote.json b/tests/unit/mock_data/quote.json index b28967f..2ea59b0 100644 --- a/tests/unit/mock_data/quote.json +++ b/tests/unit/mock_data/quote.json @@ -11199,9 +11199,15 @@ "authenticated_pdf_quote_url": null, "is_unviewed_drafted_rfq": false, "created": "2020-12-08T14:54:54+00:00", + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "order_numbers": [ 179 ], + "metadata": { + "source": "test", + "priority_level": 1, + "is_expedited": false + }, "revision_number": null, "send_from_facility": { "id": 1, diff --git a/tests/unit/test_quotes.py b/tests/unit/test_quotes.py index c303b58..9bb767f 100644 --- a/tests/unit/test_quotes.py +++ b/tests/unit/test_quotes.py @@ -20,6 +20,13 @@ def test_get_quote(self): self.assertEqual(q.number, 339) self.assertEqual(q.tax_rate, 0.0) self.assertFalse(q.is_unviewed_drafted_rfq) + # test new fields + self.assertEqual(q.uuid, 'a1b2c3d4-e5f6-7890-abcd-ef1234567890') + self.assertEqual(q.order_numbers, [179]) + self.assertIsNotNone(q.metadata) + self.assertEqual(q.metadata['source'], 'test') + self.assertEqual(q.metadata['priority_level'], 1) + self.assertFalse(q.metadata['is_expedited']) # test customer customer = q.customer self.assertIsNone(customer.id)