diff --git a/.changeset/fiery-candies-kneel.md b/.changeset/fiery-candies-kneel.md new file mode 100644 index 000000000..9ddc12263 --- /dev/null +++ b/.changeset/fiery-candies-kneel.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-data": minor +--- + +Add default values for p_app_amount_unit to baat catalogue diff --git a/.changeset/late-cycles-tan.md b/.changeset/late-cycles-tan.md new file mode 100644 index 000000000..d0f86c2a2 --- /dev/null +++ b/.changeset/late-cycles-tan.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": minor +--- + +Fertilizer application amounts are now shown in units relevant to the fertilizer type used, possibly saving the user from converting application amounts to kg/ha all the time. diff --git a/.changeset/ninety-sloths-notice.md b/.changeset/ninety-sloths-notice.md new file mode 100644 index 000000000..00adb323d --- /dev/null +++ b/.changeset/ninety-sloths-notice.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-core": minor +--- + +addFertilizerApplication and updateFertilizerApplication functions now expect an application value in the unit defined for the fertilizer, instead of kg/ha for every fertilizer. The unit is included in the return values of `getFertilizer`, `getFertilizers` etc. as `p_app_amount_unit`. `getFertilizerApplication`, `getFertilizerApplications` etc. now include `p_app_amount_display` and `p_app_amount_unit` which are to be shown to the user instead of p_app_amount and `kg/ha`. diff --git a/fdm-agents/src/agents/gerrit/agent.ts b/fdm-agents/src/agents/gerrit/agent.ts index 75728cefa..c4566ced9 100644 --- a/fdm-agents/src/agents/gerrit/agent.ts +++ b/fdm-agents/src/agents/gerrit/agent.ts @@ -40,9 +40,10 @@ IMPORTANT CONSTRAINTS: 6. APPLICATION METHOD: For each application, you must propose a valid "p_app_method". Choose ONLY from the "p_app_method_options" returned by the search tool for that specific fertilizer. 7. REALISTIC DATES: Ensure all "p_app_date" values are realistic for the crop type, cultivation season, and Dutch climate. Use the provided "b_lu_start" (sowing/start date) as a critical reference point for each crop. 8. REALISTIC APPLICATION AMOUNTS: Ensure the proposed "p_app_amount" per application matches the technical capabilities of common farming equipment. If the total advice requires more, you MUST split it into multiple applications on different dates. - - slurry (drijfmest): 15,000 - 30,000 kg/ha per application (15-30 m³/ha). - - Solid manure / compost (vaste mest): 10,000 - 30,000 kg/ha per application (10-30 t/ha). + - slurry (drijfmest): 15-30 m³/ha per application. + - Solid manure / compost (vaste mest): 10-30 t/ha per application. - Mineral fertilizers: 50 - 450 kg/ha per application. + - Liquid mineral fertilizers (oplossing): 10 - 1000 l/ha per application. 9. PRIORITIZATION: If legal norms (especially Nitrogen or Phosphate) limit the total nutrient space on the farm, prioritize fulfilling the nutrient advice for high-value crops (e.g., potatoes, onions, sugar beets, vegetables) over lower-value crops or grasslands. Strategy should focus on maximizing the economic return of the limited nutrient space. 10. ORGANIC FARMING: If "Organic Farming" is YES, you MUST NOT use any mineral fertilizers ("p_type": "mineral") in the plan. 11. MANURE FILLING STRATEGY: @@ -83,7 +84,14 @@ Your final response MUST be a JSON object with exactly this structure (all field { "b_id": "string", "applications": [ - { "p_id_catalogue": "string", "p_app_amount": number, "p_app_date": "YYYY-MM-DD", "p_app_method": "string" } + { + "p_id_catalogue": "string", + "p_app_amount": number, + "p_app_amount_display": number, + "p_app_amount_unit": "kg/ha" | "l/ha" | "t/ha" | "m3/ha", + "p_app_date": "YYYY-MM-DD", + "p_app_method": "string" + } ], "fieldMetrics": { "advice": { @@ -137,15 +145,27 @@ CALCULATOR REFERENCE (units and semantics for the simulation tool): - "omBalance" (organische stofbalans): net organic matter balance, kg EOM/ha. Positive = good. Aim for ≥ 0. - "nBalance": nitrogen balance structured exactly as fdm-calculator outputs. "nBalance.balance" and "nBalance.target" are in kg N/ha. "nBalance.emission.ammonia.total" and "nBalance.emission.nitrate.total" are also in kg N/ha. The farm-level averages are automatically area-weighted by the simulation tool. nBalance.balance must be ≤ nBalance.target if keepNitrogenBalanceBelowTarget is YES. - "p_app_amount": application amount — **always in kg/ha, regardless of fertilizer type**. - - Liquid manure / digestate / slurry: convert m³/ha → kg/ha using 1 m³ = 1000 kg. Round to nearest 1000. Example: 18 m³/ha = 18000 kg/ha. - - Solid manure / compost: convert t/ha → kg/ha using 1 t = 1000 kg. Round to nearest 1000. Example: 20 t/ha = 20000 kg/ha. - - Mineral fertilizers: already in kg/ha, round to nearest 5 or 10. Example: 200 kg/ha KAS. + - Propose a round number for the native display unit (e.g., 25 m³/ha, 20 t/ha, 300 l/ha, 200 kg/ha) and then convert to kg/ha for the tools. + - Liquid manure / digestate / slurry: convert m³/ha → kg/ha using: 1 m³/ha = 1000 * density (kg/l). + Example: 25 m³/ha with density 1.005 kg/l = 25 * 1000 * 1.005 = 25125 kg/ha. + - Solid manure / compost: convert t/ha → kg/ha using: 1 t/ha = 1000 kg/ha. + Example: 20 t/ha = 20000 kg/ha. + - Liquid mineral fertilizers (e.g. Ammoniumnitraatureanoplossing): convert l/ha → kg/ha using: 1 l/ha = density (kg/l). + Example: 300 l/ha with density 1.2 kg/l = 300 * 1.2 = 360 kg/ha. + - Solid mineral fertilizers: already in kg/ha, round to nearest 5 or 10. + Example: 200 kg/ha KAS. +- "p_app_amount_display": application amount formatted with its native unit — **use this for the user-facing plan and summary**. + - Use the "p_app_amount_unit" and "p_density" (if needed for volume-to-mass conversion) from the search tool results to determine the correct unit. + - Liquid manure / slurry: e.g., "18 m³/ha". + - Solid manure / compost: e.g., "20 t/ha". + - Mineral fertilizers: e.g., "200 kg/ha". + - Liquid mineral fertilizers: e.g., "300 l/ha". - "p_ef_nh3": ammonia emission factor (fraction of N applied lost as NH3). Lower = less emission. TOOL RETURN SHAPES: - "getFarmFields" returns { fields: [...] } — access the array via result.fields. Each field includes main cultivation details (b_lu_catalogue, b_lu_name, b_lu_start). - "getFarmNutrientAdvice" returns { advicePerField: [...] } — access via result.advicePerField - "getFarmLegalNorms" returns { normsPerField: [...] } — access via result.normsPerField -- "searchFertilizers" returns { fertilizers: [...] } — access via result.fertilizers +- "searchFertilizers" returns { fertilizers: [...] } — access via result.fertilizers. Each fertilizer includes "p_app_amount_unit" and "p_density". - "simulateFarmPlan" returns { fieldResults: [...], farmTotals: {...}, isValid: bool, complianceIssues: [...], agronomicWarnings: [...] }. Each entry in "fieldResults" has: { b_id, b_area, isValid, fieldMetrics: { normsFilling: { manure, nitrogen, phosphate }, norms: { manure, nitrogen, phosphate }, proposedDose: { p_dose_n, p_dose_nw, p_dose_p, p_dose_k, p_dose_s, p_dose_mg, p_dose_ca, p_dose_na, p_dose_cu, p_dose_zn, p_dose_b, p_dose_mn, p_dose_mo, p_dose_co }, omBalance, nBalance, advice } }. Use "proposedDose.p_dose_nw" (werkzame stikstof, kg/ha) to compare against "advice.d_n_req" — this is the agronomically correct workable-N value. "proposedDose.p_dose_n" is total N and is provided for reference only. diff --git a/fdm-agents/src/tools/fertilizer-planner/index.ts b/fdm-agents/src/tools/fertilizer-planner/index.ts index 3b1cd2cc2..cd120e4d4 100644 --- a/fdm-agents/src/tools/fertilizer-planner/index.ts +++ b/fdm-agents/src/tools/fertilizer-planner/index.ts @@ -321,6 +321,8 @@ export function createFertilizerPlannerTools(fdm: FdmType) { p_eom: f.p_eom, p_ef_nh3: f.p_ef_nh3, p_source: f.p_source, + p_app_amount_unit: f.p_app_amount_unit, + p_density: f.p_density, })), } }, @@ -371,6 +373,18 @@ export function createFertilizerPlannerTools(fdm: FdmType) { p_app_amount: z .number() .describe("Application amount in kg/ha"), + p_app_amount_unit: z + .string() + .optional() + .describe( + "The unit of the application amount (e.g., m3/ha, kg/ha, l/ha, t/ha)", + ), + p_app_amount_display: z + .number() + .optional() + .describe( + "The numeric application amount (unit is carried separately in p_app_amount_unit)", + ), p_app_date: z .string() .describe( diff --git a/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx b/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx index ffe12121e..325d8c637 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/columns.tsx @@ -61,14 +61,16 @@ function formatDateRange(dates: Date[]) { * @param numbers array of numbers. Nulls and undefined items are not allowed. * @returns the formatted string. */ -function formatNumberRange(numbers: number[], unit = "") { +function formatNumberRange(numbers: number[], unit = "", precision = 2) { if (numbers.length === 0) return "" const firstNumber = numbers[0] const lastNumber = numbers[numbers.length - 1] + const pow = 10 ** precision + const round = (x: number) => Math.round(pow * x) / pow return firstNumber === lastNumber || Math.abs(lastNumber - firstNumber) < Math.abs(firstNumber) / 100 - ? `${firstNumber} ${unit}` - : `${firstNumber} - ${lastNumber} ${unit}` + ? `${round(firstNumber)} ${unit}` + : `${round(firstNumber)} - ${round(lastNumber)} ${unit}` } /** @@ -163,10 +165,10 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => formatNumberRange( row.original.applications - .map((application) => application.p_app_amount) - .filter((amount) => amount !== null) + .map((application) => application.p_app_amount_display) + .filter((amount) => amount !== null && amount !== undefined) .sort((a, b) => a - b), - "kg / ha", + row.original.applications[0]?.p_app_amount_unit ?? "kg/ha", ), }, { diff --git a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx index 8f2fb98f1..98badadf2 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/form.tsx @@ -9,6 +9,7 @@ import type { Navigation } from "react-router" import { Form, useNavigate, useSearchParams } from "react-router" import { RemixFormProvider, useRemixForm } from "remix-hook-form" import { useFieldFertilizerFormStore } from "@/app/store/field-fertilizer-form" +import { getApplicationAmountUnitLabel } from "~/components/blocks/fertilizer-applications/utils" import { Combobox } from "~/components/custom/combobox" import { DatePicker } from "~/components/custom/date-picker-v2" import { Button } from "~/components/ui/button" @@ -36,12 +37,7 @@ import { FormSchemaModify, type FormSchemaPartial, } from "./formschema" - -export type FertilizerOption = { - value: string - label: string - applicationMethodOptions?: { value: string; label: string }[] -} +import type { FertilizerOption } from "./types.d" /** * Renders a fertilizer application creation or modification form. @@ -92,7 +88,7 @@ export function FertilizerApplicationForm({ : fertilizerApplication?.p_app_id, p_id: fertilizerApplication?.p_id, p_app_method: fertilizerApplication?.p_app_method, - p_app_amount: undefined, // Handled through an effect due to blank behavior + p_app_amount_display: undefined, // Handled through an effect due to blank behavior p_app_date: fertilizerApplication?.p_app_date ? fertilizerApplication.p_app_date : exampleFertilizerApplication @@ -107,12 +103,17 @@ export function FertilizerApplicationForm({ const selectedFertilizer = options.find((option) => option.value === p_id) const isSubmitting = navigation.state !== "idle" + // If the user switched the fertilizer, clear the application method and amount useEffect(() => { if ( p_id && (!fertilizerApplication || fertilizerApplication.p_id !== p_id) ) { form.setValue("p_app_method", "") + form.setValue( + "p_app_amount_display", + undefined as unknown as number, + ) } }, [p_id, fertilizerApplication, form.setValue]) @@ -138,6 +139,7 @@ export function FertilizerApplicationForm({ const fieldFertilizerFormStore = useFieldFertilizerFormStore() + // If the user had a saved fertilizer form and was creating a new fertilizer, fill the form back in useEffect(() => { if (b_id_farm && b_id_or_b_lu_catalogue) { const savedFormValues = fieldFertilizerFormStore.load( @@ -165,11 +167,17 @@ export function FertilizerApplicationForm({ ]) useEffect(() => { - const p_app_amount = fertilizerApplication?.p_app_amount - if (p_app_amount !== null && typeof p_app_amount !== "undefined") { - form.setValue("p_app_amount", p_app_amount) + const p_app_amount_display = fertilizerApplication?.p_app_amount_display + if ( + p_app_amount_display !== null && + typeof p_app_amount_display !== "undefined" + ) { + form.setValue( + "p_app_amount_display", + Math.round(100 * p_app_amount_display) / 100, + ) } - }, [fertilizerApplication?.p_app_amount, form.setValue]) + }, [fertilizerApplication?.p_app_amount_display, form.setValue]) // Change fertilizer selection if the user has added a new fertilizer const new_p_id = searchParams.get("p_id") @@ -209,6 +217,12 @@ export function FertilizerApplicationForm({ ) } + const currentApplicationUnit = + options.find((opt) => opt.value === p_id)?.p_app_amount_unit ?? "kg/ha" + const currentApplicationUnitLabel = getApplicationAmountUnitLabel( + currentApplicationUnit, + ) + return (
({ )} /> ( - Hoeveelheid + + Hoeveelheid ( + {currentApplicationUnitLabel}) + (typeof val === "string" && val !== "" ? Number(val) : val), z .number({ diff --git a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx index 8f4780d43..e44f3f694 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/list.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/list.tsx @@ -106,8 +106,17 @@ export function FertilizerApplicationsList({

- {application.p_app_amount} kg / - ha + {application.p_app_amount_display !== + null && + application.p_app_amount_display !== + undefined + ? `${application.p_app_amount_display} ${application.p_app_amount_unit}` + : application.p_app_amount !== + null && + application.p_app_amount !== + undefined + ? `${application.p_app_amount} kg/ha` + : "Onbekend hoeveelheid"}

{application.p_app_method diff --git a/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx b/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx index 5a8d3b41a..cca98d834 100644 --- a/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx +++ b/fdm-app/app/components/blocks/fertilizer-applications/types.d.tsx @@ -1,3 +1,4 @@ +import type { AppAmountUnit } from "@nmi-agro/fdm-core" import type { ApplicationMethods } from "@nmi-agro/fdm-data" export interface FertilizerApplication { @@ -16,6 +17,7 @@ export interface FertilizerOption { value: ApplicationMethods label: string }[] + p_app_amount_unit: AppAmountUnit } export interface FertilizerApplicationsFormProps { diff --git a/fdm-app/app/components/blocks/fertilizer-applications/utils.ts b/fdm-app/app/components/blocks/fertilizer-applications/utils.ts new file mode 100644 index 000000000..fa97d9c33 --- /dev/null +++ b/fdm-app/app/components/blocks/fertilizer-applications/utils.ts @@ -0,0 +1,26 @@ +import type { AppAmountUnit } from "@nmi-agro/fdm-core" + +export const applicationUnitOptions = { + "kg/ha": { label: "kg/ha", totalLabel: "kg" }, + "ton/ha": { label: "ton/ha", totalLabel: "ton" }, + "l/ha": { label: "l/ha", totalLabel: "L" }, + "m3/ha": { label: "m³/ha", totalLabel: "m³" }, +} as const + +/** + * Get the pretty-printed mass or volume per area unit for fertilizer applications + * @param unit unit to get the label for + * @returns the pretty-printed unit + */ +export function getApplicationAmountUnitLabel(unit: AppAmountUnit) { + return applicationUnitOptions[unit].label +} + +/** + * Gets the pretty-printed mass or volume unit for fertilizer applications (for when taking sum of areas times application amounts) + * @param unit unit to get the corresponding unit for + * @returns the pretty-printed corresponding mass or volume unit + */ +export function getApplicationAmountTotalUnitLabel(unit: AppAmountUnit) { + return applicationUnitOptions[unit].totalLabel +} diff --git a/fdm-app/app/components/blocks/fertilizer/form.tsx b/fdm-app/app/components/blocks/fertilizer/form.tsx index 7ce0313f2..82dbc7951 100644 --- a/fdm-app/app/components/blocks/fertilizer/form.tsx +++ b/fdm-app/app/components/blocks/fertilizer/form.tsx @@ -167,7 +167,10 @@ export function FertilizerForm({ key={option.value} value={option.value} > - {`${option.label} (${option.value})`} + {param.parameter === + "p_app_amount_unit" + ? option.label + : `${option.label} (${option.value})`} ))} diff --git a/fdm-app/app/components/blocks/fertilizer/formschema.tsx b/fdm-app/app/components/blocks/fertilizer/formschema.tsx index 3b99803fa..65d9968e9 100644 --- a/fdm-app/app/components/blocks/fertilizer/formschema.tsx +++ b/fdm-app/app/components/blocks/fertilizer/formschema.tsx @@ -464,7 +464,25 @@ export const FormSchema = z .refine((value) => value.some((item) => item), { error: "Selecteer minimaal 1 methode", }), + p_app_amount_unit: z + .enum(["kg/ha", "l/ha", "m3/ha", "ton/ha"]) + .default("kg/ha"), }) + .refine( + (data) => { + if ( + data.p_app_amount_unit === "m3/ha" || + data.p_app_amount_unit === "l/ha" + ) { + return data.p_density !== undefined + } + return true + }, + { + path: ["p_density"], + error: "Dichtheid is verplicht bij gebruik van l/ha of m³/ha", + }, + ) .refine( (data) => { if (data.p_n_rt && data.p_n_wc === undefined) { diff --git a/fdm-app/app/components/blocks/fertilizer/utils.ts b/fdm-app/app/components/blocks/fertilizer/utils.ts index 9255c57e4..779094429 100644 --- a/fdm-app/app/components/blocks/fertilizer/utils.ts +++ b/fdm-app/app/components/blocks/fertilizer/utils.ts @@ -61,6 +61,7 @@ export function buildFertilizerDefaults( p_hg_rt: toUndefined(fertilizer.p_hg_rt), p_cl_rt: toUndefined(fertilizer.p_cl_rt), p_app_method_options: fertilizer.p_app_method_options || [], + p_app_amount_unit: fertilizer.p_app_amount_unit || "kg/ha", } } @@ -125,5 +126,6 @@ export function buildCataloguePayload( p_cl_rt: formValues.p_cl_rt, p_ef_nh3: undefined, p_app_method_options: formValues.p_app_method_options, + p_app_amount_unit: formValues.p_app_amount_unit, } } diff --git a/fdm-app/app/components/blocks/gerrit/plan-table.tsx b/fdm-app/app/components/blocks/gerrit/plan-table.tsx index 617450012..0e9a72e0a 100644 --- a/fdm-app/app/components/blocks/gerrit/plan-table.tsx +++ b/fdm-app/app/components/blocks/gerrit/plan-table.tsx @@ -36,6 +36,7 @@ import { } from "~/components/ui/tooltip" import { FertilizerIcon } from "./fertilizer-icon" import type { ParsedPlan, PlanRow } from "./types" +import { getApplicationAmountUnitLabel } from "../fertilizer-applications/utils" const columnHelper = createColumnHelper() @@ -102,7 +103,7 @@ const columns = [

{apps.map((app, _i) => ( @@ -115,7 +116,14 @@ const columns = [ {app.p_name_nl} - {app.p_app_amount} {"kg/ha"} + {Math.round( + (app.p_app_amount_display ?? + 0) * 100, + ) / 100}{" "} + {getApplicationAmountUnitLabel( + app.p_app_amount_unit ?? + "kg/ha", + )} · diff --git a/fdm-app/app/components/blocks/gerrit/types.ts b/fdm-app/app/components/blocks/gerrit/types.ts index 1b4611490..cfbbe41ed 100644 --- a/fdm-app/app/components/blocks/gerrit/types.ts +++ b/fdm-app/app/components/blocks/gerrit/types.ts @@ -7,6 +7,7 @@ import type { NormFilling, NutrientAdvice, } from "@nmi-agro/fdm-calculator" +import type { AppAmountUnit } from "@nmi-agro/fdm-core" export interface ParsedPlanApplication { p_id_catalogue: string @@ -58,6 +59,8 @@ export interface PlanRow { p_name_nl: string | null p_type: string p_app_method_name?: string | null + p_app_amount_display: number + p_app_amount_unit: AppAmountUnit } > fieldMetrics: FieldMetrics | null diff --git a/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx b/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx index cca183f59..f15f7c1db 100644 --- a/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx +++ b/fdm-app/app/components/blocks/pdf/bemestingsplan/BemestingsplanPDF.tsx @@ -1,6 +1,10 @@ import { Document, Image, Link, Page, Text, View } from "@react-pdf/renderer" import { format } from "date-fns" import { nl } from "date-fns/locale" +import { + getApplicationAmountTotalUnitLabel, + getApplicationAmountUnitLabel, +} from "~/components/blocks/fertilizer-applications/utils" import { PdfCard } from "../PdfCard" import { PdfTable, @@ -881,7 +885,7 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( Product - Totaal (kg) + Totaal N-totaal (kg) @@ -902,18 +906,36 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( .reduce( (acc, f) => { f.applications.forEach((app) => { + if ( + app.quantity === null || + app.quantity_display === + null + ) { + return + } + if (!acc[app.product]) { acc[app.product] = { amount: 0, + amount_display: 0, + amount_unit: + getApplicationAmountTotalUnitLabel( + app.quantity_unit, + ) ?? "kg", n: 0, nw: 0, p: 0, k: 0, } } - // app.quantity is per ha, so multiply by area + // app.quantity and app.quantity_display are per ha, so multiply by area acc[app.product].amount += app.quantity * f.area + acc[ + app.product + ].amount_display += + app.quantity_display * + f.area acc[app.product].n += app.p_dose_n * f.area acc[app.product].nw += @@ -929,6 +951,8 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( string, { amount: number + amount_display: number + amount_unit: string n: number nw: number p: number @@ -985,11 +1009,11 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( > {Math.round( - stats.amount, + stats.amount_display, ).toLocaleString( "nl-NL", )}{" "} - kg + {stats.amount_unit} ( Datum / product - Hoeveelheid (kg/ha) + Hoeveelheid N tot. / w. (kg/ha) @@ -1855,7 +1879,14 @@ export const BemestingsplanPDF = ({ data }: { data: BemestingsplanData }) => ( - {Math.round(app.quantity)} kg/ha + {app.quantity_display + ? Math.round( + app.quantity_display, + ) + : "?"}{" "} + {getApplicationAmountUnitLabel( + app.quantity_unit, + )} diff --git a/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts b/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts index 0fd59e3b0..797eef1c1 100644 --- a/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts +++ b/fdm-app/app/components/blocks/pdf/bemestingsplan/types.d.ts @@ -1,3 +1,5 @@ +import type { AppAmountUnit } from "@nmi-agro/fdm-core" + export interface BemestingsplanData { config: { name: string @@ -114,7 +116,9 @@ export interface BemestingsplanData { applications: Array<{ date: string product: string - quantity: number + quantity: number | null + quantity_display: number | null + quantity_unit: AppAmountUnit p_dose_n: number p_dose_nw: number p_dose_p: number diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx index c7b7813b7..ee3171a16 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.bemestingsplan[.]pdf.tsx @@ -281,7 +281,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { date: formatDate(app.p_app_date), product: fert?.p_name_nl || app.p_id, - quantity: app.p_app_amount || 0, + quantity: app.p_app_amount ?? 0, + quantity_display: app.p_app_amount_display ?? 0, + quantity_unit: app.p_app_amount_unit, p_dose_n: appDose.p_dose_n || 0, p_dose_nw: appDose.p_dose_nw || 0, p_dose_p: appDose.p_dose_p || 0, diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx index ba1be5f3a..d3b1a7cb1 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.fertilizer._index.tsx @@ -116,9 +116,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Map fertilizers to options for the combobox const fertilizerOptions = fertilizers.map((fertilizer) => { const applicationMethodOptions = fertilizer.p_app_method_options - .map((opt: any) => { - const meta = applicationMethods.options.find( - (x: any) => x.value === opt, + ?.map((opt) => { + const meta = applicationMethods.options?.find( + (x) => x.value === opt, ) return meta ? { value: opt, label: meta.label } : undefined }) @@ -127,6 +127,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { value: fertilizer.p_id, label: fertilizer.p_name_nl, applicationMethodOptions: applicationMethodOptions, + p_app_amount_unit: fertilizer.p_app_amount_unit, } }) @@ -334,14 +335,15 @@ export async function action({ request, params }: ActionFunctionArgs) { request, FormSchema, ) - const { p_id, p_app_amount, p_app_date, p_app_method } = formValues + const { p_id, p_app_amount_display, p_app_date, p_app_method } = + formValues await addFertilizerApplication( fdm, session.principal_id, b_id, p_id, - p_app_amount, + p_app_amount_display, p_app_method, p_app_date, ) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx index 189ce8e74..d0ed22d11 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.fertilizer._index.tsx @@ -171,20 +171,18 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Map fertilizers to options for the combobox const fertilizerOptions = fertilizers.map((fertilizer) => { const applicationMethodOptions = fertilizer.p_app_method_options - .map((opt) => { - const meta = applicationMethods.options.find( + ?.map((opt) => { + const meta = applicationMethods.options?.find( (x) => x.value === opt, ) return meta ? { value: opt, label: meta.label } : undefined }) - .filter( - (option): option is { value: string; label: string } => - option !== undefined, - ) + .filter((option) => option !== undefined) return { value: fertilizer.p_id, - label: fertilizer.p_name_nl, + label: fertilizer.p_name_nl as string, applicationMethodOptions: applicationMethodOptions, + p_app_amount_unit: fertilizer.p_app_amount_unit, } }) @@ -223,7 +221,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { p_id: "string", p_app_date: "date", p_app_method: "string", - p_app_amount: "number", + p_app_amount_display: "number", } as const const keys = Object.keys(keyTypes) as (keyof typeof keyTypes)[] @@ -275,10 +273,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!fertilizerApplication.p_id) { delete fertilizerApplication.p_app_method // Also, no specific placeholder should be shown - delete exampleFertilizerApplication.p_app_amount + delete exampleFertilizerApplication.p_app_amount_display } - loaderExampleFertilizerApplication = exampleFertilizerApplication + loaderExampleFertilizerApplication = Object.fromEntries( + Object.entries(exampleFertilizerApplication).map(([k, v]) => [ + k, + v === null ? undefined : v, + ]), + ) } const loaderFertilizerApplication = fertilizerApplication @@ -645,7 +648,8 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, p_app_id, validatedData.p_id ?? original.p_id, - validatedData.p_app_amount ?? original.p_app_amount, + validatedData.p_app_amount_display ?? + original.p_app_amount_display, validatedData.p_app_method ?? original.p_app_method, validatedData.p_app_date ?? original.p_app_date, ) @@ -681,7 +685,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, b_id, validatedData.p_id, - validatedData.p_app_amount, + validatedData.p_app_amount_display, validatedData.p_app_method, validatedData.p_app_date, ), diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx index d55117b88..3ee61c9ef 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.gerrit.tsx @@ -26,6 +26,7 @@ import { addFertilizerApplication, type Fertilizer, type FertilizerApplication, + fromKgPerHa, getCultivations, getCurrentSoilData, getFarms, @@ -711,8 +712,29 @@ export async function action({ request, params }: ActionFunctionArgs) { applicationMethods?.options?.find( (x: any) => x.value === app.p_app_method, ) + const p_app_amount_display = fert + ? fromKgPerHa( + app.p_app_amount, + fert.p_app_amount_unit, + fert.p_density, + ) + : null + const unitConvertedAmount = + fert && p_app_amount_display !== null + ? { + p_app_amount_display: + p_app_amount_display, + p_app_amount_unit: + fert.p_app_amount_unit, + } + : { + p_app_amount_display: + app.p_app_amount, + p_app_amount_unit: "kg/ha", + } return { ...app, + ...unitConvertedAmount, p_name_nl: fert?.p_name_nl || app.p_id_catalogue, p_type: fert?.p_type || "other", @@ -853,12 +875,24 @@ export async function action({ request, params }: ActionFunctionArgs) { ) } + const amount = fromKgPerHa( + app.p_app_amount, + fertilizer.p_app_amount_unit, + fertilizer.p_density, + ) + + if (amount === null) { + throw new Error( + `Meststof "${fertilizer.p_name_nl}" moet een waarde hebben voor zijn dichtheid.`, + ) + } + await addFertilizerApplication( tx, session.principal_id, field.b_id, fertilizer.p_id, - app.p_app_amount, + amount, app.p_app_method, new Date(app.p_app_date), ) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx index c629b0e7f..6eb48d770 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.rotation_.fertilizer._index.tsx @@ -243,6 +243,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { value: fertilizer.p_id, label: fertilizer.p_name_nl, applicationMethodOptions: applicationMethodOptions, + p_app_amount_unit: fertilizer.p_app_amount_unit, } }, ) @@ -715,7 +716,7 @@ export async function action({ request, params }: ActionFunctionArgs) { session.principal_id, fieldId, validatedData.p_id, - validatedData.p_app_amount, + validatedData.p_app_amount_display, validatedData.p_app_method, validatedData.p_app_date, ), diff --git a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts index 5af070193..27c8714ba 100644 --- a/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts +++ b/fdm-calculator/src/balance/nitrogen/emission/ammonia/fertilizers.ts @@ -1,4 +1,4 @@ -import type { FertilizerApplication } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" import type { CultivationDetail, @@ -184,7 +184,7 @@ function determineMineralAmmoniaEmissionFactor( * @throws Error if an unsupported application method is provided for the given land type. */ function determineManureAmmoniaEmissionFactor( - fertilizerApplication: FertilizerApplication, + fertilizerApplication: BaseFertilizerApplication, cultivations: FieldInput["cultivations"], cultivationDetails: Map, ) { diff --git a/fdm-calculator/src/balance/nitrogen/types.d.ts b/fdm-calculator/src/balance/nitrogen/types.d.ts index 3f0aa2581..7a3b2f02c 100644 --- a/fdm-calculator/src/balance/nitrogen/types.d.ts +++ b/fdm-calculator/src/balance/nitrogen/types.d.ts @@ -1,8 +1,8 @@ import type { + BaseFertilizerApplication, Cultivation, CultivationCatalogue, Fertilizer, - FertilizerApplication, Field, Harvest, SoilAnalysis, @@ -496,7 +496,7 @@ export type FieldInput = { | "b_soiltype_agr" | "b_gwl_class" >[] - fertilizerApplications: FertilizerApplication[] + fertilizerApplications: BaseFertilizerApplication[] depositionSupply?: NitrogenSupplyDeposition } diff --git a/fdm-calculator/src/balance/organic-matter/types.ts b/fdm-calculator/src/balance/organic-matter/types.ts index 2ebf7c008..067b487e5 100644 --- a/fdm-calculator/src/balance/organic-matter/types.ts +++ b/fdm-calculator/src/balance/organic-matter/types.ts @@ -1,7 +1,7 @@ import type { + BaseFertilizerApplication, Cultivation, CultivationCatalogue, - FertilizerApplication, Field, SoilAnalysis, } from "@nmi-agro/fdm-core" @@ -219,7 +219,7 @@ export type FieldInput = { | "b_soiltype_agr" >[] /** The list of fertilizer applications on the field. */ - fertilizerApplications: FertilizerApplication[] + fertilizerApplications: BaseFertilizerApplication[] } /** diff --git a/fdm-calculator/src/doses/calculate-dose.test.ts b/fdm-calculator/src/doses/calculate-dose.test.ts index ee6f04c84..46c2be7d9 100644 --- a/fdm-calculator/src/doses/calculate-dose.test.ts +++ b/fdm-calculator/src/doses/calculate-dose.test.ts @@ -1,4 +1,4 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" import { calculateDose } from "./calculate-dose" @@ -20,7 +20,7 @@ const initialDose = { p_dose_b: 0, } -const baseApplication: FertilizerApplication = { +const baseApplication: BaseFertilizerApplication = { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 100, @@ -53,6 +53,7 @@ const baseFertilizer: Fertilizer = { p_name_en: null, p_description: null, p_app_method_options: null, + p_app_amount_unit: "kg/ha", p_app_amount: null, p_date_acquiring: null, p_picking_date: null, @@ -90,7 +91,7 @@ const baseFertilizer: Fertilizer = { describe("calculateDose", () => { it("should calculate all nutrient doses correctly", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, @@ -150,7 +151,7 @@ describe("calculateDose", () => { }) it("should handle zero application amounts correctly", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, p_app_amount: 0, @@ -166,7 +167,7 @@ describe("calculateDose", () => { }) it("should handle zero nutrient rates correctly", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, @@ -195,7 +196,7 @@ describe("calculateDose", () => { }) it("should throw an error for negative application amounts", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, p_app_amount: -100, @@ -208,7 +209,7 @@ describe("calculateDose", () => { }) it("should throw an error for negative nutrient rates", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, @@ -225,7 +226,7 @@ describe("calculateDose", () => { }) it("should throw an error for missing fertilizers", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, p_id_catalogue: "fert_missing", @@ -248,7 +249,7 @@ describe("calculateDose", () => { }) it("should throw an error for empty fertilizers array", () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { ...baseApplication, }, diff --git a/fdm-calculator/src/doses/calculate-dose.ts b/fdm-calculator/src/doses/calculate-dose.ts index 5b22b3417..795cb6ce2 100644 --- a/fdm-calculator/src/doses/calculate-dose.ts +++ b/fdm-calculator/src/doses/calculate-dose.ts @@ -1,4 +1,4 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import type { Dose, NumericDoseKeys } from "./d" /** @@ -38,7 +38,7 @@ export function calculateDose({ applications, fertilizers, }: { - applications: FertilizerApplication[] + applications: BaseFertilizerApplication[] fertilizers: Fertilizer[] }): { dose: Dose; applications: Dose[] } { if (applications.some((app) => (app.p_app_amount ?? 0) < 0)) { diff --git a/fdm-calculator/src/doses/get-dose-field.test.ts b/fdm-calculator/src/doses/get-dose-field.test.ts index 52817aeae..0de521407 100644 --- a/fdm-calculator/src/doses/get-dose-field.test.ts +++ b/fdm-calculator/src/doses/get-dose-field.test.ts @@ -104,6 +104,7 @@ describe("getDoseForField", () => { p_cl_rt: 0, p_type: "manure", p_app_method_options: undefined, + p_app_amount_unit: "kg/ha", p_no3_rt: undefined, p_nh4_rt: undefined, p_cr_rt: undefined, diff --git a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts index 9e7a2dafc..9977f4fb6 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.test.ts @@ -1,4 +1,4 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" import { calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2025NormsFillingInput } from "./types" @@ -93,7 +93,7 @@ describe("calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNorm", () fertilizerId: string, amount: number, appId: string, - ): FertilizerApplication => ({ + ): BaseFertilizerApplication => ({ p_app_id: appId, p_id: appId, p_id_catalogue: fertilizerId, diff --git a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts index 32f880245..6cad45b66 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/fosfaatgebruiksnorm.ts @@ -1,6 +1,6 @@ import { + type BaseFertilizerApplication, type Fertilizer, - type FertilizerApplication, withCalculationCache, } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" @@ -70,12 +70,12 @@ export function calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNor // Separate applications into standard and organic-rich const standardApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal }[] = [] const organicRichApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal p_type_rvo: string @@ -237,13 +237,13 @@ export function calculateNL2025FertilizerApplicationFillingForFosfaatGebruiksNor * Determines if at least 20 kg P2O5 / ha is applied with organic-rich fertilizers. * This is Condition 1 for the "Stimuleren organische stofrijke meststoffen" regulation. * - * @param {FertilizerApplication[]} applications - An array of fertilizer applications. + * @param {BaseFertilizerApplication[]} applications - An array of fertilizer applications. * @param {Map} fertilizersMap - A map of fertilizers for efficient lookup. * @param {boolean} has_organic_certification - Indicates if the farm has organic certification. * @returns {boolean} True if the 20 kg/ha threshold is met, false otherwise. */ function determineCondition1StimuleringOrganischeStofrijkeMeststoffen( - applications: FertilizerApplication[], + applications: BaseFertilizerApplication[], fertilizersMap: Map, has_organic_certification: boolean, ): boolean { diff --git a/fdm-calculator/src/norms/nl/2025/filling/input.test.ts b/fdm-calculator/src/norms/nl/2025/filling/input.test.ts index b495d24cb..908680e22 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/input.test.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/input.test.ts @@ -1,4 +1,5 @@ import type { + BaseFertilizerApplication, Cultivation, FdmType, Fertilizer, @@ -83,12 +84,12 @@ describe("collectNL2025InputForFertilizerApplicationFilling", () => { b_lu_catalogue: "nl_2014", }, ] - const expectedApplications: FertilizerApplication[] = [ + const expectedApplications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 1000, - } as FertilizerApplication, + } as BaseFertilizerApplication, ] const expectedFertilizers: Fertilizer[] = [ { p_id: "fert1", p_n_rt: 5, p_type_rvo: "115" }, diff --git a/fdm-calculator/src/norms/nl/2025/filling/types.d.ts b/fdm-calculator/src/norms/nl/2025/filling/types.d.ts index a73d3ce5f..acea1058f 100644 --- a/fdm-calculator/src/norms/nl/2025/filling/types.d.ts +++ b/fdm-calculator/src/norms/nl/2025/filling/types.d.ts @@ -1,8 +1,8 @@ import type * as schema from "@nmi-agro/fdm-core" import type { + BaseFertilizerApplication, Cultivation, Fertilizer, - FertilizerApplication, Field, } from "@nmi-agro/fdm-core" import type { RegionKey } from "../value/types" @@ -37,7 +37,7 @@ export type WorkingCoefficientDetails = { export type NL2025NormsFillingInput = { cultivations: Cultivation[] - applications: FertilizerApplication[] + applications: BaseFertilizerApplication[] fertilizers: Fertilizer[] has_organic_certification: boolean has_grazing_intention: boolean diff --git a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts index 48e5fa04d..81a07fba4 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.test.ts @@ -1,4 +1,4 @@ -import type { Fertilizer, FertilizerApplication } from "@nmi-agro/fdm-core" +import type { BaseFertilizerApplication, Fertilizer } from "@nmi-agro/fdm-core" import { describe, expect, it } from "vitest" import { calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm } from "./fosfaatgebruiksnorm" import type { NL2026NormsFillingInput } from "./types" @@ -93,7 +93,7 @@ describe("calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNorm", () fertilizerId: string, amount: number, appId: string, - ): FertilizerApplication => ({ + ): BaseFertilizerApplication => ({ p_app_id: appId, p_id: appId, p_id_catalogue: fertilizerId, diff --git a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts index 25c7e5d4a..0d124abe9 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/fosfaatgebruiksnorm.ts @@ -1,6 +1,6 @@ import { + type BaseFertilizerApplication, type Fertilizer, - type FertilizerApplication, withCalculationCache, } from "@nmi-agro/fdm-core" import Decimal from "decimal.js" @@ -70,12 +70,12 @@ export function calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNor // Separate applications into standard and organic-rich const standardApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal }[] = [] const organicRichApplications: { - application: FertilizerApplication + application: BaseFertilizerApplication p_p_rt: Decimal p_app_amount: Decimal p_type_rvo: string @@ -243,7 +243,7 @@ export function calculateNL2026FertilizerApplicationFillingForFosfaatGebruiksNor * @returns {boolean} True if the 20 kg/ha threshold is met, false otherwise. */ function determineCondition1StimuleringOrganischeStofrijkeMeststoffen( - applications: FertilizerApplication[], + applications: BaseFertilizerApplication[], fertilizersMap: Map, has_organic_certification: boolean, ): boolean { diff --git a/fdm-calculator/src/norms/nl/2026/filling/input.test.ts b/fdm-calculator/src/norms/nl/2026/filling/input.test.ts index f1781e7fc..60cf98bb3 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/input.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/input.test.ts @@ -1,4 +1,5 @@ import type { + BaseFertilizerApplication, Cultivation, FdmType, Fertilizer, @@ -83,12 +84,12 @@ describe("collectNL2026InputForFertilizerApplicationFilling", () => { b_lu_catalogue: "nl_2014", }, ] - const expectedApplications: FertilizerApplication[] = [ + const expectedApplications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_id_catalogue: "fert1", p_app_amount: 1000, - } as FertilizerApplication, + } as BaseFertilizerApplication, ] const expectedFertilizers: Fertilizer[] = [ { diff --git a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts index 7d845fc4d..d95ad4c46 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/stikstofgebruiksnorm.test.ts @@ -1,7 +1,7 @@ import type { + BaseFertilizerApplication, Cultivation, Fertilizer, - FertilizerApplication, } from "@nmi-agro/fdm-core" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { getRegion } from "../../2025/value/stikstofgebruiksnorm" @@ -595,7 +595,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should calculate norm filling correctly for a single application with known nitrogen content", async () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -617,6 +617,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -691,7 +692,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should calculate norm filling correctly for multiple applications", async () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -722,6 +723,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -779,6 +781,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -859,7 +862,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( }) it("should use table11Mestcodes for nitrogen content if p_n_rt is 0", async () => { - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -881,6 +884,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -956,7 +960,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should throw an error if fertilizer cannot be found", async () => { vi.mocked(getRegion).mockResolvedValue("klei") - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -989,7 +993,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should treat onFarmProduced as false when has_grazing_intention is false for drijfmest", async () => { vi.mocked(getRegion).mockResolvedValue("zand_nwc") - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-05-01"), @@ -1011,6 +1015,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, @@ -1088,7 +1093,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( it("should correctly apply bouwland logic for working coefficient", async () => { vi.mocked(getRegion).mockResolvedValue("klei") // Soil type for bouwland rule - const applications: FertilizerApplication[] = [ + const applications: BaseFertilizerApplication[] = [ { p_app_id: "app1", p_app_date: new Date("2026-10-15"), // Sep 1 to Jan 31 period @@ -1110,6 +1115,7 @@ describe("calculateNL2026FertilizerApplicationFillingForStikstofGebruiksNorm", ( p_description: null, p_app_method_options: null, p_app_amount: null, + p_app_amount_unit: "kg/ha", p_date_acquiring: null, p_picking_date: null, p_n_if: null, diff --git a/fdm-calculator/src/norms/nl/2026/filling/types.d.ts b/fdm-calculator/src/norms/nl/2026/filling/types.d.ts index fcaf7f354..bbeed3581 100644 --- a/fdm-calculator/src/norms/nl/2026/filling/types.d.ts +++ b/fdm-calculator/src/norms/nl/2026/filling/types.d.ts @@ -1,8 +1,8 @@ import type * as schema from "@nmi-agro/fdm-core" import type { + BaseFertilizerApplication, Cultivation, Fertilizer, - FertilizerApplication, Field, } from "@nmi-agro/fdm-core" import type { RegionKey } from "../value/types" @@ -37,7 +37,7 @@ export type WorkingCoefficientDetails = { export type NL2026NormsFillingInput = { cultivations: Cultivation[] - applications: FertilizerApplication[] + applications: BaseFertilizerApplication[] fertilizers: Fertilizer[] has_organic_certification: boolean has_grazing_intention: boolean diff --git a/fdm-core/src/catalogues.ts b/fdm-core/src/catalogues.ts index 147282583..b5b7b4d83 100644 --- a/fdm-core/src/catalogues.ts +++ b/fdm-core/src/catalogues.ts @@ -1,3 +1,7 @@ +import type { + CatalogueFertilizer, + CatalogueFertilizerItem, +} from "@nmi-agro/fdm-data" import { getCultivationCatalogue, getFertilizersCatalogue, @@ -10,6 +14,7 @@ import type { PrincipalId } from "./authorization.types" import * as schema from "./db/schema" import { handleError } from "./error" import type { FdmType } from "./fdm.types" +import type { AppAmountUnit } from "./fertilizer-application-unit-conversion" /** * Gets all enabled fertilizer catalogues for a farm. @@ -488,10 +493,17 @@ async function syncFertilizerCatalogue(fdm: FdmType) { const baatCatalogue = await getFertilizersCatalogue("baat") const fertilizersCatalogue = [...srmCatalogue, ...baatCatalogue] + return syncFertilizerCatalogueArray(fdm, fertilizersCatalogue) +} + +export async function syncFertilizerCatalogueArray( + fdm: FdmType, + fertilizersCatalogue: CatalogueFertilizer, +) { await fdm.transaction(async (tx) => { try { - for (const item of fertilizersCatalogue) { - const hash = await hashFertilizer(item) + for (const catalogueItem of fertilizersCatalogue) { + const item = await extendCatalogueFertilizer(catalogueItem) const existing = await tx .select({ hash: schema.fertilizersCatalogue.hash }) .from(schema.fertilizersCatalogue) @@ -504,20 +516,17 @@ async function syncFertilizerCatalogue(fdm: FdmType) { .limit(1) if (existing.length === 0) { //add the item if does not exist - await tx.insert(schema.fertilizersCatalogue).values({ - ...item, - hash: hash, - }) + await tx.insert(schema.fertilizersCatalogue).values(item) } else { // update the hash if it is undefined, null or different if ( existing[0].hash === null || existing[0].hash === undefined || - existing[0].hash !== hash + existing[0].hash !== item.hash ) { await tx .update(schema.fertilizersCatalogue) - .set({ ...item, hash: hash, updated: new Date() }) + .set({ ...item, updated: new Date() }) .where( eq( schema.fertilizersCatalogue.p_id_catalogue, @@ -533,6 +542,26 @@ async function syncFertilizerCatalogue(fdm: FdmType) { }) } +/** + * Extends a catalogue fertilizer with computed properties and its up-to-date hash + * + * @param catalogueFertilizer fertilizer out of the catalogue + * @returns a fertilizer object, ready for fertilizers_catalogue table insertion/update + */ +async function extendCatalogueFertilizer( + catalogueFertilizer: CatalogueFertilizerItem, +) { + const fertWithComputedProps = { + ...catalogueFertilizer, + p_app_amount_unit: (catalogueFertilizer.p_app_amount_unit ?? + "kg/ha") as AppAmountUnit, + } + return { + ...fertWithComputedProps, + hash: await hashFertilizer(fertWithComputedProps), + } +} + async function syncCultivationCatalogue(fdm: FdmType) { const brpCatalogue = await getCultivationCatalogue("brp") diff --git a/fdm-core/src/cultivation.test.ts b/fdm-core/src/cultivation.test.ts index eeae344d6..97fa7ed74 100644 --- a/fdm-core/src/cultivation.test.ts +++ b/fdm-core/src/cultivation.test.ts @@ -1582,6 +1582,7 @@ describe("Cultivation Data Model", () => { b_id_farm, { p_app_method_options: null, + p_app_amount_unit: undefined, p_name_nl, p_name_en, p_description, diff --git a/fdm-core/src/db/migrations/0026_p_app_amount_unit.sql b/fdm-core/src/db/migrations/0026_p_app_amount_unit.sql new file mode 100644 index 000000000..f74ccf8cb --- /dev/null +++ b/fdm-core/src/db/migrations/0026_p_app_amount_unit.sql @@ -0,0 +1,3 @@ +CREATE TYPE "fdm"."p_app_amount_unit" AS ENUM('kg/ha', 'l/ha', 'm3/ha', 'ton/ha');--> statement-breakpoint +ALTER TABLE "fdm"."fertilizers_catalogue" ADD COLUMN "p_app_amount_unit" "fdm"."p_app_amount_unit" DEFAULT 'kg/ha' NOT NULL;--> statement-breakpoint +UPDATE "fdm"."fertilizers_catalogue" SET "hash" = '0000' WHERE "p_source" IN ('baat', 'srm'); \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/0026_snapshot.json b/fdm-core/src/db/migrations/meta/0026_snapshot.json new file mode 100644 index 000000000..d06cc4a37 --- /dev/null +++ b/fdm-core/src/db/migrations/meta/0026_snapshot.json @@ -0,0 +1,3951 @@ +{ + "id": "fb39566c-e775-4a6e-8953-3be53ff56e54", + "prevId": "af3bd68d-428d-4e65-b1ca-36a32d210fbf", + "version": "7", + "dialect": "postgresql", + "tables": { + "fdm.cultivation_catalogue_selecting": { + "name": "cultivation_catalogue_selecting", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk": { + "name": "cultivation_catalogue_selecting_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "cultivation_catalogue_selecting", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_ending": { + "name": "cultivation_ending", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_end": { + "name": "b_lu_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "m_cropresidue": { + "name": "m_cropresidue", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_ending_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_ending_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_ending", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_harvesting": { + "name": "cultivation_harvesting", + "schema": "fdm", + "columns": { + "b_id_harvesting": { + "name": "b_id_harvesting", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_harvest_date": { + "name": "b_lu_harvest_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "cultivation_harvesting_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_harvesting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_harvesting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_harvesting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivation_starting": { + "name": "cultivation_starting", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_start": { + "name": "b_lu_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sowing_amount": { + "name": "b_sowing_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sowing_method": { + "name": "b_sowing_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "cultivation_starting_b_id_fields_b_id_fk": { + "name": "cultivation_starting_b_id_fields_b_id_fk", + "tableFrom": "cultivation_starting", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cultivation_starting_b_lu_cultivations_b_lu_fk": { + "name": "cultivation_starting_b_lu_cultivations_b_lu_fk", + "tableFrom": "cultivation_starting", + "tableTo": "cultivations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu" + ], + "columnsTo": [ + "b_lu" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations": { + "name": "cultivations", + "schema": "fdm", + "columns": { + "b_lu": { + "name": "b_lu", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_variety": { + "name": "b_lu_variety", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_idx": { + "name": "b_lu_idx", + "columns": [ + { + "expression": "b_lu", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk": { + "name": "cultivations_b_lu_catalogue_cultivations_catalogue_b_lu_catalogue_fk", + "tableFrom": "cultivations", + "tableTo": "cultivations_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "b_lu_catalogue" + ], + "columnsTo": [ + "b_lu_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.cultivations_catalogue": { + "name": "cultivations_catalogue", + "schema": "fdm", + "columns": { + "b_lu_catalogue": { + "name": "b_lu_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_source": { + "name": "b_lu_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name": { + "name": "b_lu_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_lu_name_en": { + "name": "b_lu_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_harvestable": { + "name": "b_lu_harvestable", + "type": "b_lu_harvestable", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true + }, + "b_lu_harvestcat": { + "name": "b_lu_harvestcat", + "type": "b_lu_harvestcat", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_lu_hcat3": { + "name": "b_lu_hcat3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_hcat3_name": { + "name": "b_lu_hcat3_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_lu_croprotation": { + "name": "b_lu_croprotation", + "type": "b_lu_croprotation", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_dm": { + "name": "b_lu_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_hi": { + "name": "b_lu_hi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_n_fixation": { + "name": "b_n_fixation", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_eom": { + "name": "b_lu_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_eom_residue": { + "name": "b_lu_eom_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_rest_oravib": { + "name": "b_lu_rest_oravib", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "b_lu_variety_options": { + "name": "b_lu_variety_options", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "b_lu_start_default": { + "name": "b_lu_start_default", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_date_harvest_default": { + "name": "b_date_harvest_default", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_lu_catalogue_idx": { + "name": "b_lu_catalogue_idx", + "columns": [ + { + "expression": "b_lu_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "b_lu_start_default_format": { + "name": "b_lu_start_default_format", + "value": "b_lu_start_default IS NULL OR b_lu_start_default ~ '^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'" + }, + "b_date_harvest_default_format": { + "name": "b_date_harvest_default_format", + "value": "b_date_harvest_default IS NULL OR b_date_harvest_default ~ '^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$'" + } + }, + "isRLSEnabled": false + }, + "fdm.derogation_applying": { + "name": "derogation_applying", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_derogation": { + "name": "b_id_derogation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "derogation_one_per_farm_per": { + "name": "derogation_one_per_farm_per", + "columns": [ + { + "expression": "b_id_derogation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "derogation_applying_b_id_farm_farms_b_id_farm_fk": { + "name": "derogation_applying_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "derogation_applying", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "derogation_applying_b_id_derogation_derogations_b_id_derogation_fk": { + "name": "derogation_applying_b_id_derogation_derogations_b_id_derogation_fk", + "tableFrom": "derogation_applying", + "tableTo": "derogations", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_derogation" + ], + "columnsTo": [ + "b_id_derogation" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.derogations": { + "name": "derogations", + "schema": "fdm", + "columns": { + "b_id_derogation": { + "name": "b_id_derogation", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_derogation_year": { + "name": "b_derogation_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.farms": { + "name": "farms", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name_farm": { + "name": "b_name_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_businessid_farm": { + "name": "b_businessid_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_address_farm": { + "name": "b_address_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_postalcode_farm": { + "name": "b_postalcode_farm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_farm_idx": { + "name": "b_id_farm_idx", + "columns": [ + { + "expression": "b_id_farm", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_acquiring": { + "name": "fertilizer_acquiring", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_acquiring_amount": { + "name": "p_acquiring_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_acquiring_date": { + "name": "p_acquiring_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_acquiring_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_acquiring_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_acquiring", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_applying": { + "name": "fertilizer_applying", + "schema": "fdm", + "columns": { + "p_app_id": { + "name": "p_app_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_app_amount": { + "name": "p_app_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_app_method": { + "name": "p_app_method", + "type": "p_app_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_date": { + "name": "p_app_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_app_idx": { + "name": "p_app_idx", + "columns": [ + { + "expression": "p_app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fertilizer_applying_b_id_fields_b_id_fk": { + "name": "fertilizer_applying_b_id_fields_b_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_applying_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_applying_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_applying", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_catalogue_enabling": { + "name": "fertilizer_catalogue_enabling", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk": { + "name": "fertilizer_catalogue_enabling_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "fertilizer_catalogue_enabling", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizer_picking": { + "name": "fertilizer_picking", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_picking_date": { + "name": "p_picking_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fertilizer_picking_p_id_fertilizers_p_id_fk": { + "name": "fertilizer_picking_p_id_fertilizers_p_id_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id" + ], + "columnsTo": [ + "p_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk": { + "name": "fertilizer_picking_p_id_catalogue_fertilizers_catalogue_p_id_catalogue_fk", + "tableFrom": "fertilizer_picking", + "tableTo": "fertilizers_catalogue", + "schemaTo": "fdm", + "columnsFrom": [ + "p_id_catalogue" + ], + "columnsTo": [ + "p_id_catalogue" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers": { + "name": "fertilizers", + "schema": "fdm", + "columns": { + "p_id": { + "name": "p_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_idx": { + "name": "p_id_idx", + "columns": [ + { + "expression": "p_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fertilizers_catalogue": { + "name": "fertilizers_catalogue", + "schema": "fdm", + "columns": { + "p_id_catalogue": { + "name": "p_id_catalogue", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "p_source": { + "name": "p_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_nl": { + "name": "p_name_nl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "p_name_en": { + "name": "p_name_en", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_description": { + "name": "p_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "p_app_method_options": { + "name": "p_app_method_options", + "type": "p_app_method[]", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "p_app_amount_unit": { + "name": "p_app_amount_unit", + "type": "p_app_amount_unit", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'kg/ha'" + }, + "p_dm": { + "name": "p_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_density": { + "name": "p_density", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_om": { + "name": "p_om", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_a": { + "name": "p_a", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hc": { + "name": "p_hc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eom": { + "name": "p_eom", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_eoc": { + "name": "p_eoc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_rt": { + "name": "p_c_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_of": { + "name": "p_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_if": { + "name": "p_c_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_c_fr": { + "name": "p_c_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cn_of": { + "name": "p_cn_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_rt": { + "name": "p_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_if": { + "name": "p_n_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_of": { + "name": "p_n_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_n_wc": { + "name": "p_n_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_no3_rt": { + "name": "p_no3_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_nh4_rt": { + "name": "p_nh4_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_p_rt": { + "name": "p_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_k_rt": { + "name": "p_k_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mg_rt": { + "name": "p_mg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ca_rt": { + "name": "p_ca_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ne": { + "name": "p_ne", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_rt": { + "name": "p_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_s_wc": { + "name": "p_s_wc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cu_rt": { + "name": "p_cu_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_zn_rt": { + "name": "p_zn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_na_rt": { + "name": "p_na_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_si_rt": { + "name": "p_si_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_b_rt": { + "name": "p_b_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mn_rt": { + "name": "p_mn_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ni_rt": { + "name": "p_ni_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_fe_rt": { + "name": "p_fe_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_mo_rt": { + "name": "p_mo_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_co_rt": { + "name": "p_co_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_as_rt": { + "name": "p_as_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cd_rt": { + "name": "p_cd_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_rt": { + "name": "p_cr_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cr_vi": { + "name": "p_cr_vi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_pb_rt": { + "name": "p_pb_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_hg_rt": { + "name": "p_hg_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_cl_rt": { + "name": "p_cl_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_ef_nh3": { + "name": "p_ef_nh3", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "p_type_manure": { + "name": "p_type_manure", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_mineral": { + "name": "p_type_mineral", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_compost": { + "name": "p_type_compost", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "p_type_rvo": { + "name": "p_type_rvo", + "type": "p_type_rvo", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "p_id_catalogue_idx": { + "name": "p_id_catalogue_idx", + "columns": [ + { + "expression": "p_id_catalogue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_acquiring": { + "name": "field_acquiring", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_start": { + "name": "b_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_acquiring_method": { + "name": "b_acquiring_method", + "type": "b_acquiring_method", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_acquiring_b_id_fields_b_id_fk": { + "name": "field_acquiring_b_id_fields_b_id_fk", + "tableFrom": "field_acquiring", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "field_acquiring_b_id_farm_farms_b_id_farm_fk": { + "name": "field_acquiring_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "field_acquiring", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.field_discarding": { + "name": "field_discarding", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_end": { + "name": "b_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "field_discarding_b_id_fields_b_id_fk": { + "name": "field_discarding_b_id_fields_b_id_fk", + "tableFrom": "field_discarding", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.fields": { + "name": "fields", + "schema": "fdm", + "columns": { + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_name": { + "name": "b_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_geometry": { + "name": "b_geometry", + "type": "geometry(Polygon,4326)", + "primaryKey": false, + "notNull": false + }, + "b_id_source": { + "name": "b_id_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_bufferstrip": { + "name": "b_bufferstrip", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_idx": { + "name": "b_id_idx", + "columns": [ + { + "expression": "b_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "b_geom_idx": { + "name": "b_geom_idx", + "columns": [ + { + "expression": "b_geometry", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_analyses": { + "name": "harvestable_analyses", + "schema": "fdm", + "columns": { + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_lu_yield": { + "name": "b_lu_yield", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield_fresh": { + "name": "b_lu_yield_fresh", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_yield_bruto": { + "name": "b_lu_yield_bruto", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_tarra": { + "name": "b_lu_tarra", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_dm": { + "name": "b_lu_dm", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_moist": { + "name": "b_lu_moist", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_uww": { + "name": "b_lu_uww", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_cp": { + "name": "b_lu_cp", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_harvestable": { + "name": "b_lu_n_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_n_residue": { + "name": "b_lu_n_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_harvestable": { + "name": "b_lu_p_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_p_residue": { + "name": "b_lu_p_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_harvestable": { + "name": "b_lu_k_harvestable", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_lu_k_residue": { + "name": "b_lu_k_residue", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_analyses_idx": { + "name": "b_id_harvestable_analyses_idx", + "columns": [ + { + "expression": "b_id_harvestable_analysis", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestable_sampling": { + "name": "harvestable_sampling", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_harvestable_analysis": { + "name": "b_id_harvestable_analysis", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk": { + "name": "harvestable_sampling_b_id_harvestable_harvestables_b_id_harvestable_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestables", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable" + ], + "columnsTo": [ + "b_id_harvestable" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk": { + "name": "harvestable_sampling_b_id_harvestable_analysis_harvestable_analyses_b_id_harvestable_analysis_fk", + "tableFrom": "harvestable_sampling", + "tableTo": "harvestable_analyses", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_harvestable_analysis" + ], + "columnsTo": [ + "b_id_harvestable_analysis" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.harvestables": { + "name": "harvestables", + "schema": "fdm", + "columns": { + "b_id_harvestable": { + "name": "b_id_harvestable", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "b_id_harvestable_idx": { + "name": "b_id_harvestable_idx", + "columns": [ + { + "expression": "b_id_harvestable", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.intending_grazing": { + "name": "intending_grazing", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_grazing_intention": { + "name": "b_grazing_intention", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "b_grazing_intention_year": { + "name": "b_grazing_intention_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "intending_grazing_b_id_farm_farms_b_id_farm_fk": { + "name": "intending_grazing_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "intending_grazing", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "intending_grazing_b_id_farm_b_grazing_intention_year_pk": { + "name": "intending_grazing_b_id_farm_b_grazing_intention_year_pk", + "columns": [ + "b_id_farm", + "b_grazing_intention_year" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.organic_certifications": { + "name": "organic_certifications", + "schema": "fdm", + "columns": { + "b_id_organic": { + "name": "b_id_organic", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_organic_traces": { + "name": "b_organic_traces", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_organic_skal": { + "name": "b_organic_skal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "b_organic_issued": { + "name": "b_organic_issued", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_organic_expires": { + "name": "b_organic_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.organic_certifications_holding": { + "name": "organic_certifications_holding", + "schema": "fdm", + "columns": { + "b_id_farm": { + "name": "b_id_farm", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "b_id_organic": { + "name": "b_id_organic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organic_one_farm_per_cert": { + "name": "organic_one_farm_per_cert", + "columns": [ + { + "expression": "b_id_organic", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organic_certifications_holding_b_id_farm_farms_b_id_farm_fk": { + "name": "organic_certifications_holding_b_id_farm_farms_b_id_farm_fk", + "tableFrom": "organic_certifications_holding", + "tableTo": "farms", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_farm" + ], + "columnsTo": [ + "b_id_farm" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organic_certifications_holding_b_id_organic_organic_certifications_b_id_organic_fk": { + "name": "organic_certifications_holding_b_id_organic_organic_certifications_b_id_organic_fk", + "tableFrom": "organic_certifications_holding", + "tableTo": "organic_certifications", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id_organic" + ], + "columnsTo": [ + "b_id_organic" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_analysis": { + "name": "soil_analysis", + "schema": "fdm", + "columns": { + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "a_date": { + "name": "a_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "a_source": { + "name": "a_source", + "type": "a_source", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false, + "default": "'other'" + }, + "a_al_ox": { + "name": "a_al_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_c_of": { + "name": "a_c_of", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co": { + "name": "a_ca_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ca_co_po": { + "name": "a_ca_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_caco3_if": { + "name": "a_caco3_if", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cec_co": { + "name": "a_cec_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_clay_mi": { + "name": "a_clay_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cn_fr": { + "name": "a_cn_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_com_fr": { + "name": "a_com_fr", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_cu_cc": { + "name": "a_cu_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_density_sa": { + "name": "a_density_sa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_fe_ox": { + "name": "a_fe_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_cc": { + "name": "a_k_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co": { + "name": "a_k_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_k_co_po": { + "name": "a_k_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_cc": { + "name": "a_mg_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co": { + "name": "a_mg_co", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_mg_co_po": { + "name": "a_mg_co_po", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_pmn": { + "name": "a_n_pmn", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_n_rt": { + "name": "a_n_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nh4_cc": { + "name": "a_nh4_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_nmin_cc": { + "name": "a_nmin_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_no3_cc": { + "name": "a_no3_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_al": { + "name": "a_p_al", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_cc": { + "name": "a_p_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_ox": { + "name": "a_p_ox", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_rt": { + "name": "a_p_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_sg": { + "name": "a_p_sg", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_p_wa": { + "name": "a_p_wa", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_ph_cc": { + "name": "a_ph_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_s_rt": { + "name": "a_s_rt", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_sand_mi": { + "name": "a_sand_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_silt_mi": { + "name": "a_silt_mi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_som_loi": { + "name": "a_som_loi", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "a_zn_cc": { + "name": "a_zn_cc", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_gwl_class": { + "name": "b_gwl_class", + "type": "b_gwl_class", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "b_soiltype_agr": { + "name": "b_soiltype_agr", + "type": "b_soiltype_agr", + "typeSchema": "fdm", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm.soil_sampling": { + "name": "soil_sampling", + "schema": "fdm", + "columns": { + "b_id_sampling": { + "name": "b_id_sampling", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "b_id": { + "name": "b_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_id": { + "name": "a_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "a_depth_upper": { + "name": "a_depth_upper", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "a_depth_lower": { + "name": "a_depth_lower", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "b_sampling_date": { + "name": "b_sampling_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "b_sampling_geometry": { + "name": "b_sampling_geometry", + "type": "geometry(MultiPoint,4326)", + "primaryKey": false, + "notNull": false + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated": { + "name": "updated", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "soil_sampling_b_id_fields_b_id_fk": { + "name": "soil_sampling_b_id_fields_b_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "fields", + "schemaTo": "fdm", + "columnsFrom": [ + "b_id" + ], + "columnsTo": [ + "b_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "soil_sampling_a_id_soil_analysis_a_id_fk": { + "name": "soil_sampling_a_id_soil_analysis_a_id_fk", + "tableFrom": "soil_sampling", + "tableTo": "soil_analysis", + "schemaTo": "fdm", + "columnsFrom": [ + "a_id" + ], + "columnsTo": [ + "a_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.account": { + "name": "account", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.invitation": { + "name": "invitation", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.member": { + "name": "member", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.organization": { + "name": "organization", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.rate_limit": { + "name": "rate_limit", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "last_request": { + "name": "last_request", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_key_unique": { + "name": "rate_limit_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.session": { + "name": "session", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "schemaTo": "fdm-authn", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.user": { + "name": "user", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surname": { + "name": "surname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lang": { + "name": "lang", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'nl-NL'" + }, + "farm_active": { + "name": "farm_active", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authn.verification": { + "name": "verification", + "schema": "fdm-authn", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.audit": { + "name": "audit", + "schema": "fdm-authz", + "columns": { + "audit_id": { + "name": "audit_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "audit_timestamp": { + "name": "audit_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "audit_origin": { + "name": "audit_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource": { + "name": "target_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_resource_id": { + "name": "target_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource": { + "name": "granting_resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granting_resource_id": { + "name": "granting_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed": { + "name": "allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-authz.invitation": { + "name": "invitation", + "schema": "fdm-authz", + "columns": { + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_principal_id": { + "name": "target_principal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires": { + "name": "expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitation_unique_email_idx": { + "name": "invitation_unique_email_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_unique_principal_idx": { + "name": "invitation_unique_principal_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_target_email_idx": { + "name": "invitation_pending_target_email_idx", + "columns": [ + { + "expression": "target_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"fdm-authz\".\"invitation\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "invitation_target_check": { + "name": "invitation_target_check", + "value": "\"fdm-authz\".\"invitation\".\"target_email\" IS NOT NULL OR \"fdm-authz\".\"invitation\".\"target_principal_id\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "fdm-authz.role": { + "name": "role", + "schema": "fdm-authz", + "columns": { + "role_id": { + "name": "role_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created": { + "name": "created", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted": { + "name": "deleted", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "role_idx": { + "name": "role_idx", + "columns": [ + { + "expression": "resource", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-calculator.calculation_cache": { + "name": "calculation_cache", + "schema": "fdm-calculator", + "columns": { + "calculation_hash": { + "name": "calculation_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "calculation_function": { + "name": "calculation_function", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calculator_version": { + "name": "calculator_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "fdm-calculator.calculation_errors": { + "name": "calculation_errors", + "schema": "fdm-calculator", + "columns": { + "calculation_error_id": { + "name": "calculation_error_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "calculation_function": { + "name": "calculation_function", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calculator_version": { + "name": "calculator_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stack_trace": { + "name": "stack_trace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "fdm.b_acquiring_method": { + "name": "b_acquiring_method", + "schema": "fdm", + "values": [ + "nl_01", + "nl_02", + "nl_03", + "nl_04", + "nl_07", + "nl_09", + "nl_10", + "nl_11", + "nl_12", + "nl_13", + "nl_61", + "nl_63", + "unknown" + ] + }, + "fdm.p_app_method": { + "name": "p_app_method", + "schema": "fdm", + "values": [ + "slotted coulter", + "incorporation", + "incorporation 2 tracks", + "injection", + "shallow injection", + "spraying", + "broadcasting", + "spoke wheel", + "pocket placement", + "narrowband" + ] + }, + "fdm.b_gwl_class": { + "name": "b_gwl_class", + "schema": "fdm", + "values": [ + "I", + "Ia", + "Ic", + "II", + "IIa", + "IIb", + "IIc", + "III", + "IIIa", + "IIIb", + "IV", + "IVu", + "IVc", + "V", + "Va", + "Vao", + "Vad", + "Vb", + "Vbo", + "Vbd", + "sV", + "sVb", + "VI", + "VIo", + "VId", + "VII", + "VIIo", + "VIId", + "VIII", + "VIIIo", + "VIIId" + ] + }, + "fdm.b_lu_harvestcat": { + "name": "b_lu_harvestcat", + "schema": "fdm", + "values": [ + "HC010", + "HC020", + "HC031", + "HC040", + "HC041", + "HC042", + "HC050" + ] + }, + "fdm.b_lu_harvestable": { + "name": "b_lu_harvestable", + "schema": "fdm", + "values": [ + "none", + "once", + "multiple" + ] + }, + "fdm.b_lu_croprotation": { + "name": "b_lu_croprotation", + "schema": "fdm", + "values": [ + "other", + "clover", + "nature", + "potato", + "grass", + "rapeseed", + "starch", + "maize", + "cereal", + "sugarbeet", + "alfalfa", + "catchcrop" + ] + }, + "fdm.a_source": { + "name": "a_source", + "schema": "fdm", + "values": [ + "nl-rva-l122", + "nl-rva-l136", + "nl-rva-l264", + "nl-rva-l320", + "nl-rva-l335", + "nl-rva-l610", + "nl-rva-l648", + "nl-rva-l697", + "nl-other-nmi", + "other" + ] + }, + "fdm.b_soiltype_agr": { + "name": "b_soiltype_agr", + "schema": "fdm", + "values": [ + "moerige_klei", + "rivierklei", + "dekzand", + "zeeklei", + "dalgrond", + "veen", + "loess", + "duinzand", + "maasklei" + ] + }, + "fdm.p_app_amount_unit": { + "name": "p_app_amount_unit", + "schema": "fdm", + "values": [ + "kg/ha", + "l/ha", + "m3/ha", + "ton/ha" + ] + }, + "fdm.p_type_rvo": { + "name": "p_type_rvo", + "schema": "fdm", + "values": [ + "10", + "11", + "12", + "13", + "14", + "17", + "18", + "19", + "23", + "30", + "31", + "32", + "33", + "35", + "39", + "40", + "41", + "42", + "43", + "46", + "50", + "56", + "60", + "61", + "75", + "76", + "80", + "81", + "90", + "91", + "92", + "25", + "26", + "27", + "95", + "96", + "97", + "98", + "99", + "100", + "101", + "102", + "103", + "104", + "105", + "106", + "107", + "108", + "109", + "110", + "111", + "112", + "113", + "114", + "115", + "116", + "117", + "120" + ] + } + }, + "schemas": { + "fdm": "fdm", + "fdm-authn": "fdm-authn", + "fdm-authz": "fdm-authz", + "fdm-calculator": "fdm-calculator" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/fdm-core/src/db/migrations/meta/_journal.json b/fdm-core/src/db/migrations/meta/_journal.json index 33564b153..0bbbadf67 100644 --- a/fdm-core/src/db/migrations/meta/_journal.json +++ b/fdm-core/src/db/migrations/meta/_journal.json @@ -183,6 +183,13 @@ "when": 1773751940467, "tag": "0025_rename-b-lu-eom-residue", "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1775635544356, + "tag": "0026_p_app_amount_unit", + "breakpoints": true } ] } diff --git a/fdm-core/src/db/schema.ts b/fdm-core/src/db/schema.ts index 877db0c7f..3890fb88f 100644 --- a/fdm-core/src/db/schema.ts +++ b/fdm-core/src/db/schema.ts @@ -11,6 +11,7 @@ import { timestamp, uniqueIndex, } from "drizzle-orm/pg-core" +import { APP_AMOUNT_UNITS } from "../fertilizer-application-unit-conversion" import { geometry, numericCasted } from "./schema-custom-types" // Define postgres schema @@ -183,6 +184,7 @@ export const applicationMethodEnum = fdmSchema.enum( "p_app_method", applicationMethodOptions.map((x) => x.value) as [string, ...string[]], ) + export const fertilizerApplication = fdmSchema.table( "fertilizer_applying", { @@ -284,6 +286,10 @@ export const typeRvoEnum = fdmSchema.enum( "p_type_rvo", typeRvoOptions.map((x) => x.value) as [string, ...string[]], ) +export const typeApplicationAmountUnitsEnum = fdmSchema.enum( + "p_app_amount_unit", + APP_AMOUNT_UNITS.map((x) => x.value) as [string, ...string[]], +) // Define fertilizers_catalogue table export const fertilizersCatalogue = fdmSchema.table( @@ -295,6 +301,9 @@ export const fertilizersCatalogue = fdmSchema.table( p_name_en: text(), p_description: text(), p_app_method_options: applicationMethodEnum().array(), + p_app_amount_unit: typeApplicationAmountUnitsEnum() + .notNull() + .default("kg/ha"), p_dm: numericCasted(), p_density: numericCasted(), p_om: numericCasted(), diff --git a/fdm-core/src/fertilizer-application-unit-conversion.test.ts b/fdm-core/src/fertilizer-application-unit-conversion.test.ts new file mode 100644 index 000000000..a6633ecfb --- /dev/null +++ b/fdm-core/src/fertilizer-application-unit-conversion.test.ts @@ -0,0 +1,148 @@ +import Decimal from "decimal.js" +import { describe, expect, it } from "vitest" +import { + type AppAmountUnit, + fromKgPerHa, + toKgPerHa, +} from "./fertilizer-application-unit-conversion" + +interface ConversionUnitTestCase { + input: number + unit: AppAmountUnit + density?: number | undefined + output?: number | null + throws?: string +} + +describe("toKgPerHa", () => { + const tests: ConversionUnitTestCase[] = [ + { input: 20, unit: "kg/ha", output: 20 }, + { input: 20, unit: "ton/ha", output: 20000 }, + { input: 20, unit: "l/ha", density: 0.8, output: 16 }, + { input: 20, unit: "m3/ha", density: 0.8, output: 16000 }, + { input: 0, unit: "kg/ha", output: 0 }, + { input: 0, unit: "ton/ha", output: 0 }, + { input: 0, unit: "l/ha", density: 0.8, output: 0 }, + { input: 0, unit: "m3/ha", density: 0.8, output: 0 }, + ] + + const throwingTests: ConversionUnitTestCase[] = [ + { + input: 20, + unit: "l/ha", + density: undefined, + throws: "Positive density (p_density) is required for l/ha → kg/ha conversion", + }, + { + input: 20, + unit: "m3/ha", + density: undefined, + throws: "Positive density (p_density) is required for m3/ha → kg/ha conversion", + }, + { + input: 20, + unit: "ft3/ha" as AppAmountUnit, + density: 2, + throws: "ft3/ha → kg/ha conversion is not supported", + }, + { + input: 20, + unit: "m3/ha", + density: 0, + throws: "Positive density (p_density) is required for m3/ha → kg/ha conversion", + }, + { + input: 20, + unit: "m3/ha", + density: -1, + throws: "Positive density (p_density) is required for m3/ha → kg/ha conversion", + }, + ] + + for (const { input, unit, density, output } of tests) { + it( + density !== undefined + ? `should convert ${unit} to kg/ha with density ${density} kg/l` + : `should convert ${unit} to kg/ha without density specified`, + () => { + expect(toKgPerHa(input, unit, density)).toBe(output) + }, + ) + } + + for (const { input, unit, density, throws } of throwingTests) { + it( + density !== undefined + ? `should throw exception on conversion from ${unit} to kg/ha` + : `should throw exception on conversion from ${unit} to kg/ha without density specified`, + () => { + expect(() => toKgPerHa(input, unit, density)).toThrow(throws) + }, + ) + } + + it("should accept input of type Decimal", () => { + expect(toKgPerHa(new Decimal(10), "m3/ha", new Decimal(2))).toBe(20000) + }) +}) + +describe("fromKgPerHa", () => { + const tests: ConversionUnitTestCase[] = [ + { input: 20, unit: "kg/ha", output: 20 }, + { input: 20000, unit: "ton/ha", output: 20 }, + { input: 16, unit: "l/ha", density: 0.8, output: 20 }, + { input: 16000, unit: "m3/ha", density: 0.8, output: 20 }, + { input: 0, unit: "kg/ha", output: 0 }, + { input: 0, unit: "ton/ha", output: 0 }, + { input: 0, unit: "l/ha", density: 0.8, output: 0 }, + { input: 0, unit: "m3/ha", density: 0.8, output: 0 }, + { + input: 20, + unit: "l/ha", + density: undefined, + output: null, + }, + { + input: 20, + unit: "m3/ha", + density: undefined, + output: null, + }, + { + input: 20, + unit: "m3/ha", + density: 0, + output: null, + }, + { + input: 20, + unit: "m3/ha", + density: -1, + output: null, + }, + { + input: 20, + unit: "ft3/ha" as AppAmountUnit, + density: 2, + output: null, + }, + ] + + for (const { input, unit, density, output } of tests) { + it( + density !== undefined + ? `should convert kg/ha to ${unit} with density ${density} kg/l` + : `should convert kg/ha to ${unit} without density specified`, + () => { + const value = fromKgPerHa(input, unit, density) + expect(value).toBe(output) + }, + ) + } + + it("should accept input of type Decimal", () => { + expect(fromKgPerHa(new Decimal(20000), "m3/ha", new Decimal(2))).toBe( + 10, + ) + }) +}) diff --git a/fdm-core/src/fertilizer-application-unit-conversion.ts b/fdm-core/src/fertilizer-application-unit-conversion.ts new file mode 100644 index 000000000..bbe1d292d --- /dev/null +++ b/fdm-core/src/fertilizer-application-unit-conversion.ts @@ -0,0 +1,96 @@ +import Decimal from "decimal.js" + +export type AppAmountUnit = "kg/ha" | "l/ha" | "m3/ha" | "ton/ha" + +export const APP_AMOUNT_UNITS: { value: AppAmountUnit; label: string }[] = [ + { value: "kg/ha", label: "kg/ha" }, + { value: "l/ha", label: "l/ha" }, + { value: "m3/ha", label: "m³/ha" }, + { value: "ton/ha", label: "ton/ha" }, +] + +/** + * Convert a user-entered amount (in display unit) to kg/ha for storage. + * + * Uses Decimal.js to avoid floating-point rounding errors. + * Throws if conversion requires density but density is null/undefined/0. + * + * @param value The value to convert. + * @param unit The display unit of the value. + * @param density The density of the fertilizer in kg/l. Required for volume-based units. + * @returns The converted value in kg/ha. + * @alpha + */ +export function toKgPerHa( + value: number | Decimal | string, + unit: AppAmountUnit, + density?: number | Decimal | null, // kg/l +): number { + const d = new Decimal(value) + switch (unit) { + case "kg/ha": + return new Decimal(d).toNumber() + case "ton/ha": + return new Decimal(1000).times(d).toNumber() + case "l/ha": + if ( + density === null || + density === undefined || + new Decimal(0).greaterThanOrEqualTo(density) + ) + throw new Error( + "Positive density (p_density) is required for l/ha → kg/ha conversion", + ) + return new Decimal(density).times(d).toNumber() + case "m3/ha": + if ( + density === null || + density === undefined || + new Decimal(0).greaterThanOrEqualTo(density) + ) + throw new Error( + "Positive density (p_density) is required for m3/ha → kg/ha conversion", + ) + return new Decimal(1000).times(d).times(density).toNumber() + default: + throw new Error(`${unit} → kg/ha conversion is not supported`) + } +} + +/** + * Convert a stored kg/ha value back to the preferred display unit. + * + * Uses Decimal.js to avoid floating-point rounding errors. + * Returns null if conversion requires density but density is missing. + * + * @param valueKgPerHa The value in kg/ha to convert. + * @param unit The target display unit. + * @param density The density of the fertilizer in kg/l. Required for volume-based units. + * @returns The converted value in the target unit, or null if density is missing. + * @alpha + */ +export function fromKgPerHa( + valueKgPerHa: number | Decimal | string, + unit: AppAmountUnit, + density?: number | Decimal | null, // kg/l +): number | null { + const d = new Decimal(valueKgPerHa) + const densityNotProvided = + density === null || + typeof density === "undefined" || + new Decimal(0).greaterThanOrEqualTo(density) + switch (unit) { + case "kg/ha": + return d.toNumber() + case "ton/ha": + return d.dividedBy(1000).toNumber() + case "l/ha": + if (densityNotProvided) return null + return d.dividedBy(new Decimal(density)).toNumber() + case "m3/ha": + if (densityNotProvided) return null + return d.dividedBy(new Decimal(density).times(1000)).toNumber() + default: + return null + } +} \ No newline at end of file diff --git a/fdm-core/src/fertilizer.test.ts b/fdm-core/src/fertilizer.test.ts index 3636f4641..d580fbea6 100644 --- a/fdm-core/src/fertilizer.test.ts +++ b/fdm-core/src/fertilizer.test.ts @@ -108,6 +108,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: ["injection", "incorporation"], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -188,6 +189,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -266,6 +268,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -375,6 +378,7 @@ describe("Fertilizer Data Model", () => { randomAppMethod(), ]), ], + p_app_amount_unit: undefined, }) return fert as Parameters[3] } @@ -550,6 +554,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -650,6 +655,7 @@ describe("Fertilizer Data Model", () => { p_name_en: "Test Fertilizer (EN)", p_description: "This is a test fertilizer", p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -867,6 +873,7 @@ describe("Fertilizer Data Model", () => { p_name_en: "Test Fertilizer (EN) 2", p_description: "This is a test fertilizer 2", p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -939,6 +946,7 @@ describe("Fertilizer Data Model", () => { p_name_en: "RVO-mapped fertilizer (EN)", p_description: "This is a test fertilizer for RVO mapping", p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 100, p_density: 1, p_om: 0, @@ -1020,6 +1028,7 @@ describe("Fertilizer Data Model", () => { describe("Fertilizer Application", () => { let b_id: string let p_id: string + let p_id_liquid: string beforeAll(async () => { const farmName = "Test Farm" @@ -1071,6 +1080,7 @@ describe("Fertilizer Data Model", () => { p_name_en, p_description, p_app_method_options: [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, @@ -1129,13 +1139,83 @@ describe("Fertilizer Data Model", () => { p_acquiring_amount, p_acquiring_date, ) + + // Fertilizer whose application amount is given in volume per ha + const p_id_catalogue_liquid = await addFertilizerToCatalogue( + fdm, + principal_id, + b_id_farm, + { + p_name_nl, + p_name_en, + p_description, + p_app_method_options: [], + p_app_amount_unit: "l/ha", + p_dm: 37, + p_density: 1.2, + p_om: 20, + p_a: 30, + p_hc: 40, + p_eom: 50, + p_eoc: 60, + p_c_rt: 70, + p_c_of: 80, + p_c_if: 90, + p_c_fr: 100, + p_cn_of: 110, + p_n_rt: 120, + p_n_if: 130, + p_n_of: 140, + p_n_wc: 150, + p_no3_rt: 400, + p_nh4_rt: 410, + p_p_rt: 160, + p_k_rt: 170, + p_mg_rt: 180, + p_ca_rt: 190, + p_ne: 200, + p_s_rt: 210, + p_s_wc: 220, + p_cu_rt: 230, + p_zn_rt: 240, + p_na_rt: 250, + p_si_rt: 260, + p_b_rt: 270, + p_mn_rt: 280, + p_ni_rt: 290, + p_fe_rt: 300, + p_mo_rt: 310, + p_co_rt: 320, + p_as_rt: 330, + p_cd_rt: 340, + p_cr_rt: 350, + p_cr_vi: 360, + p_pb_rt: 370, + p_hg_rt: 380, + p_cl_rt: 390, + p_ef_nh3: 0.8, + p_type: "mineral", + p_type_rvo: "115", + }, + ) + + const p_acquiring_amount_liquid = 1000 + const p_acquiring_date_liquid = new Date() + p_id_liquid = await addFertilizer( + fdm, + principal_id, + p_id_catalogue_liquid, + b_id_farm, + p_acquiring_amount_liquid, + p_acquiring_date_liquid, + ) }) afterAll(async () => { // Clean up the database after each test (optional) }) - it("should add a new fertilizer application", async () => { + it("should add a new fertilizer application with no amount specified", async () => { const p_app_date = new Date("2024-03-15") const new_p_app_id = await addFertilizerApplication( @@ -1143,7 +1223,7 @@ describe("Fertilizer Data Model", () => { principal_id, b_id, p_id, - 100, + 0, "broadcasting", p_app_date, ) @@ -1156,7 +1236,60 @@ describe("Fertilizer Data Model", () => { ) expect(fertilizerApplication).toBeDefined() expect(fertilizerApplication?.p_id).toBe(p_id) - expect(fertilizerApplication?.p_app_amount).toBe(100) + expect(fertilizerApplication?.p_app_amount).toBe(0) + expect(fertilizerApplication?.p_app_method).toBe("broadcasting") + expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) + }) + + it("should add a new fertilizer application with amount specified", async () => { + const p_app_date = new Date("2024-03-15") + + const new_p_app_id = await addFertilizerApplication( + fdm, + principal_id, + b_id, + p_id_liquid, + 120, + "broadcasting", + p_app_date, + ) + expect(new_p_app_id).toBeDefined() + + const fertilizerApplication = await getFertilizerApplication( + fdm, + principal_id, + new_p_app_id, + ) + expect(fertilizerApplication).toBeDefined() + expect(fertilizerApplication?.p_id).toBe(p_id_liquid) + expect(fertilizerApplication?.p_app_amount).toBe(144) + expect(fertilizerApplication?.p_app_method).toBe("broadcasting") + expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) + }) + + it("should add a new fertilizer application with no amount specified", async () => { + const p_app_date = new Date("2024-03-15") + + const new_p_app_id = await addFertilizerApplication( + fdm, + principal_id, + b_id, + p_id_liquid, + 0, + "broadcasting", + p_app_date, + ) + expect(new_p_app_id).toBeDefined() + + const fertilizerApplication = await getFertilizerApplication( + fdm, + principal_id, + new_p_app_id, + ) + expect(fertilizerApplication).toBeDefined() + expect(fertilizerApplication?.p_id).toBe(p_id_liquid) + expect(fertilizerApplication?.p_app_amount).toBe(0) + expect(fertilizerApplication?.p_app_amount_display).toBe(0) expect(fertilizerApplication?.p_app_method).toBe("broadcasting") expect(fertilizerApplication?.p_app_date).toEqual(p_app_date) }) @@ -1195,6 +1328,40 @@ describe("Fertilizer Data Model", () => { expect(updatedApplication?.p_app_date).toEqual(p_app_date2) }) + it("should update a fertilizer application with amount specified in volume per ha", async () => { + const p_app_date1 = new Date("2024-03-15") + const p_app_date2 = new Date("2024-04-20") + + const p_app_id = await addFertilizerApplication( + fdm, + principal_id, + b_id, + p_id_liquid, + 100, + "broadcasting", + p_app_date1, + ) + + await updateFertilizerApplication( + fdm, + principal_id, + p_app_id, + p_id_liquid, + 200, + "injection", + p_app_date2, + ) + + const updatedApplication = await getFertilizerApplication( + fdm, + principal_id, + p_app_id, + ) + expect(updatedApplication?.p_app_amount).toBe(240) + expect(updatedApplication?.p_app_method).toBe("injection") + expect(updatedApplication?.p_app_date).toEqual(p_app_date2) + }) + it("should remove a fertilizer application", async () => { const new_p_app_id = await addFertilizerApplication( fdm, @@ -1368,7 +1535,7 @@ describe("Fertilizer Data Model", () => { describe("getFertilizerParametersDescription", () => { it("should return the correct fertilizer parameter descriptions for NL-nl locale", () => { const descriptions = getFertilizerParametersDescription("NL-nl") - expect(descriptions).toHaveLength(24) + expect(descriptions).toHaveLength(25) for (const description of descriptions) { expect(description).toHaveProperty("parameter") expect(description).toHaveProperty("unit") @@ -1393,7 +1560,7 @@ describe("getFertilizerParametersDescription", () => { it("should return the correct fertilizer parameter descriptions for default locale", () => { const descriptions = getFertilizerParametersDescription() - expect(descriptions).toHaveLength(24) + expect(descriptions).toHaveLength(25) for (const description of descriptions) { expect(description).toHaveProperty("parameter") expect(description).toHaveProperty("unit") @@ -1434,6 +1601,7 @@ describe("getFertilizerApplicationsForFarm", () => { p_name_en: "Test Fertilizer EN", p_description: "desc", p_app_method_options: [] as [], + p_app_amount_unit: undefined, p_dm: 37, p_density: 20, p_om: 20, diff --git a/fdm-core/src/fertilizer.ts b/fdm-core/src/fertilizer.ts index a32f35c13..78fea848c 100644 --- a/fdm-core/src/fertilizer.ts +++ b/fdm-core/src/fertilizer.ts @@ -12,11 +12,18 @@ import * as authZSchema from "./db/schema-authz" import { handleError } from "./error" import type { FdmType } from "./fdm.types" import type { + BaseFertilizerApplication, Fertilizer, FertilizerApplication, FertilizerCatalogue, FertilizerParameterDescription, } from "./fertilizer.types" +import { + APP_AMOUNT_UNITS, + type AppAmountUnit, + fromKgPerHa, + toKgPerHa, +} from "./fertilizer-application-unit-conversion" import { createId } from "./id" import type { Timeframe } from "./timeframe" @@ -131,6 +138,7 @@ export async function getFertilizersFromCatalogues( p_app_method_options: result.p_app_method_options as | ApplicationMethods[] | null, + p_app_amount_unit: result.p_app_amount_unit as AppAmountUnit, p_type: deriveFertilizerType(result), })) } catch (err) { @@ -160,6 +168,7 @@ export async function addFertilizerToCatalogue( p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] p_app_method_options: schema.fertilizersCatalogueTypeInsert["p_app_method_options"] + p_app_amount_unit: schema.fertilizersCatalogueTypeInsert["p_app_amount_unit"] p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] p_density: schema.fertilizersCatalogueTypeInsert["p_density"] p_om: schema.fertilizersCatalogueTypeInsert["p_om"] @@ -344,6 +353,8 @@ export async function getFertilizer( p_description: schema.fertilizersCatalogue.p_description, p_app_method_options: schema.fertilizersCatalogue.p_app_method_options, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, @@ -422,7 +433,9 @@ export async function getFertilizer( return { ...result, - p_type: deriveFertilizerType(result as Partial), + p_type: deriveFertilizerType( + result as Partial, + ), } as unknown as Fertilizer } catch (err) { throw handleError(err, "Exception for getFertilizer", { @@ -453,6 +466,7 @@ export async function updateFertilizerFromCatalogue( p_name_en: schema.fertilizersCatalogueTypeInsert["p_name_en"] p_description: schema.fertilizersCatalogueTypeInsert["p_description"] p_app_method_options: schema.fertilizersCatalogueTypeInsert["p_app_method_options"] + p_app_amount_unit: schema.fertilizersCatalogueTypeInsert["p_app_amount_unit"] p_dm: schema.fertilizersCatalogueTypeInsert["p_dm"] p_density: schema.fertilizersCatalogueTypeInsert["p_density"] p_om: schema.fertilizersCatalogueTypeInsert["p_om"] @@ -609,6 +623,8 @@ export async function getFertilizers( p_description: schema.fertilizersCatalogue.p_description, p_app_method_options: schema.fertilizersCatalogue.p_app_method_options, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, p_acquiring_amount: schema.fertilizerAcquiring.p_acquiring_amount, p_acquiring_date: schema.fertilizerAcquiring.p_acquiring_date, @@ -683,7 +699,9 @@ export async function getFertilizers( return fertilizers.map((f: (typeof fertilizers)[number]) => { return { ...f, - p_type: deriveFertilizerType(f as Partial), + p_type: deriveFertilizerType( + f as Partial, + ), } as unknown as Fertilizer }) } catch (err) { @@ -736,7 +754,7 @@ export async function removeFertilizer( * @param principal_id - The ID of the principal performing the operation. * @param b_id - The ID of the field where the fertilizer application is recorded. * @param p_id - The ID of the fertilizer to be applied. - * @param p_app_amount - The amount of fertilizer applied. + * @param p_app_amount_display - The amount of fertilizer applied in the display unit. * @param p_app_method - The method used for applying the fertilizer. * @param p_app_date - The date of the fertilizer application. * @returns A Promise that resolves with the unique ID of the newly created fertilizer application record. @@ -748,7 +766,7 @@ export async function addFertilizerApplication( principal_id: PrincipalId, b_id: schema.fertilizerApplicationTypeInsert["b_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: number, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -771,18 +789,17 @@ export async function addFertilizerApplication( throw new Error(`Field with b_id ${b_id} does not exist`) } - // Validate that the fertilizer exists - const fertilizerExists = await fdm - .select() - .from(schema.fertilizers) - .where(eq(schema.fertilizers.p_id, p_id)) - .limit(1) - if (fertilizerExists.length === 0) { - throw new Error(`Fertilizer with p_id ${p_id} does not exist`) - } + // Validate that the fertilizer exists and get it + const fertilizer = await getFertilizer(fdm, p_id) const p_app_id = createId() + const p_app_amount = toKgPerHa( + p_app_amount_display, + fertilizer.p_app_amount_unit, + fertilizer.p_density, + ) + await fdm.insert(schema.fertilizerApplication).values({ p_app_id, b_id, @@ -797,7 +814,7 @@ export async function addFertilizerApplication( throw handleError(err, "Exception for addFertilizerApplication", { b_id, p_id, - p_app_amount, + p_app_amount_display, p_app_method, p_app_date, }) @@ -811,7 +828,7 @@ export async function addFertilizerApplication( * @param principal_id - The ID of the principal performing the update. * @param p_app_id - The unique identifier of the fertilizer application record. * @param p_id - The unique identifier of the associated fertilizer. - * @param p_app_amount - The amount of fertilizer applied. + * @param p_app_amount_display - The amount of fertilizer applied in the display unit. * @param p_app_method - The method used for applying the fertilizer. * @param p_app_date - The date when the fertilizer was applied. * @@ -822,7 +839,7 @@ export async function updateFertilizerApplication( principal_id: PrincipalId, p_app_id: schema.fertilizerApplicationTypeInsert["p_app_id"], p_id: schema.fertilizerApplicationTypeInsert["p_id"], - p_app_amount: schema.fertilizerApplicationTypeInsert["p_app_amount"], + p_app_amount_display: number | undefined | null, p_app_method: schema.fertilizerApplicationTypeInsert["p_app_method"], p_app_date: schema.fertilizerApplicationTypeInsert["p_app_date"], ): Promise { @@ -835,6 +852,15 @@ export async function updateFertilizerApplication( principal_id, "updateFertilizerApplication", ) + const fertilizer = await getFertilizer(fdm, p_id) + const p_app_amount = + p_app_amount_display !== null && p_app_amount_display !== undefined + ? toKgPerHa( + p_app_amount_display, + fertilizer.p_app_amount_unit, + fertilizer.p_density, + ) + : undefined await fdm .update(schema.fertilizerApplication) .set({ p_id, p_app_amount, p_app_method, p_app_date }) @@ -843,7 +869,7 @@ export async function updateFertilizerApplication( throw handleError(err, "Exception for updateFertilizerApplication", { p_app_id, p_id, - p_app_amount, + p_app_amount_display, p_app_method, p_app_date, }) @@ -915,12 +941,15 @@ export async function getFertilizerApplication( "getFertilizerApplication", ) - const result = await fdm + const result = (await fdm .select({ p_id: schema.fertilizerApplication.p_id, p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_app_amount: schema.fertilizerApplication.p_app_amount, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, + p_density: schema.fertilizersCatalogue.p_density, p_app_method: schema.fertilizerApplication.p_app_method, p_app_date: schema.fertilizerApplication.p_app_date, p_app_id: schema.fertilizerApplication.p_app_id, @@ -940,9 +969,20 @@ export async function getFertilizerApplication( schema.fertilizerPicking.p_id_catalogue, ), ) - .where(eq(schema.fertilizerApplication.p_app_id, p_app_id)) + .where( + eq(schema.fertilizerApplication.p_app_id, p_app_id), + )) as (BaseFertilizerApplication & { + p_app_amount_unit: AppAmountUnit + p_density: number | null + })[] - return (result[0] || null) as FertilizerApplication | null + return result.length > 0 + ? extendFertilizerApplication( + result[0], + result[0].p_app_amount_unit, + result[0].p_density, + ) + : null } catch (err) { throw handleError(err, "Exception for getFertilizerApplication", { p_app_id, @@ -980,12 +1020,15 @@ export async function getFertilizerApplications( "getFertilizerApplications", ) - return await fdm + const results = (await fdm .select({ p_id: schema.fertilizerApplication.p_id, p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_app_amount: schema.fertilizerApplication.p_app_amount, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, + p_density: schema.fertilizersCatalogue.p_density, p_app_method: schema.fertilizerApplication.p_app_method, p_app_date: schema.fertilizerApplication.p_app_date, p_app_id: schema.fertilizerApplication.p_app_id, @@ -1024,7 +1067,20 @@ export async function getFertilizerApplications( ) : eq(schema.fertilizerApplication.b_id, b_id), ) - .orderBy(desc(schema.fertilizerApplication.p_app_date)) as FertilizerApplication[] + .orderBy( + desc(schema.fertilizerApplication.p_app_date), + )) as (BaseFertilizerApplication & { + p_app_amount_unit: AppAmountUnit + p_density: number | null + })[] + + return results.map((result) => + extendFertilizerApplication( + result, + result.p_app_amount_unit, + result.p_density, + ), + ) } catch (err) { throw handleError(err, "Exception for getFertilizerApplications", { b_id, @@ -1062,12 +1118,15 @@ export async function getFertilizerApplicationsForFarm( "getFertilizerApplicationsForFarm", ) - const rows = await fdm + const rows = (await fdm .select({ p_id: schema.fertilizerApplication.p_id, p_id_catalogue: schema.fertilizersCatalogue.p_id_catalogue, p_name_nl: schema.fertilizersCatalogue.p_name_nl, p_app_amount: schema.fertilizerApplication.p_app_amount, + p_app_amount_unit: + schema.fertilizersCatalogue.p_app_amount_unit, + p_density: schema.fertilizersCatalogue.p_density, p_app_method: schema.fertilizerApplication.p_app_method, p_app_date: schema.fertilizerApplication.p_app_date, p_app_id: schema.fertilizerApplication.p_app_id, @@ -1115,18 +1174,28 @@ export async function getFertilizerApplicationsForFarm( ) : eq(schema.fieldAcquiring.b_id_farm, b_id_farm), ) - .orderBy(desc(schema.fertilizerApplication.p_app_date)) + .orderBy( + desc(schema.fertilizerApplication.p_app_date), + )) as (BaseFertilizerApplication & { + b_id: schema.fertilizerApplicationTypeSelect["b_id"] + p_app_amount_unit: AppAmountUnit + p_density: schema.fertilizersCatalogueTypeSelect["p_density"] + })[] const result = new Map() for (const row of rows) { if (!row.b_id) continue // b_id is used for grouping only and is not part of FertilizerApplication - const { b_id, ...fertilizerApplication } = row - const existing = result.get(b_id) + const fertilizerApplication = extendFertilizerApplication( + row, + row.p_app_amount_unit, + row.p_density, + ) + const existing = result.get(row.b_id) if (existing) { existing.push(fertilizerApplication as FertilizerApplication) } else { - result.set(b_id, [ + result.set(row.b_id, [ fertilizerApplication as FertilizerApplication, ]) } @@ -1222,6 +1291,16 @@ export function getFertilizerParametersDescription( description: "Mestcode volgens RVO", options: schema.typeRvoOptions, }, + { + parameter: "p_app_amount_unit", + unit: "", + name: "Voorkeurseenheid", + type: "enum", + category: "general", + description: + "Eenheid voor het weergeven van de hoeveelheid van deze meststof", + options: APP_AMOUNT_UNITS, + }, { parameter: "p_app_method_options", unit: "", @@ -1545,3 +1624,37 @@ function deriveFertilizerType( } return null } + +/** + * Extends the given fertilizer application with computed data and removes unknown properties + * @param app fertilizer application + * @returns the same fertilizer application with p_app_amount_display filled in and properties + * that do not belong to FertilizerApplication removed + */ +function extendFertilizerApplication< + T extends BaseFertilizerApplication & { + p_app_amount_unit: AppAmountUnit + p_density: number | null + }, +>( + app: T, + p_app_amount_unit: AppAmountUnit, + p_density: number | null, +): FertilizerApplication { + const p_app_amount_display = + app.p_app_amount !== null && app.p_app_amount !== undefined + ? fromKgPerHa(app.p_app_amount, p_app_amount_unit, p_density) + : app.p_app_amount + + return { + p_id: app.p_id, + p_id_catalogue: app.p_id_catalogue, + p_name_nl: app.p_name_nl, + p_app_amount: app.p_app_amount, + p_app_amount_unit: p_app_amount_unit, + p_app_amount_display: p_app_amount_display, + p_app_method: app.p_app_method, + p_app_date: app.p_app_date, + p_app_id: app.p_app_id, + } +} diff --git a/fdm-core/src/fertilizer.types.d.ts b/fdm-core/src/fertilizer.types.d.ts index 8394ad66e..f4b4c6297 100644 --- a/fdm-core/src/fertilizer.types.d.ts +++ b/fdm-core/src/fertilizer.types.d.ts @@ -1,5 +1,6 @@ import type { ApplicationMethods } from "@nmi-agro/fdm-data" import type * as schema from "./db/schema" +import type { AppAmountUnit } from "./fertilizer-application-unit-conversion" export interface FertilizerCatalogue { p_id_catalogue: string @@ -8,6 +9,7 @@ export interface FertilizerCatalogue { p_name_en: string | null p_description: string | null p_app_method_options: ApplicationMethods[] | null + p_app_amount_unit: AppAmountUnit p_dm: number | null p_density: number | null p_om: number | null @@ -70,11 +72,18 @@ export interface FertilizerApplication { p_id_catalogue: string p_name_nl: string | null p_app_amount: number | null + p_app_amount_unit: AppAmountUnit + p_app_amount_display: number | null p_app_method: ApplicationMethods | null p_app_date: Date p_app_id: string } +export type BaseFertilizerApplication = Omit< + FertilizerApplication, + "p_app_amount_display" | "p_app_amount_unit" +> + export type FertilizerParameters = | "p_id_catalogue" | "p_source" @@ -82,6 +91,7 @@ export type FertilizerParameters = | "p_name_en" | "p_description" | "p_app_method_options" + | "p_app_amount_unit" | "p_dm" | "p_density" | "p_om" diff --git a/fdm-core/src/index.ts b/fdm-core/src/index.ts index 4f15945ca..49ccf50c1 100644 --- a/fdm-core/src/index.ts +++ b/fdm-core/src/index.ts @@ -114,6 +114,7 @@ export { updateFertilizerFromCatalogue, } from "./fertilizer" export type { + BaseFertilizerApplication, Fertilizer, FertilizerApplication, FertilizerCatalogue, @@ -121,6 +122,11 @@ export type { FertilizerParameterDescriptionItem, FertilizerParameters, } from "./fertilizer.types" +export type { AppAmountUnit } from "./fertilizer-application-unit-conversion" +export { + fromKgPerHa, + toKgPerHa, +} from "./fertilizer-application-unit-conversion" export { addField, getField, diff --git a/fdm-data/src/fertilizers/catalogues/baat.json b/fdm-data/src/fertilizers/catalogues/baat.json index 21e493a9f..643c745d0 100644 --- a/fdm-data/src/fertilizers/catalogues/baat.json +++ b/fdm-data/src/fertilizers/catalogues/baat.json @@ -36,6 +36,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -77,6 +78,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -118,6 +120,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -159,6 +162,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -200,6 +204,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -241,6 +246,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -282,6 +288,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -323,6 +330,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -364,6 +372,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -405,6 +414,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -446,6 +456,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -487,6 +498,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -528,6 +540,7 @@ "p_zn_rt": 42, "p_type_rvo": 110, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": true @@ -569,6 +582,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -610,6 +624,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -651,6 +666,7 @@ "p_zn_rt": 73, "p_type_rvo": 80, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -692,6 +708,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -733,6 +750,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -774,6 +792,7 @@ "p_zn_rt": 55, "p_type_rvo": 61, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -815,6 +834,7 @@ "p_zn_rt": 111, "p_type_rvo": 111, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": true @@ -856,6 +876,7 @@ "p_zn_rt": 38, "p_type_rvo": 111, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": true @@ -897,6 +918,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -938,6 +960,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -979,6 +1002,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1020,6 +1044,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1061,6 +1086,7 @@ "p_zn_rt": 167, "p_type_rvo": 23, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1102,6 +1128,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1143,6 +1170,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1184,6 +1212,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1225,6 +1254,7 @@ "p_zn_rt": 190, "p_type_rvo": 35, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1266,6 +1296,7 @@ "p_zn_rt": 81, "p_type_rvo": 90, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1307,6 +1338,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1348,6 +1380,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1389,6 +1422,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1430,6 +1464,7 @@ "p_zn_rt": null, "p_type_rvo": 120, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1471,6 +1506,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1512,6 +1548,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1553,6 +1590,7 @@ "p_zn_rt": 89, "p_type_rvo": 75, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1594,6 +1632,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1635,6 +1674,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1676,6 +1716,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1717,6 +1758,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1758,6 +1800,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1799,6 +1842,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1840,6 +1884,7 @@ "p_zn_rt": 57, "p_type_rvo": 25, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1881,6 +1926,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -1922,6 +1968,7 @@ "p_zn_rt": null, "p_type_rvo": 33, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -1963,6 +2010,7 @@ "p_zn_rt": 4, "p_type_rvo": 19, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2004,6 +2052,7 @@ "p_zn_rt": 38, "p_type_rvo": 10, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2045,6 +2094,7 @@ "p_zn_rt": 17, "p_type_rvo": 14, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2086,6 +2136,7 @@ "p_zn_rt": 5, "p_type_rvo": 12, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2127,6 +2178,7 @@ "p_zn_rt": null, "p_type_rvo": 13, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2168,6 +2220,7 @@ "p_zn_rt": 55, "p_type_rvo": 56, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2209,6 +2262,7 @@ "p_zn_rt": null, "p_type_rvo": 116, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2250,6 +2304,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2291,6 +2346,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2332,6 +2388,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2373,6 +2430,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2414,6 +2472,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "spoke wheel||spraying", + "p_app_amount_unit": "l/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2455,6 +2514,7 @@ "p_zn_rt": 248, "p_type_rvo": 40, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2496,6 +2556,7 @@ "p_zn_rt": null, "p_type_rvo": 43, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2537,6 +2598,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": false, "p_type_manure": false, "p_type_compost": false @@ -2578,6 +2640,7 @@ "p_zn_rt": 23, "p_type_rvo": 39, "p_app_method_options": "broadcasting||incorporation||incorporation 2 tracks", + "p_app_amount_unit": "ton/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2619,6 +2682,7 @@ "p_zn_rt": 89, "p_type_rvo": 50, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2660,6 +2724,7 @@ "p_zn_rt": 81, "p_type_rvo": 42, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2701,6 +2766,7 @@ "p_zn_rt": 4, "p_type_rvo": 18, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2742,6 +2808,7 @@ "p_zn_rt": 64, "p_type_rvo": 46, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2783,6 +2850,7 @@ "p_zn_rt": 10, "p_type_rvo": 42, "p_app_method_options": "slotted coulter||incorporation||incorporation 2 tracks||injection||shallow injection||narrowband", + "p_app_amount_unit": "m3/ha", "p_type_mineral": false, "p_type_manure": true, "p_type_compost": false @@ -2824,6 +2892,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false @@ -2865,6 +2934,7 @@ "p_zn_rt": null, "p_type_rvo": 115, "p_app_method_options": "broadcasting||pocket placement||incorporation", + "p_app_amount_unit": "kg/ha", "p_type_mineral": true, "p_type_manure": false, "p_type_compost": false diff --git a/fdm-data/src/fertilizers/catalogues/baat.ts b/fdm-data/src/fertilizers/catalogues/baat.ts index d3e255726..ff6df0d2a 100644 --- a/fdm-data/src/fertilizers/catalogues/baat.ts +++ b/fdm-data/src/fertilizers/catalogues/baat.ts @@ -1,5 +1,6 @@ import type { ApplicationMethods, + ApplicationUnits, CatalogueFertilizer, CatalogueFertilizerItem, } from "../d" @@ -30,6 +31,10 @@ export async function getCatalogueBaat(): Promise { : (fertilizer.p_app_method_options.split( "||", ) as ApplicationMethods[]), + p_app_amount_unit: + fertilizer.p_app_amount_unit === undefined + ? "kg/ha" + : (fertilizer.p_app_amount_unit as ApplicationUnits), p_ef_nh3: fertilizer.p_ef_nh3 === undefined ? null : fertilizer.p_ef_nh3, p_dm: fertilizer.p_dm === undefined ? null : fertilizer.p_dm, diff --git a/fdm-data/src/fertilizers/catalogues/srm.ts b/fdm-data/src/fertilizers/catalogues/srm.ts index 48fd7f0e7..65fb6b3c7 100644 --- a/fdm-data/src/fertilizers/catalogues/srm.ts +++ b/fdm-data/src/fertilizers/catalogues/srm.ts @@ -1,5 +1,6 @@ import type { ApplicationMethods, + ApplicationUnits, CatalogueFertilizer, CatalogueFertilizerItem, } from "../d" @@ -30,6 +31,11 @@ export async function getCatalogueSrm(): Promise { : (fertilizer.p_app_method_options.split( "||", ) as ApplicationMethods[]), + p_app_amount_unit: + (fertilizer as any).p_app_amount_unit === undefined + ? "kg/ha" + : ((fertilizer as any) + .p_app_amount_unit as ApplicationUnits), p_dm: fertilizer.p_dm === undefined ? null : fertilizer.p_dm, p_density: fertilizer.p_density === undefined diff --git a/fdm-data/src/fertilizers/d.ts b/fdm-data/src/fertilizers/d.ts index 7b1df1d6b..fdceb9a34 100644 --- a/fdm-data/src/fertilizers/d.ts +++ b/fdm-data/src/fertilizers/d.ts @@ -12,6 +12,8 @@ export type ApplicationMethods = | "pocket placement" | "narrowband" +export type ApplicationUnits = "kg/ha" | "ton/ha" | "l/ha" | "m3/ha" + export interface CatalogueFertilizerItem { p_source: CatalogueFertilizerName | string p_id_catalogue: string @@ -19,6 +21,7 @@ export interface CatalogueFertilizerItem { p_name_en?: string | null | undefined p_description?: string | null | undefined p_app_method_options?: ApplicationMethods[] | null | undefined + p_app_amount_unit?: ApplicationUnits | null | undefined p_ef_nh3?: number | null p_dm?: number | null p_density?: number | null