Skip to content

Add a Manager with Measurements

Learn how to add a manager that stores measurement values with automatic unit conversion.

Step 1: Declare the manager

# core/managers.py
from django.db.models import CharField

from general_manager.interface import DatabaseInterface
from general_manager.manager import GeneralManager
from general_manager.measurement import Measurement, MeasurementField

class InventoryItem(GeneralManager):
    name: str
    weight: Measurement
    price: Measurement

    class Interface(DatabaseInterface):
        name = CharField(max_length=120)
        weight = MeasurementField(base_unit="kg", null=True, blank=True)
        price = MeasurementField(base_unit="EUR", null=True, blank=True)

If a new instance should receive a logical default, define it on the MeasurementField:

def default_weight() -> Measurement:
    return Measurement(2.6, "kg")


# Inside InventoryItem.Interface:
weight = MeasurementField(
    base_unit="kg", default=default_weight, null=True, blank=True
)

The Django model generated by InventoryItem.Interface materializes that value into weight_value and weight_unit when it initializes a new instance. Passing weight=None or another explicit weight suppresses the default, as does supplying the backing attributes directly. The same initialization rule applies to subclasses that inherit the field; it is not a database-side default.

For ORM interface metadata, concrete Measurement and parseable string defaults are normalized to Measurement objects and checked for compatibility with base_unit, retaining the supplied unit. The backing-column magnitude is converted separately to base_unit. Generated GraphQL SDL and introspection can expose these defaults. Callable defaults are not evaluated during schema construction and are omitted from GraphQL defaults. Generated partial-update arguments omit defaults: leaving weight out keeps the stored value, while explicitly passing null clears it when the field is nullable.

Step 2: Run migrations

python manage.py makemigrations
python manage.py migrate

Step 3: Create data

item = InventoryItem.create(
    creator_id=1,  # replace with your user ID
    name="Battery pack",
    weight="2.6 kg",
    price="199.00 EUR",
)

Step 4: Convert units

When you read data from the manager, you receive Measurement objects:

item.weight # returns Measurement(2.6, 'kg')
item.weight.to("gram") # returns Measurement(2600.0, 'gram')
item.price.to("USD", exchange_rate=1.1) # returns Measurement(218.9, 'USD')

You can also create and parse measurements directly when building calculated values or tests:

weight = Measurement(2.6, "kg")
price = Measurement.from_string("199.00 EUR")
ratio = Measurement.from_string("0.25") # dimensionless

total_weight = weight + Measurement(400, "gram")
discounted_price = price * Measurement(10, "percent")
discounted_price.to("EUR") # returns Measurement(19.9, 'EUR')

Use "count" for discrete item quantities. Counts keep their unit through scalar operations such as weighted averages and are separate from plain dimensionless ratios:

units = (Measurement(2, "count") + Measurement(4, "count")) / 2
units.to("count") # returns Measurement(3, 'count')

Addition and subtraction require compatible units. Currency values must use the same currency code for arithmetic, and converting between currencies requires an explicit exchange rate. The exchange rate is target currency per one source currency unit:

price.to("USD", exchange_rate=1.1)

# Raises MissingExchangeRateError because no exchange rate is provided.
price.to("USD")

Measurement.from_string() accepts either <value> <unit> or a single dimensionless value. Empty-string and "dimensionless" units both expose the canonical unit "dimensionless". Invalid text raises InvalidMeasurementStringError or InvalidDimensionlessValueError; incompatible arithmetic raises one of the measurement-specific TypeError or ValueError subclasses. Equivalent measurements compare and hash by normalized base-unit value, so Measurement(1, "kg") and Measurement(1000, "gram") can be used interchangeably as dictionary keys. Count measurements use the public unit "count" while Pint stores their internal base unit as "piece".

Step 5: Expose via GraphQL

The GraphQL schema exposes measurements as objects. Clients can request measurements directly and specify the desired unit.

query {
  inventoryItemList {
    name
    weight(targetUnit: "gram") {
      value
      unit
    }
    price {
      value
      unit
    }
  }
}

You now have a measurement-aware manager ready for reporting and analytics.