feat: add edit/sync support for Material Types + Material Quantities

This commit is contained in:
Lance Edgar 2026-03-09 14:36:53 -05:00
parent 6bc5f06f7a
commit dfc8dc0de3
20 changed files with 1075 additions and 86 deletions

View file

@ -177,6 +177,48 @@ class WuttaFarmAppHandler(base.AppHandler):
with self.short_session(session=session) as sess:
return sess.query(model.Unit).order_by(model.Unit.name).all()
def get_material_types(self, session=None):
"""
Returns a list of all known material types.
"""
model = self.model
with self.short_session(session=session) as sess:
return (
sess.query(model.MaterialType).order_by(model.MaterialType.name).all()
)
def get_quantity_models(self):
model = self.model
return {
"standard": model.StandardQuantity,
"material": model.MaterialQuantity,
}
def get_true_quantity(self, quantity, require=True):
model = self.model
if not isinstance(quantity, model.Quantity):
if require and not quantity:
raise ValueError(f"quantity is not valid: {quantity}")
return quantity
session = self.get_session(quantity)
models = self.get_quantity_models()
if require and quantity.quantity_type_id not in models:
raise ValueError(
f"quantity has invalid quantity_type_id: {quantity.quantity_type_id}"
)
true_quantity = session.get(models[quantity.quantity_type_id], quantity.uuid)
if require and not true_quantity:
raise ValueError(f"quantity has no true/typed quantity record: {quantity}")
return true_quantity
def make_true_quantity(self, quantity_type_id, **kwargs):
models = self.get_quantity_models()
kwargs["quantity_type_id"] = quantity_type_id
return models[quantity_type_id](**kwargs)
def auto_sync_to_farmos(self, obj, model_name=None, client=None, require=True):
"""
Export the given object to farmOS, using configured handler.

View file

@ -0,0 +1,211 @@
"""add MaterialQuantity
Revision ID: 9c53513f8862
Revises: 1c89f3fbb521
Create Date: 2026-03-08 18:14:05.587678
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import wuttjamaican.db.util
# revision identifiers, used by Alembic.
revision: str = "9c53513f8862"
down_revision: Union[str, None] = "1c89f3fbb521"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# quantity_material
op.create_table(
"quantity_material",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["uuid"], ["quantity.uuid"], name=op.f("fk_quantity_material_uuid_quantity")
),
sa.PrimaryKeyConstraint("uuid", name=op.f("pk_quantity_material")),
)
op.create_table(
"quantity_material_version",
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
sa.Column(
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint(
"uuid", "transaction_id", name=op.f("pk_quantity_material_version")
),
)
op.create_index(
op.f("ix_quantity_material_version_end_transaction_id"),
"quantity_material_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_material_version_operation_type"),
"quantity_material_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_quantity_material_version_pk_transaction_id",
"quantity_material_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_quantity_material_version_pk_validity",
"quantity_material_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_material_version_transaction_id"),
"quantity_material_version",
["transaction_id"],
unique=False,
)
# quantity_material_material_type
op.create_table(
"quantity_material_material_type",
sa.Column("uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("quantity_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.Column("material_type_uuid", wuttjamaican.db.util.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["material_type_uuid"],
["material_type.uuid"],
name=op.f(
"fk_quantity_material_material_type_material_type_uuid_material_type"
),
),
sa.ForeignKeyConstraint(
["quantity_uuid"],
["quantity_material.uuid"],
name=op.f(
"fk_quantity_material_material_type_quantity_uuid_quantity_material"
),
),
sa.PrimaryKeyConstraint(
"uuid", name=op.f("pk_quantity_material_material_type")
),
)
op.create_table(
"quantity_material_material_type_version",
sa.Column(
"uuid", wuttjamaican.db.util.UUID(), autoincrement=False, nullable=False
),
sa.Column(
"quantity_uuid",
wuttjamaican.db.util.UUID(),
autoincrement=False,
nullable=True,
),
sa.Column(
"material_type_uuid",
wuttjamaican.db.util.UUID(),
autoincrement=False,
nullable=True,
),
sa.Column(
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint(
"uuid",
"transaction_id",
name=op.f("pk_quantity_material_material_type_version"),
),
)
op.create_index(
op.f("ix_quantity_material_material_type_version_end_transaction_id"),
"quantity_material_material_type_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_material_material_type_version_operation_type"),
"quantity_material_material_type_version",
["operation_type"],
unique=False,
)
op.create_index(
"ix_quantity_material_material_type_version_pk_transaction_id",
"quantity_material_material_type_version",
["uuid", sa.literal_column("transaction_id DESC")],
unique=False,
)
op.create_index(
"ix_quantity_material_material_type_version_pk_validity",
"quantity_material_material_type_version",
["uuid", "transaction_id", "end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_quantity_material_material_type_version_transaction_id"),
"quantity_material_material_type_version",
["transaction_id"],
unique=False,
)
def downgrade() -> None:
# quantity_material_material_type
op.drop_index(
op.f("ix_quantity_material_material_type_version_transaction_id"),
table_name="quantity_material_material_type_version",
)
op.drop_index(
"ix_quantity_material_material_type_version_pk_validity",
table_name="quantity_material_material_type_version",
)
op.drop_index(
"ix_quantity_material_material_type_version_pk_transaction_id",
table_name="quantity_material_material_type_version",
)
op.drop_index(
op.f("ix_quantity_material_material_type_version_operation_type"),
table_name="quantity_material_material_type_version",
)
op.drop_index(
op.f("ix_quantity_material_material_type_version_end_transaction_id"),
table_name="quantity_material_material_type_version",
)
op.drop_table("quantity_material_material_type_version")
op.drop_table("quantity_material_material_type")
# quantity_material
op.drop_index(
op.f("ix_quantity_material_version_transaction_id"),
table_name="quantity_material_version",
)
op.drop_index(
"ix_quantity_material_version_pk_validity",
table_name="quantity_material_version",
)
op.drop_index(
"ix_quantity_material_version_pk_transaction_id",
table_name="quantity_material_version",
)
op.drop_index(
op.f("ix_quantity_material_version_operation_type"),
table_name="quantity_material_version",
)
op.drop_index(
op.f("ix_quantity_material_version_end_transaction_id"),
table_name="quantity_material_version",
)
op.drop_table("quantity_material_version")
op.drop_table("quantity_material")

View file

@ -32,7 +32,13 @@ from .users import WuttaFarmUser
# wuttafarm proper models
from .unit import Unit, Measure
from .material_type import MaterialType
from .quantities import QuantityType, Quantity, StandardQuantity
from .quantities import (
QuantityType,
Quantity,
StandardQuantity,
MaterialQuantity,
MaterialQuantityMaterialType,
)
from .asset import AssetType, Asset, AssetParent
from .asset_land import LandType, LandAsset
from .asset_structure import StructureType, StructureAsset

View file

@ -24,6 +24,8 @@ Model definition for Material Types
"""
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db import model
@ -76,5 +78,23 @@ class MaterialType(model.Base):
""",
)
_quantities = orm.relationship(
"MaterialQuantityMaterialType",
cascade="all, delete-orphan",
cascade_backrefs=False,
back_populates="material_type",
)
def _make_material_quantity(qty):
from wuttafarm.db.model import MaterialQuantityMaterialType
return MaterialQuantityMaterialType(quantity=qty)
quantities = association_proxy(
"_quantities",
"quantity",
creator=_make_material_quantity,
)
def __str__(self):
return self.name or ""

