Skip to content

Computed Fields

Computed fields automatically calculate their values from other fields.

from fullfinity.engine.base import *
class Order(Model):
quantity = Integer(default=1)
unit_price = Monetary(default=0.0)
# Computed field
total = Monetary(
calculate="_compute_total",
store=True
)
@Model.calculate("quantity", "unit_price")
async def _compute_total(self):
self.total = self.quantity * self.unit_price

Declares which fields trigger recomputation:

@Model.calculate("quantity", "unit_price")
async def _compute_total(self):
self.total = self.quantity * self.unit_price

When quantity or unit_price changes, _compute_total is called automatically.

  • Saved in database
  • Computed on save
  • Can be filtered/sorted
  • Better for frequently accessed data
total = Monetary(calculate="_compute_total", store=True)
  • Computed on-the-fly
  • Not in database
  • Cannot be filtered
  • Better for derived display values
  • Only hydrated on root-level records by default
display_name = Char(
max_length=255,
calculate="get_display_name",
store=False
)

:::warning Hydration Depth Non-stored computed fields are only automatically hydrated on root-level query results. For nested records (via prefetch_related), you must explicitly request them using with_fields().

# Root level: margin is hydrated ✓
products = await Product.filter().all()
print(product.margin) # Works
# Nested: margin is NOT hydrated ✗
order = await Order.filter(id=1).prefetch_related("lines__product").first()
print(order.lines[0].product.margin) # Raises AttributeError!
# Fix: Use with_fields to explicitly request nested calculated fields
order = await Order.filter(id=1).with_fields("lines__product__margin").first()
print(order.lines[0].product.margin) # Works ✓

See Querying Data - Select Specific Fields for more details. :::

Computed fields can depend on other computed fields:

class Order(Model):
quantity = Integer(default=1)
unit_price = Monetary(default=0.0)
discount_percent = Float(default=0.0)
tax_rate = Float(default=10.0)
# Subtotal depends on quantity and price
subtotal = Monetary(calculate="_compute_subtotal", store=True)
# Discount depends on subtotal
discount_amount = Monetary(calculate="_compute_discount", store=True)
# Total depends on subtotal, discount, and tax
total = Monetary(calculate="_compute_total", store=True)
@Model.calculate("quantity", "unit_price")
async def _compute_subtotal(self):
self.subtotal = self.quantity * self.unit_price
@Model.calculate("subtotal", "discount_percent")
async def _compute_discount(self):
self.discount_amount = self.subtotal * (self.discount_percent / 100)
@Model.calculate("subtotal", "discount_amount", "tax_rate")
async def _compute_total(self):
after_discount = self.subtotal - self.discount_amount
self.total = after_discount * (1 + self.tax_rate / 100)

Compute from related records:

class Invoice(Model):
customer = ManyToOne("Contact", related_name="invoices")
# Depends on lines (OneToMany)
total = Monetary(calculate="_compute_total", store=True)
@Model.calculate("lines", "lines__amount")
async def _compute_total(self):
await self.fetch_related("lines")
self.total = sum(line.amount for line in self.lines)
class InvoiceLine(Model):
invoice = ManyToOne("Invoice", related_name="lines")
quantity = Integer(default=1)
unit_price = Monetary()
amount = Monetary(calculate="_compute_amount", store=True)
@Model.calculate("quantity", "unit_price")
async def _compute_amount(self):
self.amount = self.quantity * self.unit_price

Simpler alternative for single-value lookups:

class Contact(Model):
company = ManyToOne("Company", related_name="contacts")
# Auto-computed from relation
company_name = Char(
max_length=255,
related_field="company__name",
store=False
)
country_code = Char(
max_length=10,
related_field="company__country__code",
store=False
)

Common pattern for record display:

class Contact(Model):
first_name = Char(max_length=100)
last_name = Char(max_length=100)
email = Char(max_length=255)
async def get_display_name(self):
if self.first_name and self.last_name:
self.display_name = f"{self.first_name} {self.last_name}"
elif self.email:
self.display_name = self.email
else:
self.display_name = f"Contact #{self.id}"
class TaskList(Model):
name = Char(max_length=255)
task_count = Integer(calculate="_compute_task_count", store=False)
completed_count = Integer(calculate="_compute_task_count", store=False)
progress = Float(calculate="_compute_task_count", store=False)
@Model.calculate("tasks", "tasks__status")
async def _compute_task_count(self):
await self.fetch_related("tasks")
self.task_count = len(self.tasks) if self.tasks else 0
self.completed_count = sum(
1 for t in self.tasks if t.status == "Done"
) if self.tasks else 0
self.progress = (
self.completed_count / self.task_count * 100
if self.task_count > 0 else 0
)
class Product(Model):
type = Selection(choices=["Physical", "Digital"], default="Physical")
weight = Float(default=0.0)
file_size = Integer(default=0)
shipping_required = Boolean(calculate="_compute_shipping", store=True)
@Model.calculate("type")
async def _compute_shipping(self):
self.shipping_required = self.type == "Physical"

1. Always Use the @Model.calculate Decorator

Section titled “1. Always Use the @Model.calculate Decorator”

Every computed field method must have the @Model.calculate decorator. Without it, the system won’t know to call your method:

# Good - decorator present
@Model.calculate("quantity", "unit_price")
async def compute_total(self):
self.total = self.quantity * self.unit_price
# Bad - missing decorator (raises error at startup!)
async def compute_total(self):
self.total = self.quantity * self.unit_price

For fields with no dependencies (computed from external sources), use empty parentheses:

@Model.calculate()
async def compute_is_current(self):
# Query another model
Website = get_model("Website")
website = await Website.filter(theme__id__eq=self.id).first()
self.is_current = website is not None
# Good
@Model.calculate("quantity", "unit_price", "discount")
async def _compute_total(self):
self.total = (self.quantity * self.unit_price) - self.discount
# Bad - missing dependency
@Model.calculate("quantity", "unit_price") # Missing "discount"!
async def _compute_total(self):
self.total = (self.quantity * self.unit_price) - self.discount
# Good - simple calculation
@Model.calculate("lines")
async def _compute_line_count(self):
await self.fetch_related("lines")
self.line_count = len(self.lines)
# Bad - expensive operation in computed field
@Model.calculate("lines")
async def _compute_statistics(self):
# Don't do complex operations here
await self.fetch_related("lines__product__category__parent")
# ... complex aggregation
# Good - not stored, just for display
display_name = Char(calculate="get_display_name", store=False)
# Consider storing if you need to filter/sort
searchable_name = Char(calculate="get_searchable_name", store=True, index=True)
@Model.calculate("quantity", "unit_price")
async def _compute_total(self):
qty = self.quantity or 0
price = self.unit_price or 0
self.total = qty * price