diff --git a/back/boxtribute_server/business_logic/warehouse/box/fields.py b/back/boxtribute_server/business_logic/warehouse/box/fields.py index 1ff03ee6a..61bff16d9 100644 --- a/back/boxtribute_server/business_logic/warehouse/box/fields.py +++ b/back/boxtribute_server/business_logic/warehouse/box/fields.py @@ -55,6 +55,15 @@ async def resolve_box_location(box_obj, info): return +@box.field("measureValue") +def resolve_box_measure_value(box_obj, _): + if box_obj.display_unit_id is None: + # Boxes with size-products (i.e. clothing) don't have any measure value assigned + return + # Convert value from base dimension to front-end unit + return box_obj.display_unit.conversion_factor * box_obj.measure_value + + @box.field("state") def resolve_box_state(box_obj, _): # Instead of a BoxState instance return an integer for EnumType conversion diff --git a/back/boxtribute_server/business_logic/warehouse/box/queries.py b/back/boxtribute_server/business_logic/warehouse/box/queries.py index 7f4c968de..4ac6b0391 100644 --- a/back/boxtribute_server/business_logic/warehouse/box/queries.py +++ b/back/boxtribute_server/business_logic/warehouse/box/queries.py @@ -1,10 +1,12 @@ from ariadne import QueryType +from peewee import JOIN from ....authz import authorize, authorize_for_reading_box from ....graph_ql.filtering import derive_box_filter from ....graph_ql.pagination import load_into_page from ....models.definitions.box import Box from ....models.definitions.location import Location +from ....models.definitions.unit import Unit query = QueryType() @@ -12,8 +14,9 @@ @query.field("box") def resolve_box(*_, label_identifier): box = ( - Box.select(Box, Location) + Box.select(Box, Location, Unit) .join(Location) + .join(Unit, JOIN.LEFT_OUTER, src=Box) # for measureValue resolver .where(Box.label_identifier == label_identifier) .get() ) @@ -25,13 +28,18 @@ def resolve_box(*_, label_identifier): def resolve_boxes(*_, base_id, pagination_input=None, filter_input=None): authorize(permission="stock:read", base_id=base_id) - selection = Box.select().join( - Location, - on=( - (Box.location == Location.id) - & (Location.base == base_id) - & ((Box.deleted_on == 0) | (Box.deleted_on.is_null())) - ), + selection = ( + # Omit Unit.id because it confuses .count() in _compute_total_count() + Box.select(Box, Unit.conversion_factor, Unit.symbol, Unit.name) + .join( + Location, + on=( + (Box.location == Location.id) + & (Location.base == base_id) + & ((Box.deleted_on == 0) | (Box.deleted_on.is_null())) + ), + ) + .join(Unit, JOIN.LEFT_OUTER, src=Box) # for measureValue resolver ) filter_condition, selection = derive_box_filter(filter_input, selection=selection) diff --git a/back/boxtribute_server/business_logic/warehouse/location/fields.py b/back/boxtribute_server/business_logic/warehouse/location/fields.py index 4aee96b4a..f71174037 100644 --- a/back/boxtribute_server/business_logic/warehouse/location/fields.py +++ b/back/boxtribute_server/business_logic/warehouse/location/fields.py @@ -1,9 +1,11 @@ from ariadne import ObjectType +from peewee import JOIN from ....authz import authorize from ....graph_ql.filtering import derive_box_filter from ....graph_ql.pagination import load_into_page from ....models.definitions.box import Box +from ....models.definitions.unit import Unit classic_location = ObjectType("ClassicLocation") @@ -17,7 +19,10 @@ def resolve_location_default_box_state(location_obj, _): @classic_location.field("boxes") def resolve_location_boxes(location_obj, _, pagination_input=None, filter_input=None): authorize(permission="stock:read", base_id=location_obj.base_id) - filter_condition, selection = derive_box_filter(filter_input) + selection = Box.select(Box, Unit) # for measureValue resolver + filter_condition, selection = derive_box_filter(filter_input, selection=selection) + selection = selection.join(Unit, JOIN.LEFT_OUTER, src=Box) + return load_into_page( Box, Box.location == location_obj.id, diff --git a/back/boxtribute_server/graph_ql/definitions/protected/types.graphql b/back/boxtribute_server/graph_ql/definitions/protected/types.graphql index 639307211..25c4ac738 100644 --- a/back/boxtribute_server/graph_ql/definitions/protected/types.graphql +++ b/back/boxtribute_server/graph_ql/definitions/protected/types.graphql @@ -32,6 +32,10 @@ type Box implements ItemsCollection { product: Product " If the box holds a 'measure' product (i.e. classified by a package measure like 500gr), its size is null " size: Size + " Information about the unit that the measure shall be displayed in. If the box holds a product with size (e.g. clothing), its unit is null " + displayUnit: Unit + " The value of the measure, expressed in ``unit``. If the box holds a product with size (e.g. clothing), its measure value is null " + measureValue: Float state: BoxState! qrCode: QrCode createdBy: User diff --git a/back/boxtribute_server/models/definitions/box.py b/back/boxtribute_server/models/definitions/box.py index c2efa4c7f..876791a3b 100644 --- a/back/boxtribute_server/models/definitions/box.py +++ b/back/boxtribute_server/models/definitions/box.py @@ -1,4 +1,4 @@ -from peewee import SQL, CharField, DateTimeField, IntegerField, TextField +from peewee import SQL, CharField, DateTimeField, DecimalField, IntegerField, TextField from ...db import db from ...enums import BoxState as BoxStateEnum @@ -9,6 +9,7 @@ from .product import Product from .qr_code import QrCode from .size import Size +from .unit import Unit from .user import User @@ -93,6 +94,22 @@ class Box(db.Model): # type: ignore on_update="CASCADE", on_delete="RESTRICT", ) + # The unit that the measure is displayed in the front-end in + display_unit = UIntForeignKeyField( + column_name="display_unit_id", + field="id", + model=Unit, + null=True, + on_update="CASCADE", + on_delete="RESTRICT", + ) + # The value of the measure in the dimension's base unit (kilogram for mass, liter + # for volume). Multiply by unit.conversion_factor to obtain value in FE unit + measure_value = DecimalField( + max_digits=36, + decimal_places=18, + null=True, + ) class Meta: table_name = "stock" diff --git a/back/minimal.sql b/back/minimal.sql index 606c37186..84c69c35d 100644 --- a/back/minimal.sql +++ b/back/minimal.sql @@ -2945,6 +2945,8 @@ CREATE TABLE `stock` ( `box_id` varchar(11) NOT NULL DEFAULT '', `product_id` int(11) unsigned NOT NULL, `size_id` int(11) unsigned NOT NULL, + `display_unit_id` int(11) unsigned DEFAULT NULL, + `measure_value` decimal(36,18) unsigned DEFAULT NULL, `items` int(11) DEFAULT NULL, `location_id` int(11) unsigned NOT NULL, `distro_event_id` int(11) unsigned DEFAULT NULL, @@ -2967,12 +2969,14 @@ CREATE TABLE `stock` ( KEY `created_by` (`created_by`), KEY `modified_by` (`modified_by`), KEY `distro_event_id` (`distro_event_id`), + KEY `display_unit_id` (`display_unit_id`), CONSTRAINT `stock_ibfk_10` FOREIGN KEY (`created_by`) REFERENCES `cms_users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_11` FOREIGN KEY (`modified_by`) REFERENCES `cms_users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_14` FOREIGN KEY (`location_id`) REFERENCES `locations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_15` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_16` FOREIGN KEY (`size_id`) REFERENCES `sizes` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_17` FOREIGN KEY (`distro_event_id`) REFERENCES `distro_events` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT `stock_ibfk_18` FOREIGN KEY (`display_unit_id`) REFERENCES `units` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_3` FOREIGN KEY (`qr_id`) REFERENCES `qr` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT `stock_ibfk_9` FOREIGN KEY (`box_state_id`) REFERENCES `box_state` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=100000247 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC; @@ -2985,8 +2989,8 @@ CREATE TABLE `stock` ( LOCK TABLES `stock` WRITE; /*!40000 ALTER TABLE `stock` DISABLE KEYS */; INSERT INTO `stock` VALUES - (100000000,'328765',1163,68,50,100000002,NULL,100000000,'Cypress seed test box','2015-01-01 11:15:32',1,NULL,NULL,'0000-00-00 00:00:00',5), - (100000001,'235563',1165,68,50,100000005,NULL,100000001,'50 dummy products','2019-09-29 18:15:32',1,NULL,NULL,'0000-00-00 00:00:00',5); + (100000000,'328765',1163,68,NULL,NULL,50,100000002,NULL,100000000,'Cypress seed test box','2015-01-01 11:15:32',1,NULL,NULL,'0000-00-00 00:00:00',5), + (100000001,'235563',1165,68,NULL,NULL,50,100000005,NULL,100000001,'50 dummy products','2019-09-29 18:15:32',1,NULL,NULL,'0000-00-00 00:00:00',5); /*!40000 ALTER TABLE `stock` ENABLE KEYS */; UNLOCK TABLES; diff --git a/back/test/data/box.py b/back/test/data/box.py index 5dfa2c13d..519907613 100644 --- a/back/test/data/box.py +++ b/back/test/data/box.py @@ -14,6 +14,7 @@ qr_code_for_not_delivered_box_data, ) from .size import another_size_data, default_size_data +from .unit import gram_unit_data from .user import default_user_data @@ -31,6 +32,8 @@ def default_box_data(): "size": default_size_data()["id"], "location": default_location_data()["id"], "qr_code": default_qr_code_data()["id"], + "display_unit": None, + "measure_value": None, } @@ -165,6 +168,8 @@ def measure_product_box_data(): data["label_identifier"] = "88111177" data["product"] = product_data()[7]["id"] data["size"] = None + data["display_unit"] = gram_unit_data()["id"] + data["measure_value"] = 0.5 data["state"] = BoxState.InStock return data diff --git a/back/test/endpoint_tests/test_box.py b/back/test/endpoint_tests/test_box.py index 0da03ae30..6826bb8c9 100644 --- a/back/test/endpoint_tests/test_box.py +++ b/back/test/endpoint_tests/test_box.py @@ -19,7 +19,12 @@ def test_box_query_by_label_identifier( - read_only_client, default_box, tags, in_transit_box, default_shipment_detail + read_only_client, + default_box, + tags, + in_transit_box, + default_shipment_detail, + measure_product_box, ): # Test case 8.1.1 label_identifier = default_box["label_identifier"] @@ -31,6 +36,8 @@ def test_box_query_by_label_identifier( numberOfItems product {{ id }} size {{ id }} + displayUnit {{ id }} + measureValue state qrCode {{ id }} createdBy {{ id }} @@ -53,6 +60,8 @@ def test_box_query_by_label_identifier( "numberOfItems": default_box["number_of_items"], "product": {"id": str(default_box["product"])}, "size": {"id": str(default_box["size"])}, + "displayUnit": None, + "measureValue": None, "state": BoxState.InStock.name, "qrCode": {"id": str(default_box["qr_code"])}, "createdBy": {"id": str(default_box["created_by"])}, @@ -87,6 +96,22 @@ def test_box_query_by_label_identifier( queried_box = assert_successful_request(read_only_client, query) assert queried_box == {"shipmentDetail": {"id": str(default_shipment_detail["id"])}} + label_identifier = measure_product_box["label_identifier"] + query = f"""query {{ + box(labelIdentifier: "{label_identifier}") {{ + product {{ id }} + size {{ id }} + displayUnit {{ id }} + measureValue + }} }}""" + box = assert_successful_request(read_only_client, query) + assert box == { + "product": {"id": str(measure_product_box["product"])}, + "size": None, + "displayUnit": {"id": str(measure_product_box["display_unit"])}, + "measureValue": 1000 * measure_product_box["measure_value"], + } + def test_box_query_by_qr_code(read_only_client, default_box, default_qr_code): # Test case 8.1.5 diff --git a/front/src/types/generated/graphql.ts b/front/src/types/generated/graphql.ts index 7803a81d1..2ad857d70 100755 --- a/front/src/types/generated/graphql.ts +++ b/front/src/types/generated/graphql.ts @@ -200,6 +200,8 @@ export type Box = ItemsCollection & { createdBy?: Maybe; createdOn?: Maybe; deletedOn?: Maybe; + /** Information about the unit that the measure shall be displayed in. If the box holds a product with size (e.g. clothing), its unit is null */ + displayUnit?: Maybe; distributionEvent?: Maybe; /** Sorted by date, newest first */ history?: Maybe>; @@ -209,6 +211,8 @@ export type Box = ItemsCollection & { lastModifiedBy?: Maybe; lastModifiedOn?: Maybe; location?: Maybe; + /** The value of the measure, expressed in ``unit``. If the box holds a product with size (e.g. clothing), its measure value is null */ + measureValue?: Maybe; numberOfItems?: Maybe; product?: Maybe; qrCode?: Maybe;