View file

@ -211,6 +211,9 @@ class QuantityMixin:
cascade_backrefs=False,
)
def get_value_decimal(self):
return self.quantity.get_value_decimal()
def render_as_text(self, config=None):
return self.quantity.render_as_text(config)
@ -249,3 +252,64 @@ class StandardQuantity(QuantityMixin, model.Base):
add_quantity_proxies(StandardQuantity)
class MaterialQuantity(QuantityMixin, model.Base):
"""
Represents a Material Quantity from farmOS
"""
__tablename__ = "quantity_material"
__versioned__ = {}
__wutta_hint__ = {
"model_title": "Material Quantity",
"model_title_plural": "Material Quantities",
"farmos_quantity_type": "material",
}
_material_types = orm.relationship(
"MaterialQuantityMaterialType",
cascade="all, delete-orphan",
cascade_backrefs=False,
back_populates="quantity",
)
material_types = association_proxy(
"_material_types",
"material_type",
creator=lambda mtype: MaterialQuantityMaterialType(material_type=mtype),
)
def render_as_text(self, config=None):
text = super().render_as_text(config)
mtypes = ", ".join([str(mt) for mt in self.material_types])
return f"{mtypes} {text}"
add_quantity_proxies(MaterialQuantity)
class MaterialQuantityMaterialType(model.Base):
"""
Represents a "material quantity's material type relationship" from
farmOS.
"""
__tablename__ = "quantity_material_material_type"
__versioned__ = {}
uuid = model.uuid_column()
quantity_uuid = model.uuid_fk_column("quantity_material.uuid", nullable=False)
quantity = orm.relationship(
MaterialQuantity,
foreign_keys=quantity_uuid,
back_populates="_material_types",
)
material_type_uuid = model.uuid_fk_column("material_type.uuid", nullable=False)
material_type = orm.relationship(
"MaterialType",
foreign_keys=material_type_uuid,
back_populates="_quantities",
)

View file

@ -617,6 +617,49 @@ class ToFarmOSQuantity(ToFarmOS):
return payload
class MaterialQuantityImporter(ToFarmOSQuantity):
model_title = "MaterialQuantity"
farmos_quantity_type = "material"
def get_supported_fields(self):
fields = list(super().get_supported_fields())
fields.extend(
[
"material_types",
]
)
return fields
def normalize_target_object(self, quantity):
data = super().normalize_target_object(quantity)
if "material_types" in self.fields:
data["material_types"] = [
UUID(mtype["id"])
for mtype in quantity["relationships"]["material_type"]["data"]
]
return data
def get_quantity_payload(self, source_data):
payload = super().get_quantity_payload(source_data)
rels = {}
if "material_types" in self.fields:
rels["material_type"] = {"data": []}
for uuid in source_data["material_types"]:
rels["material_type"]["data"].append(
{
"id": str(uuid),
"type": "taxonomy_term--material_type",
}
)
payload.setdefault("relationships", {}).update(rels)
return payload
class StandardQuantityImporter(ToFarmOSQuantity):
model_title = "StandardQuantity"

View file

@ -106,6 +106,7 @@ class FromWuttaFarmToFarmOS(FromWuttaFarmHandler, ToFarmOSHandler):
importers["PlantAsset"] = PlantAssetImporter
importers["Unit"] = UnitImporter
importers["MaterialType"] = MaterialTypeImporter
importers["MaterialQuantity"] = MaterialQuantityImporter
importers["StandardQuantity"] = StandardQuantityImporter
importers["ActivityLog"] = ActivityLogImporter
importers["HarvestLog"] = HarvestLogImporter
@ -449,6 +450,24 @@ class FromWuttaFarmQuantity(FromWuttaFarm):
}
class MaterialQuantityImporter(
FromWuttaFarmQuantity, farmos_importing.model.MaterialQuantityImporter
):
"""
WuttaFarm farmOS API exporter for Material Quantities
"""
source_model_class = model.MaterialQuantity
def normalize_source_object(self, quantity):
data = super().normalize_source_object(quantity)
if "material_types" in self.fields:
data["material_types"] = [mt.farmos_uuid for mt in quantity.material_types]
return data
class StandardQuantityImporter(
FromWuttaFarmQuantity, farmos_importing.model.StandardQuantityImporter
):

View file

@ -117,6 +117,7 @@ class FromFarmOSToWuttaFarm(FromFarmOSHandler, ToWuttaFarmHandler):
importers["MaterialType"] = MaterialTypeImporter
importers["QuantityType"] = QuantityTypeImporter
importers["StandardQuantity"] = StandardQuantityImporter
importers["MaterialQuantity"] = MaterialQuantityImporter
importers["LogType"] = LogTypeImporter
importers["ActivityLog"] = ActivityLogImporter
importers["HarvestLog"] = HarvestLogImporter
@ -1419,3 +1420,76 @@ class StandardQuantityImporter(QuantityImporterBase):
"units_uuid",
"label",
]
class MaterialQuantityImporter(QuantityImporterBase):
"""
farmOS API WuttaFarm importer for Material Quantities
"""
model_class = model.MaterialQuantity
def get_supported_fields(self):
fields = list(super().get_supported_fields())
fields.extend(
[
"material_types",
]
)
return fields
def normalize_source_object(self, quantity):
""" """
data = super().normalize_source_object(quantity)
if "material_types" in self.fields:
data["material_types"] = [
UUID(mtype["id"])
for mtype in quantity["relationships"]["material_type"]["data"]
]
return data
def normalize_target_object(self, quantity):
data = super().normalize_target_object(quantity)
if "material_types" in self.fields:
data["material_types"] = [
mtype.farmos_uuid for mtype in quantity.material_types
]
return data
def update_target_object(self, quantity, source_data, target_data=None):
model = self.app.model
quantity = super().update_target_object(quantity, source_data, target_data)
if "material_types" in self.fields:
if (
not target_data
or target_data["material_types"] != source_data["material_types"]
):
for farmos_uuid in source_data["material_types"]:
if (
not target_data
or farmos_uuid not in target_data["material_types"]
):
mtype = (
self.target_session.query(model.MaterialType)
.filter(model.MaterialType.farmos_uuid == farmos_uuid)
.one()
)
quantity.material_types.append(mtype)
if target_data:
for farmos_uuid in target_data["material_types"]:
if farmos_uuid not in source_data["material_types"]:
mtype = (
self.target_session.query(model.MaterialType)
.filter(model.MaterialType.farmos_uuid == farmos_uuid)
.one()
)
quantity.material_types.remove(mtype)
return quantity

View file

@ -260,27 +260,29 @@ class Normalizer(GenericHandler):
measure_id = attrs["measure"]
quantity_objects.append(
{
quantity_object = {
"uuid": quantity["id"],
"drupal_id": attrs["drupal_internal__id"],
"quantity_type_uuid": rels["quantity_type"]["data"][
"id"
"quantity_type_uuid": rels["quantity_type"]["data"]["id"],
"quantity_type_id": rels["quantity_type"]["data"]["meta"][
"drupal_internal__target_id"
],
"quantity_type_id": rels["quantity_type"]["data"][
"meta"
]["drupal_internal__target_id"],
"measure_id": measure_id,
"measure_name": self.get_farmos_measure_name(
measure_id
),
"measure_name": self.get_farmos_measure_name(measure_id),
"value_numerator": value["numerator"],
"value_decimal": value["decimal"],
"value_denominator": value["denominator"],
"unit_uuid": unit_uuid,
"unit_name": unit["attributes"]["name"],
}
)
if quantity_object["quantity_type_id"] == "material":
quantity_object["material_types"] = [
{"uuid": mtype["id"]}
for mtype in quantity["relationships"]["material_type"][
"data"
]
]
quantity_objects.append(quantity_object)
if owners := relationships.get("owner"):
for user in owners["data"]:

View file

@ -164,10 +164,9 @@ class FarmOSRefs(WuttaSet):
self.route_prefix = route_prefix
def serialize(self, node, appstruct):
if appstruct is colander.null:
if not appstruct:
return colander.null
return json.dumps(appstruct)
return appstruct
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import FarmOSRefsWidget
@ -288,6 +287,37 @@ class PlantTypeRefs(WuttaSet):
return PlantTypeRefsWidget(self.request, **kwargs)
class MaterialTypeRefs(colander.List):
"""
Schema type for Material Types field (on a Material Asset).
"""
def __init__(self, request):
super().__init__()
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
def serialize(self, node, appstruct):
if not appstruct:
return colander.null
mtypes = []
for mtype in appstruct:
mtypes.append(
{
"uuid": mtype.uuid.hex,
"name": mtype.name,
}
)
return mtypes
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import MaterialTypeRefsWidget
return MaterialTypeRefsWidget(self.request, **kwargs)
class SeasonRefs(WuttaSet):
"""
Schema type for Plant Types field (on a Plant Asset).
@ -454,11 +484,11 @@ class QuantityRefs(colander.List):
quantities = []
for qty in appstruct:
quantities.append(
{
quantity = {
"uuid": qty.uuid.hex,
"quantity_type": {
"id": qty.quantity_type_id,
"drupal_id": qty.quantity_type_id,
"name": qty.quantity_type.name,
},
"measure": qty.measure_id,
@ -468,9 +498,23 @@ class QuantityRefs(colander.List):
"name": qty.units.name,
},
"as_text": qty.render_as_text(self.config),
# nb. always include this regardless of quantity type,
# for sake of easier frontend logic
"material_types": [],
}
if qty.quantity_type_id == "material":
quantity["material_types"] = []
for mtype in qty.material_types:
quantity["material_types"].append(
{
"uuid": mtype.uuid.hex,
"name": mtype.name,
}
)
quantities.append(quantity)
return quantities
def widget_maker(self, **kwargs):

View file

@ -124,7 +124,7 @@ class FarmOSRefsWidget(Widget):
return HTML.tag("span")
links = []
for obj in json.loads(cstruct):
for obj in cstruct:
url = self.request.route_url(
f"{self.route_prefix}.view", uuid=obj["uuid"]
)
@ -332,6 +332,72 @@ class PlantTypeRefsWidget(Widget):
return set(pstruct.split(","))
class MaterialTypeRefsWidget(Widget):
"""
Widget for Material Types field (on a Material Asset).
"""
template = "materialtyperefs"
values = ()
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
self.config = self.request.wutta_config
self.app = self.config.get_app()
def serialize(self, field, cstruct, **kw):
""" """
model = self.app.model
session = Session()
if not cstruct:
cstruct = []
if readonly := kw.get("readonly", self.readonly):
items = []
for mtype in cstruct:
items.append(
HTML.tag(
"li",
c=tags.link_to(
mtype["name"],
self.request.route_url(
"material_types.view", uuid=mtype["uuid"]
),
),
)
)
return HTML.tag("ul", c=items)
tmpl_values = self.get_template_values(field, cstruct, kw)
return field.renderer(self.template, **tmpl_values)
def get_template_values(self, field, cstruct, kw):
""" """
values = super().get_template_values(field, cstruct, kw)
session = Session()
material_types = []
for mtype in self.app.get_material_types(session):
material_types.append(
{
"uuid": mtype.uuid.hex,
"name": mtype.name,
}
)
values["material_types"] = json.dumps(material_types)
return values
def deserialize(self, field, pstruct):
""" """
if not pstruct:
return []
return json.loads(pstruct)
class SeasonRefsWidget(Widget):
"""
Widget for Seasons field (on a Plant Asset).
@ -550,11 +616,10 @@ class QuantityRefsWidget(Widget):
return ""
quantities = []
for qty in cstruct:
# TODO: support more quantity types
url = self.request.route_url(
"quantities_standard.view", uuid=qty["uuid"]
f"quantities_{qty['quantity_type']['drupal_id']}.view",
uuid=qty["uuid"],
)
quantities.append(HTML.tag("li", c=tags.link_to(qty["as_text"], url)))
@ -570,8 +635,6 @@ class QuantityRefsWidget(Widget):
qtypes = []
for qtype in self.app.get_quantity_types(session):
# TODO: add support for other quantity types
if qtype.drupal_id == "standard":
qtypes.append(
{
"uuid": qtype.uuid.hex,
@ -581,6 +644,16 @@ class QuantityRefsWidget(Widget):
)
values["quantity_types"] = qtypes
material_types = []
for mtype in self.app.get_material_types(session):
material_types.append(
{
"uuid": mtype.uuid.hex,
"name": mtype.name,
}
)
values["material_types"] = material_types
measures = []
for measure in self.app.get_measures(session):
measures.append(

View file

@ -182,6 +182,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "quantities",
"perm": "quantities.list",
},
{
"title": "Material Quantities",
"route": "quantities_material",
"perm": "quantities_material.list",
},
{
"title": "Standard Quantities",
"route": "quantities_standard",
@ -322,6 +327,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "farmos_quantity_types",
"perm": "farmos_quantity_types.list",
},
{
"title": "Material Quantities",
"route": "farmos_quantities_material",
"perm": "farmos_quantities_material.list",
},
{
"title": "Standard Quantities",
"route": "farmos_quantities_standard",
@ -451,6 +461,11 @@ class WuttaFarmMenuHandler(base.MenuHandler):
"route": "farmos_quantity_types",
"perm": "farmos_quantity_types.list",
},
{
"title": "Material Quantities",
"route": "farmos_quantities_material",
"perm": "farmos_quantities_material.list",
},
{
"title": "Standard Quantities",
"route": "farmos_quantities_standard",

View file

@ -0,0 +1,13 @@
<div tal:define="
name name|field.name;
oid oid|field.oid;
vmodel vmodel|'modelData.'+oid;
can_create can_create|False;"
tal:omit-tag="">
<material-types-picker tal:attributes="name name;
v-model vmodel;
:material-types material_types;
:can-create str(can_create).lower();" />
</div>

View file

@ -7,6 +7,7 @@
<quantities-editor tal:attributes="name name;
v-model vmodel;
:quantity-types quantity_types;
:material-types material_types;
:measures measures;
:units units;" />

View file

@ -2,6 +2,7 @@
<%def name="make_wuttafarm_components()">
${self.make_assets_picker_component()}
${self.make_animal_type_picker_component()}
${self.make_material_types_picker_component()}
${self.make_quantity_editor_component()}
${self.make_quantities_editor_component()}
${self.make_plant_types_picker_component()}
@ -241,10 +242,134 @@
</script>
</%def>
<%def name="make_material_types_picker_component()">
<script type="text/x-template" id="material-types-picker-template">
<div>
<input type="hidden" :name="name" :value="JSON.stringify(value)" />
<div style="display: flex; gap: 0.5rem; align-items: center;">
<span>Add:</span>
<b-autocomplete v-model="addName"
ref="addName"
:data="addNameData"
field="name"
open-on-focus
keep-first
@select="addNameSelected"
clear-on-select
style="flex-grow: 1;">
<template #empty>No results found</template>
</b-autocomplete>
</div>
<${b}-table :data="materialTypeData">
<${b}-table-column field="name" v-slot="props">
<span>{{ props.row.name }}</span>
</${b}-table-column>
<${b}-table-column v-slot="props">
<a href="#"
class="has-text-danger"
@click.prevent="removeMaterialType(props.row)">
<i class="fas fa-trash" /> &nbsp; Remove
</a>
</${b}-table-column>
</${b}-table>
</div>
</script>
<script>
const MaterialTypesPicker = {
template: '#material-types-picker-template',
props: {
name: String,
value: Array,
materialTypes: Array,
},
data() {
return {
addName: '',
}
},
computed: {
materialTypeData() {
const data = []
if (this.value) {
const mtypes = new Map(this.materialTypes.map((mtype) => {
return [mtype.uuid, mtype.name]
}))
for (const mtype of this.value) {
if (mtypes.has(mtype.uuid)) {
data.push(mtype)
}
}
}
return data
},
addNameData() {
if (!this.addName) {
return this.materialTypes
}
return this.materialTypes.filter((mtype) => {
return mtype.name.toLowerCase().indexOf(this.addName.toLowerCase()) >= 0
})
},
},
methods: {
focus() {
this.$refs.addName.focus()
},
addNameSelected(option) {
if (!option) {
return
}
const value = Array.from(this.value || [])
if (!value.includes(option)) {
value.push(option)
this.$emit('input', value)
}
this.addName = null
},
removeMaterialType(ptype) {
const value = Array.from(this.value)
const i = value.indexOf(ptype)
value.splice(i, 1)
this.$emit('input', value)
},
},
}
Vue.component('material-types-picker', MaterialTypesPicker)
<% request.register_component('material-types-picker', 'MaterialTypesPicker') %>
</script>
</%def>
<%def name="make_quantity_editor_component()">
<script type="text/x-template" id="quantity-editor-template">
<div>
<b-field v-if="quantityType == 'material'"
label="Material Types"
horizontal>
<material-types-picker v-model="value.material_types"
ref="materials"
:material-types="materialTypes" />
</b-field>
<b-field label="Measure" horizontal
## TODO: why is this needed?
style="margin-bottom: 1rem;">
@ -306,6 +431,8 @@
props: {
name: String,
value: Object,
quantityType: String,
materialTypes: Array,
measures: Array,
units: Array,
creating: {
@ -314,8 +441,6 @@
}
},
data() {
return {
measure: this.value.measure,
valueAmount: this.value.value,
@ -349,8 +474,12 @@
},
methods: {
focusMeasure() {
focusForCreate() {
if (this.value.quantity_type.drupal_id == 'material') {
this.$refs.materials.focus()
} else {
this.$refs.measure.focus()
}
},
focusValue() {
@ -439,6 +568,8 @@
<template #detail="props">
<quantity-editor v-model="props.row"
:quantity-type="props.row.quantity_type.drupal_id"
:material-types="materialTypes"
:measures="measures"
:units="units"
@save="editSave"
@ -468,6 +599,8 @@
<quantity-editor v-show="creating"
v-model="newQuantity"
:quantity-type="quantityType"
:material-types="materialTypes"
ref="newQuantity"
creating
@save="createSave"
@ -484,6 +617,7 @@
name: String,
value: Array,
quantityTypes: Array,
materialTypes: Array,
defaultQuantityType: {
type: String,
default: 'standard',
@ -530,7 +664,7 @@
qty = Object.fromEntries(Object.entries(qty))
qty.uuid = 'new_' + this.newCounter++
qty.as_text = "( " + this.measureMap[qty.measure] + " ) " + qty.value + " " + qty.units.name
qty.as_text = this.getQuantityText(qty)
const value = Array.from(this.value || [])
value.push(qty)
@ -557,7 +691,7 @@
this.creating = true
this.$nextTick(() => {
this.$refs.newQuantity.focusMeasure()
this.$refs.newQuantity.focusForCreate()
})
},
@ -566,11 +700,25 @@
this.editing[qty.uuid] = true
},
editSave(row) {
row.as_text = "( " + this.measureMap[row.measure] + " ) " + row.value + " " + row.units.name
getQuantityText(qty) {
let text = "( " + this.measureMap[qty.measure] + " ) "
+ qty.value + " " + qty.units.name
if (qty.quantity_type.drupal_id == 'material') {
const materials = qty.material_types.map((mtype) => {
return mtype.name
}).join(', ')
text = materials + " " + text
}
return text
},
editSave(qty) {
qty.as_text = this.getQuantityText(qty)
this.$emit('input', this.value)
this.editing[row.uuid] = false
this.$refs.table.closeDetailRow(row)
this.editing[qty.uuid] = false
this.$refs.table.closeDetailRow(qty)
},
editCancel(qty) {

View file

@ -78,4 +78,10 @@ def render_quantity_object(quantity):
measure = quantity["measure_name"]
value = quantity["value_decimal"]
unit = quantity["unit_name"]
return f"( {measure} ) {value} {unit}"
text = f"( {measure} ) {value} {unit}"
if quantity["quantity_type_id"] == "material":
materials = ", ".join([mtype["name"] for mtype in quantity["material_types"]])
return f"{materials} {text}"
return text

View file

@ -45,7 +45,7 @@ from wuttafarm.web.forms.schema import (
LogQuick,
Notes,
)
from wuttafarm.web.util import render_quantity_objects
from wuttafarm.web.util import render_quantity_objects, render_quantity_object
class LogMasterView(FarmOSMasterView):
@ -199,7 +199,20 @@ class LogMasterView(FarmOSMasterView):
)
self.raw_json = result
included = {obj["id"]: obj for obj in result.get("included", [])}
return self.normalize_log(result["data"], included)
instance = self.normalize_log(result["data"], included)
for qty in instance["quantities"]:
if qty["quantity_type_id"] == "material":
for mtype in qty["material_types"]:
result = self.farmos_client.resource.get_id(
"taxonomy_term", "material_type", mtype["uuid"]
)
mtype["name"] = result["data"]["attributes"]["name"]
qty["as_text"] = render_quantity_object(qty)
return instance
def get_instance_title(self, log):
return log["name"]

View file

@ -32,7 +32,7 @@ from wuttaweb.forms.schema import WuttaDateTime
from wuttaweb.forms.widgets import WuttaDateTimeWidget
from wuttafarm.web.views.farmos import FarmOSMasterView
from wuttafarm.web.forms.schema import FarmOSUnitRef
from wuttafarm.web.forms.schema import FarmOSUnitRef, FarmOSRefs
from wuttafarm.web.grids import ResourceData
@ -143,6 +143,7 @@ class QuantityMasterView(FarmOSMasterView):
sort_defaults = ("drupal_id", "desc")
form_fields = [
"quantity_type_name",
"measure",
"value",
"units",
@ -207,18 +208,23 @@ class QuantityMasterView(FarmOSMasterView):
def get_instance(self):
# TODO: this pattern should be repeated for other views
try:
quantity = self.farmos_client.resource.get_id(
"quantity", self.farmos_quantity_type, self.request.matchdict["uuid"]
result = self.farmos_client.resource.get_id(
"quantity",
self.farmos_quantity_type,
self.request.matchdict["uuid"],
params={"include": self.get_farmos_api_includes()},
)
except requests.HTTPError as exc:
if exc.response.status_code == 404:
raise self.notfound()
self.raw_json = quantity
self.raw_json = result
data = self.normalize_quantity(quantity["data"])
included = {obj["id"]: obj for obj in result.get("included", [])}
assert included
data = self.normalize_quantity(result["data"], included)
if relationships := quantity["data"].get("relationships"):
if relationships := result["data"].get("relationships"):
# add units
if units := relationships.get("units"):
@ -286,6 +292,11 @@ class QuantityMasterView(FarmOSMasterView):
f = form
super().configure_form(f)
# quantity_type_name
f.set_label("quantity_type_name", "Quantity Type")
f.set_readonly("quantity_type_name")
f.set_default("quantity_type_name", self.farmos_quantity_type.capitalize())
# created
f.set_node("created", WuttaDateTime(self.request))
f.set_widget("created", WuttaDateTimeWidget(self.request))
@ -338,6 +349,90 @@ class StandardQuantityView(QuantityMasterView):
return buttons
class MaterialQuantityView(QuantityMasterView):
"""
View for farmOS Material Quantities
"""
model_name = "farmos_material_quantity"
model_title = "farmOS Material Quantity"
model_title_plural = "farmOS Material Quantities"
route_prefix = "farmos_quantities_material"
url_prefix = "/farmOS/quantities/material"
farmos_quantity_type = "material"
farmos_refurl_path = "/log-quantities/material"
def get_farmos_api_includes(self):
includes = super().get_farmos_api_includes()
includes.update({"material_type"})
return includes
def normalize_quantity(self, quantity, included={}):
normal = super().normalize_quantity(quantity, included)
material_type_objects = []
material_type_uuids = []
if relationships := quantity["relationships"]:
if material_types := relationships["material_type"]["data"]:
for mtype in material_types:
uuid = mtype["id"]
material_type_uuids.append(uuid)
material_type = {
"uuid": uuid,
"type": mtype["type"],
}
if mtype := included.get(uuid):
material_type.update(
{
"name": mtype["attributes"]["name"],
}
)
material_type_objects.append(material_type)
normal.update(
{
"material_types": material_type_objects,
"material_type_uuids": material_type_uuids,
}
)
return normal
def configure_form(self, form):
f = form
super().configure_form(f)
# material_types
f.fields.insert_before("measure", "material_types")
f.set_node("material_types", FarmOSRefs(self.request, "farmos_material_types"))
def get_xref_buttons(self, material_quantity):
model = self.app.model
session = self.Session()
buttons = []
if wf_material_quantity := (
session.query(model.MaterialQuantity)
.join(model.Quantity)
.filter(model.Quantity.farmos_uuid == material_quantity["uuid"])
.first()
):
buttons.append(
self.make_button(
f"View {self.app.get_title()} record",
primary=True,
url=self.request.route_url(
"quantities_material.view", uuid=wf_material_quantity.uuid
),
icon_left="eye",
)
)
return buttons
def defaults(config, **kwargs):
base = globals()
@ -349,6 +444,11 @@ def defaults(config, **kwargs):
)
StandardQuantityView.defaults(config)
MaterialQuantityView = kwargs.get(
"MaterialQuantityView", base["MaterialQuantityView"]
)
MaterialQuantityView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -290,7 +290,9 @@ class LogMasterView(WuttaFarmMasterView):
f.set_node("quantities", QuantityRefs(self.request))
if not self.creating:
# nb. must explicity declare value for non-standard field
f.set_default("quantities", log.quantities)
f.set_default(
"quantities", [self.app.get_true_quantity(q) for q in log.quantities]
)
# notes
f.set_widget("notes", "notes")
@ -389,15 +391,15 @@ class LogMasterView(WuttaFarmMasterView):
model = self.app.model
session = self.Session()
current = {qty.uuid.hex: qty for qty in log.quantities}
current = {
qty.uuid.hex: self.app.get_true_quantity(qty) for qty in log.quantities
}
for new_qty in desired:
units = session.get(model.Unit, new_qty["units"]["uuid"])
assert units
if new_qty["uuid"].startswith("new_"):
assert new_qty["quantity_type"]["drupal_id"] == "standard"
factory = model.StandardQuantity
qty = factory(
quantity_type_id=new_qty["quantity_type"]["drupal_id"],
qty = self.app.make_true_quantity(
new_qty["quantity_type"]["drupal_id"],
measure_id=new_qty["measure"],
value_numerator=int(new_qty["value"]),
value_denominator=1,
@ -413,6 +415,8 @@ class LogMasterView(WuttaFarmMasterView):
old_qty.value_numerator = int(new_qty["value"])
old_qty.value_denominator = 1
old_qty.units = units
if old_qty.quantity_type_id == "material":
self.set_material_types(old_qty, new_qty["material_types"])
desired = [qty["uuid"] for qty in desired]
for old_qty in list(log.quantities):
@ -421,6 +425,22 @@ class LogMasterView(WuttaFarmMasterView):
if old_qty.uuid and old_qty.uuid.hex not in desired:
log.quantities.remove(old_qty)
def set_material_types(self, quantity, desired):
model = self.app.model
session = self.Session()
current = {mtype.uuid: mtype for mtype in quantity.material_types}
for new_mtype in desired:
mtype = session.get(model.MaterialType, new_mtype["uuid"])
assert mtype
if mtype.uuid not in current:
quantity.material_types.append(mtype)
desired = [mtype["uuid"] for mtype in desired]
for old_mtype in current.values():
if old_mtype.uuid.hex not in desired:
quantity.material_types.remove(old_mtype)
def auto_sync_to_farmos(self, client, log):
model = self.app.model
session = self.Session()
@ -429,10 +449,7 @@ class LogMasterView(WuttaFarmMasterView):
session.flush()
for qty in log.quantities:
# TODO: support more quantity types
if qty.quantity_type_id == "standard":
qty = session.get(model.StandardQuantity, qty.uuid)
assert qty
qty = self.app.get_true_quantity(qty)
self.app.auto_sync_to_farmos(qty, client=client)
self.app.auto_sync_to_farmos(log, client=client)

View file

@ -30,8 +30,13 @@ from webhelpers2.html import tags
from wuttaweb.db import Session
from wuttafarm.web.views import WuttaFarmMasterView
from wuttafarm.db.model import QuantityType, Quantity, StandardQuantity
from wuttafarm.web.forms.schema import UnitRef, LogRef
from wuttafarm.db.model import (
QuantityType,
Quantity,
StandardQuantity,
MaterialQuantity,
)
from wuttafarm.web.forms.schema import UnitRef, LogRef, MaterialTypeRefs
from wuttafarm.util import get_log_type_enum
@ -370,6 +375,74 @@ class StandardQuantityView(QuantityMasterView):
farmos_refurl_path = "/log-quantities/standard"
class MaterialQuantityView(QuantityMasterView):
"""
Master view for Material Quantities
"""
model_class = MaterialQuantity
route_prefix = "quantities_material"
url_prefix = "/quantities/material"
farmos_bundle = "material"
farmos_refurl_path = "/log-quantities/material"
def configure_grid(self, grid):
g = grid
super().configure_grid(g)
# material_types
g.columns.append("material_types")
g.set_label("material_types", "Material Type", column_only=True)
g.set_renderer("material_types", self.render_material_types_for_grid)
def render_material_types_for_grid(self, quantity, field, value):
if self.farmos_style_grid_links:
links = []
for mtype in quantity.material_types:
url = self.request.route_url("material_types.view", uuid=mtype.uuid)
links.append(tags.link_to(str(mtype), url))
return ", ".join(links)
return ", ".join([str(mtype) for mtype in quantity.material_types])
def configure_form(self, form):
f = form
super().configure_form(f)
quantity = form.model_instance
# material_types
f.fields.insert_after("quantity_type", "material_types")
f.set_node("material_types", MaterialTypeRefs(self.request))
if not self.creating:
f.set_default("material_types", quantity.material_types)
def objectify(self, form):
quantity = super().objectify(form)
data = form.validated
self.set_material_types(quantity, data["material_types"])
return quantity
def set_material_types(self, quantity, desired):
model = self.app.model
session = self.Session()
current = {mt.uuid.hex: mt for mt in quantity.material_types}
for mtype in desired:
if mtype["uuid"] not in current:
mtype = session.get(model.MaterialType, mtype["uuid"])
assert mtype
quantity.material_types.append(mtype)
desired = [mtype["uuid"] for mtype in desired]
for uuid, mtype in current.items():
if uuid not in desired:
quantity.material_types.remove(mtype)
def defaults(config, **kwargs):
base = globals()
@ -384,6 +457,11 @@ def defaults(config, **kwargs):
)
StandardQuantityView.defaults(config)
MaterialQuantityView = kwargs.get(
"MaterialQuantityView", base["MaterialQuantityView"]
)
MaterialQuantityView.defaults(config)
def includeme(config):
defaults(config)