Jump to content
Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Since 7 October 2026 every request to landrace.wiki passes through Cloudflare, a security service brought in after a denial-of-service attack made the site unreachable. The Privacy policy now explains what this means for your data.

This is a documentation subpage for Seed lot. It contains usage information, categories, and other content that is not part of the original template.

Module:Seed lot records seed lots and shows their readings. A seed lot is one holder's physical holding of one accession's seed at one storage site. A lot is recorded in one of two ways, and both store the same properties, so a query reads every lot whichever way it was made:

  • the holder's main lot, a subobject of the accession page named lot (ZOM-…#lot),
 built by Template:Accession from the accession's own fields: seed_storage_location is the holder,
 seed_quantity the lot's first, undated, inventory report, and the lot_ fields the rest.
 Only an accession whose collection method is Seeds, or is not recorded, gets one: a herbarium specimen, a tissue
 sample or any other method never does;
  • every other lot on its own page, Lot:<accession ID>/<site code>, through
 Template:Seed lot and Form:Seed lot: backups, portions passed on, and any holder's seed recorded apart
 from the accession page.

Inventory reports and germination tests are written as Template:Lot inventory and Template:Lot germination test calls inside the lot's report fields. Each call only hands its fields to this module; the module stores each report as a subobject naming its lot (Property:Is report on) and typed by Property:Has report type, so the lot and its reports are written in one place.

Readings

The reading of each lot comes from Module:Grow-out priority. The lot and its reports are taken from the page's own arguments, so a save shows the new reading at once; the lineage, the population and the population's other lots come from the store. The reading is stored on the lot (Property:Has seed lot step, Property:Has seed lot factors, Property:Has lot action, Property:Is queued for sowing, Property:Is small sowing, Property:Is flagged for preservation grow-out, Property:Has method version). On a field collection in a population the accession page also stores Property:Has collection step.

A reading depends on other pages (the population page, other lots, the lineage), and on the date: a test falls due with no edit. Each one says the date it was worked out. A page whose inputs changed elsewhere shows its old reading until it is parsed again.

Entry points

  • {{#invoke:Seed lot|primary|page={{FULLPAGENAME}}|collection_date_iso=…}}: in
 Template:Accession. Stores the main lot and its reports, and returns the infobox rows for every lot of the
 accession; every other field is read from Template:Accession's own arguments.
  • {{#invoke:Seed lot|page}}: Template:Seed lot. Stores a lot page and shows it.
  • {{#invoke:Seed lot|report|kind=inventory}} and kind=germination test:
 Template:Lot inventory and Template:Lot germination test.
  • {{#invoke:Seed lot|pagename|accession=…|site=…}}: Form:Seed lot's page name,
 numbered (2), (3) … when a page of that name exists.

Fields

Every field is described on Template:Seed lot (lot fields), Template:Lot inventory and Template:Lot germination test (reports), and Template:Accession (the main lot's lot_ fields and the lineage facts). Empty means not known, everywhere; nothing is filled in for a missing value.


-- Module:Seed lot
-- The seed lot record: one holder's physical holding of one accession's seed at one storage site.
-- A lot is recorded in one of two ways, with the same properties:
--   * the holder's main lot, as a subobject (#lot) on the accession page, built by Template:Accession
--     from the accession's own fields (entry point `primary`);
--   * every other lot (backups, portions passed on, any holder who does not edit the accession page)
--     on its own page, Lot:<accession ID>/<site code>, through Template:Seed lot (entry point `page`).
-- Inventory reports and germination tests are given as {{Lot inventory}} and {{Lot germination test}}
-- calls inside the lot's inventory and germination_tests fields. Each call only hands its fields to
-- this module (entry point `report`); this module stores every report as a subobject naming its lot
-- (Is report on), so the lot and its reports are written in one place.
-- The reading of each lot (Module:Grow-out priority) is worked out from the page's own arguments, so a
-- save shows the new reading at once; the lineage, the population and the population's other lots
-- come from the store.

local GOP = require('Module:Grow-out priority')
local R = GOP._read

local p = {}

local STYLES = 'Template:Grow-out priority/styles.css'
local PRIMARY = 'lot' -- subobject name of the main lot on an accession page

------------------------------------------------------------------------------------------
-- Field tables
------------------------------------------------------------------------------------------

-- Lot fields: parameter on a Lot: page, parameter on an accession page, property, engine key.
local LOT_FIELDS = {
	{ 'accession', nil, 'Is lot of', 'accession' },
	{ 'holder', nil, 'Has lot holder', 'holder' },
	{ 'lot_role', nil, 'Has lot role', 'lot_role' },
	{ 'duplicate_of', nil, 'Is safety duplicate of', 'duplicate_of', lotRef = true },
	{ 'site', 'lot_site', 'Has storage site', 'storage_site' },
	{ 'site_country', 'lot_site_country', 'Has storage country', 'storage_country' },
	{ 'backup_distance', 'lot_backup_distance', 'Has backup distance', 'backup_distance' },
	{ 'distance_measured_from', 'lot_distance_measured_from', 'Has distance measured from', 'distance_measured_from', lotRef = true },
	{ 'received_date', 'lot_received_date', 'Has lot received date', 'received_date' },
	{ 'original_kept', 'lot_original_kept', 'Has original sample kept', 'original_kept' },
	{ 'handed_out', 'lot_handed_out', 'Is handed out', 'handed_out', multi = true },
	{ 'backup_status', 'lot_backup_status', 'Has backup status', 'backup_status' },
	{ 'lot_status', 'lot_status', 'Has lot status', 'lot_status' },
	{ 'lot_status_date', 'lot_status_date', 'Has lot status date', 'lot_status_date' },
	{ 'gone_reason', 'lot_gone_reason', 'Has gone reason', 'gone_reason' },
	{ 'lot_source', 'lot_source', 'Has lot source', 'lot_source' },
}

-- Report fields: parameter, property, engine key.
local REPORTS = {
	inventory = {
		type = 'inventory', prefix = 'inventory',
		fields = {
			{ 'date', 'Has report date', 'date' },
			{ 'quantity', 'Has seed count', 'quantity' },
			{ 'unit', 'Has seed count unit', 'quantity_unit' },
			{ 'thousand_seed_weight', 'Has thousand seed weight', 'thousand_seed_weight' },
			{ 'storage', 'Has storage regime', 'storage' },
			{ 'storage_temperature', 'Has storage temperature', 'storage_temperature' },
			{ 'seed_moisture', 'Has seed moisture', 'seed_moisture' },
			{ 'moisture_basis', 'Has seed moisture basis', 'moisture_basis' },
			{ 'reported_by', 'Has reported by', 'reported_by' },
			{ 'source', 'Has report source', 'source' },
		},
	},
	['germination test'] = {
		type = 'germination test', prefix = 'test',
		fields = {
			{ 'date', 'Has test date', 'date' },
			{ 'seeds_tested', 'Has seeds tested', 'seeds_tested' },
			{ 'seedlings', 'Has seedlings counted', 'seedlings' },
			{ 'percent', 'Has germination percent', 'percent' },
			{ 'basis', 'Has germination basis', 'basis' },
			{ 'method', 'Has germination method', 'method' },
			{ 'method_note', 'Has germination method note', 'method_note' },
			{ 'dormancy_check', 'Has dormancy check', 'dormancy_check' },
			{ 'days_after_harvest', 'Has days after harvest', 'days_after_harvest' },
			{ 'reported_by', 'Has reported by', 'reported_by' },
			{ 'source', 'Has report source', 'source' },
		},
	},
}

local ROLE_LABEL = { main = 'Main lot', ['safety duplicate'] = 'Backup', ['distributed portion'] = 'Portion passed on' }

-- Only seed is held as a lot: an accession whose stored collection method is Seeds, or is not recorded,
-- gets a main lot; a herbarium specimen, a tissue sample or any other method never does.
local LOT_METHODS = { seeds = true, [''] = true }

------------------------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------------------------

local function trim(s)
	if s == nil then return nil end
	s = mw.text.trim(tostring(s))
	if s == '' then return nil end
	return s
end

local function splitList(s, sep)
	local out = {}
	if not trim(s) then return out end
	for _, v in ipairs(mw.text.split(s, sep or ',', true)) do
		v = trim(v)
		if v then out[#out + 1] = v end
	end
	return out
end

local function num(v)
	v = trim(v)
	if not v then return nil end
	return tonumber((v:gsub(',', '')))
end

-- A lot named in a field: a Lot: page, a lot subobject (Page#lot), or an accession page, which means
-- that accession's main lot.
local function lotRef(v)
	v = trim(v)
	if not v then return nil end
	v = v:gsub('^%[%[', ''):gsub('%]%]$', ''):gsub('|.*$', '')
	v = trim(v)
	if not v then return nil end
	if v:find('#', 1, true) or v:match('^[Ll]ot:') then return v end
	return v .. '#' .. PRIMARY
end
p._lotRef = lotRef

local function link(title, label)
	local t = mw.title.new(title)
	if t and t.exists then return '[[' .. title .. (label and ('|' .. label) or '') .. ']]' end
	return label or title
end

-- A link to a lot: a Lot: page, or an accession's main lot (shown as "<accession> (main lot)").
local function lotLink(ref)
	if not ref then return nil end
	local page, sub = ref:match('^(.-)#(.*)$')
	if page and sub == PRIMARY then return '[[' .. page .. ']] (main lot)' end
	return '[[' .. ref .. ']]'
end

local function stepSpan(step)
	return '<span class="gop-step gop-step--' .. step:lower():gsub('%s+', '-') .. '">' .. step .. '</span>'
end

------------------------------------------------------------------------------------------
-- Reports: {{Lot inventory}} and {{Lot germination test}} hand their fields over as a hidden token
------------------------------------------------------------------------------------------

-- {{#invoke:Seed lot|report|kind=inventory}} or kind=germination test; the fields come from the
-- calling template's arguments.
function p.report(frame)
	local kind = trim(frame.args.kind)
	local spec = kind and REPORTS[kind]
	if not spec then return '<strong class="error">Seed lot report: kind must be inventory or germination test.</strong>' end
	local parent = frame:getParent()
	local args = parent and parent.args or {}
	local data = { kind = kind }
	for _, f in ipairs(spec.fields) do data[f[1]] = trim(args[f[1]]) end
	local enc = mw.text.encode(mw.text.jsonEncode(data))
	return '<span class="lw-lot-report" style="display:none" data-lot-report="' .. enc .. '"></span>'
end

-- The reports handed over in one field, in the order written.
local function readReports(text)
	local out = { inventory = {}, ['germination test'] = {} }
	if not trim(text) then return out end
	for enc in tostring(text):gmatch('data%-lot%-report="([^"]*)"') do
		local ok, data = pcall(mw.text.jsonDecode, mw.text.decode(enc))
		if ok and type(data) == 'table' and out[data.kind] then table.insert(out[data.kind], data) end
	end
	return out
end
p._readReports = readReports

-- A germination test's percentage: worked out from the counts when both are given; a typed
-- percentage alone is kept as reported and marked as such.
local function percentOf(t)
	local n, s = num(t.seeds_tested), num(t.seedlings)
	if n and s and n > 0 then return tostring(math.floor(s / n * 1000 + 0.5) / 10), false end
	if num(t.percent) then return trim(t.percent), true end
	return nil, nil
end

------------------------------------------------------------------------------------------
-- Building a lot from arguments
------------------------------------------------------------------------------------------

-- fields: parameter -> value; reports: {inventory = {...}, ['germination test'] = {...}}.
local function buildLot(id, fields, reports)
	local lot = { id = id, inventory = {}, tests = {} }
	for _, f in ipairs(LOT_FIELDS) do
		local v = fields[f[1]]
		if f.multi then
			lot[f[4]] = splitList(v)
		elseif f.lotRef then
			lot[f[4]] = lotRef(v)
		else
			lot[f[4]] = trim(v)
		end
	end
	for _, r in ipairs(reports.inventory) do
		local e = {}
		for _, f in ipairs(REPORTS.inventory.fields) do e[f[3]] = trim(r[f[1]]) end
		if not e.quantity_unit and e.quantity then e.quantity_unit = 'seeds' end
		table.insert(lot.inventory, e)
	end
	for _, r in ipairs(reports['germination test']) do
		local e = {}
		for _, f in ipairs(REPORTS['germination test'].fields) do e[f[3]] = trim(r[f[1]]) end
		e.percent = percentOf(r)
		table.insert(lot.tests, e)
	end
	lot.reports = reports
	return lot
end

-- The accession's lineage facts, from the accession's own arguments.
local function accessionFromArgs(page, a)
	return {
		id = page,
		parents = splitList(a.parent_accession),
		collection = splitList(a.collection),
		generation = trim(a.regrow_generation),
		regrow_isolation = trim(a.regrow_isolation),
		parent_females = trim(a.parent_females),
		parent_males = trim(a.parent_males),
		parent_total = trim(a.parent_total),
		population = trim(a.population),
		copy_status = trim(a.copy_status),
		collection_date = trim(a.collection_date_iso),
		introgression_at_collection = trim(a.perceived_introgression_level),
		classification = trim(a.classification),
	}
end

-- For the fixtures (tests run the builders on data they make, with no store).
p._buildLot, p._accessionFromArgs = buildLot, accessionFromArgs

------------------------------------------------------------------------------------------
-- Data from the store
------------------------------------------------------------------------------------------

-- Everything the reading of `lot` needs besides the lot itself: its accession and lineage, its
-- population with the population's accessions and lots, and the other lots of the same accession.
-- `acc` is the accession built from the page's own arguments (on an accession page) or nil.
local function loadData(frame, lot, acc, asof)
	local data = { lots = {}, accessions = {}, populations = {} }
	local accId = lot.accession
	if not acc and accId then
		R.accessions(frame, data, '[[' .. accId .. ']]')
		acc = data.accessions[accId]
	end
	local popId = acc and acc.population
	if popId then
		R.population(frame, data, popId, asof)
	end
	if accId and not (popId and data.populations[popId]) then
		-- Not in a population: the other lots of this accession, and their reports.
		for _, row in ipairs(R.ask(frame, '[[' .. R.LOT_PRINTOUTS[1][2] .. '::' .. accId .. ']]', R.LOT_PRINTOUTS)) do
			data.lots[row.subject] = data.lots[row.subject] or R.lotFromRow(row)
		end
		R.reports(frame, data, '[[Is report on.Is lot of::' .. accId .. ']]')
	end
	-- The page's own arguments win over the store's copy of the last save.
	if acc then data.accessions[acc.id] = acc end
	data.lots[lot.id] = lot
	if accId then R.ancestors(frame, data, accId) end
	return data
end

------------------------------------------------------------------------------------------
-- Storing
------------------------------------------------------------------------------------------

local function propArgs(list, prop, v)
	if type(v) == 'table' then
		if #v > 0 then
			list[#list + 1] = prop .. '=' .. table.concat(v, ';')
			list[#list + 1] = '+sep=;'
		end
	elseif trim(v) then
		list[#list + 1] = prop .. '=' .. trim(v)
	end
end

-- The lot's own fields plus its reading, as Prop=value arguments for #set or #subobject.
local function lotArgs(lot, reading)
	local out = {}
	for _, f in ipairs(LOT_FIELDS) do propArgs(out, f[3], lot[f[4]]) end
	if reading then
		local values = GOP.lotValues(reading)
		local props = {}
		for prop in pairs(values) do props[#props + 1] = prop end
		table.sort(props)
		for _, prop in ipairs(props) do propArgs(out, prop, values[prop]) end
	end
	return out
end

-- One subobject per report, each naming its lot.
local function storeReports(frame, lot, namePrefix)
	local out = {}
	for kind, spec in pairs(REPORTS) do
		for i, r in ipairs(lot.reports[kind] or {}) do
			local args = { namePrefix .. spec.prefix .. '-' .. i, 'Is report on=' .. lot.id, 'Has report type=' .. spec.type }
			for _, f in ipairs(spec.fields) do
				local v = r[f[1]]
				if f[1] == 'percent' then
					local pc, only = percentOf(r)
					v = pc
					if only ~= nil then args[#args + 1] = 'Has percentage only=' .. (only and 'true' or 'false') end
				end
				if f[1] == 'unit' and not trim(v) and trim(r.quantity) then v = 'seeds' end
				propArgs(args, f[2], v)
			end
			out[#out + 1] = frame:callParserFunction('#subobject', args)
		end
	end
	return table.concat(out)
end

------------------------------------------------------------------------------------------
-- Display
------------------------------------------------------------------------------------------

-- Short reading: the step and the factors behind it, worst first.
local function shortReading(r)
	if r.error then return "''" .. r.error .. "''" end
	if r.gone then return r.goneText end
	local t = 'Seed lot: ' .. stepSpan(r.step)
	if #r.reasons > 0 then
		t = t .. '<br /><small>' .. table.concat(r.reasons, ', ') .. (r.irreplaceable and ' (irreplaceable)' or '') .. '</small>'
	elseif r.step == GOP.STEP.dd and #r.missing > 0 then
		t = t .. '<br /><small>not known: ' .. table.concat(r.missing, ', ') .. '</small>'
	end
	if r.existsUnknown then t = t .. '<br /><small>not known whether it still exists (last report ' .. r.lastReport .. ')</small>' end
	return t
end

local function fmtReport(kind, e)
	local bits = {}
	if kind == 'inventory' then
		if e.quantity then bits[#bits + 1] = e.quantity .. ' ' .. (e.unit or e.quantity_unit or 'seeds') end
		if e.storage then bits[#bits + 1] = e.storage end
		if e.storage_temperature then bits[#bits + 1] = e.storage_temperature .. ' °C' end
		if e.seed_moisture then bits[#bits + 1] = e.seed_moisture .. '% moisture' .. (e.moisture_basis and (' (' .. e.moisture_basis .. ')') or '') end
	else
		local pc, only = percentOf(e)
		if num(e.seeds_tested) and num(e.seedlings) then
			bits[#bits + 1] = e.seedlings .. ' seedlings from ' .. e.seeds_tested .. ' seeds (' .. pc .. '%)'
		elseif pc then
			bits[#bits + 1] = pc .. '% (percentage only)'
		end
		if e.basis and e.basis ~= 'germination test' then bits[#bits + 1] = e.basis .. (e.basis == 'prediction' and ' (predicted from age and storage)' or '') end
		if e.method then bits[#bits + 1] = e.method .. (e.method_note and (': ' .. e.method_note) or '') end
		if e.dormancy_check and e.dormancy_check ~= 'none' then bits[#bits + 1] = e.dormancy_check end
		if e.days_after_harvest then bits[#bits + 1] = e.days_after_harvest .. ' days after harvest' end
	end
	local who = {}
	if e.reported_by then who[#who + 1] = 'reported by ' .. e.reported_by end
	if e.source then who[#who + 1] = e.source end
	return (e.date or 'undated') .. ': ' .. table.concat(bits, ', ') .. (#who > 0 and (' <small>(' .. table.concat(who, '; ') .. ')</small>') or '')
end

local function reportList(lot)
	local o = {}
	for _, kind in ipairs({ 'inventory', 'germination test' }) do
		local list = lot.reports and lot.reports[kind] or {}
		if #list > 0 then
			o[#o + 1] = "'''" .. (kind == 'inventory' and 'Inventory' or 'Germination tests') .. "'''"
			for _, e in ipairs(list) do o[#o + 1] = '* ' .. fmtReport(kind, e) end
		end
	end
	return table.concat(o, '\n')
end

-- The full reading of a lot (Module:Grow-out priority's own wording), with its reports.
local function details(r, lot)
	-- The step itself is already shown above the details.
	local body = GOP.renderLot(r):gsub('^Seed lot: <span class="[^"]*">[^<]*</span>', '', 1):gsub('^<br />', '', 1)
	local rep = reportList(lot)
	if rep ~= '' then body = body .. '\n' .. rep end
	return '<div class="mw-collapsible mw-collapsed lw-lot-details" data-expandtext="details" data-collapsetext="hide">\n'
		.. body .. '\n</div>'
end

local function lotLabel(lot)
	return ROLE_LABEL[(lot.lot_role or ''):lower()] or 'Seed lot'
end

-- One infobox row for a lot: its role as the label (linked to its page when it has one), then the
-- holder and the reading.
local function lotRow(r, lot, isPrimary)
	-- The main lot's row carries the anchor its subobject name points to (Page#lot).
	local label = isPrimary and ('<span id="' .. PRIMARY .. '"></span>' .. lotLabel(lot))
		or ('[[' .. lot.id .. '|' .. lotLabel(lot) .. ']]')
	local holder = lot.holder and link(lot.holder) or "''holder not named''"
	return '! class="lw-infobox__label" | ' .. label .. '\n| class="lw-infobox__value" | ' .. holder .. '<br />'
		.. shortReading(r) .. '\n' .. details(r, lot) .. '\n|-'
end

------------------------------------------------------------------------------------------
-- Entry points
------------------------------------------------------------------------------------------

local function asof() return os.date('!%Y-%m-%d') end

-- Every seed lot of an accession, for its infobox, storing the main lot.
-- In Template:Accession: {{#invoke:Seed lot|primary|page={{FULLPAGENAME}}|collection_date_iso=…}};
-- every other field is read from Template:Accession's own arguments.
function p.primary(frame)
	local a = {}
	local parent = frame:getParent()
	if parent then for k, v in pairs(parent.args) do a[k] = v end end
	for k, v in pairs(frame.args) do a[k] = v end
	local page = trim(a.page) or mw.title.getCurrentTitle().prefixedText
	local holder = trim(a.seed_storage_location)
	local method = (trim(a.collection_method) or ''):lower()
	local out, rows = {}, {}
	local acc = accessionFromArgs(page, a)
	local mainLot, data
	if holder and LOT_METHODS[method] then
		local fields = { accession = page, holder = holder, lot_role = 'main' }
		for _, f in ipairs(LOT_FIELDS) do
			if f[2] then fields[f[1]] = a[f[2]] end
		end
		local reports = readReports(a.lot_inventory)
		reports['germination test'] = readReports(a.lot_germination_tests)['germination test']
		-- The holder's seed count on the accession is the lot's first, undated, inventory report.
		if num(a.seed_quantity) then
			table.insert(reports.inventory, 1, { kind = 'inventory', quantity = trim(a.seed_quantity), unit = 'seeds' })
		end
		mainLot = buildLot(page .. '#' .. PRIMARY, fields, reports)
	end
	-- Lots on their own pages.
	local pageLots = {}
	if mainLot then
		data = loadData(frame, mainLot, acc, asof())
	else
		data = { lots = {}, accessions = {}, populations = {} }
		for _, row in ipairs(R.ask(frame, '[[Is lot of::' .. page .. ']]', R.LOT_PRINTOUTS)) do
			data.lots[row.subject] = R.lotFromRow(row)
		end
		if next(data.lots) then
			if acc.population then R.population(frame, data, acc.population, asof()) end
			R.reports(frame, data, '[[Is report on.Is lot of::' .. page .. ']]')
			data.accessions[page] = acc
			R.ancestors(frame, data, page)
		end
	end
	for id, lot in pairs(data.lots) do
		if id ~= (mainLot and mainLot.id) and lot.accession == page then pageLots[#pageLots + 1] = id end
	end
	table.sort(pageLots)
	local ctx = {}
	if mainLot then
		local r = GOP.evaluateLot(mainLot.id, data, ctx)
		local args = lotArgs(mainLot, r)
		table.insert(args, 1, PRIMARY)
		out[#out + 1] = frame:callParserFunction('#subobject', args)
		out[#out + 1] = storeReports(frame, mainLot, PRIMARY .. '-')
		rows[#rows + 1] = lotRow(r, mainLot, true)
	end
	for _, id in ipairs(pageLots) do
		local lot = data.lots[id]
		lot.reports = { inventory = lot.inventory, ['germination test'] = lot.tests }
		rows[#rows + 1] = lotRow(GOP.evaluateLot(id, data, ctx), lot, false)
	end
	-- The collection's step (on a collection in a population): the best of its verified copies.
	if acc.population and data.populations[acc.population] and #acc.parents == 0 then
		local pr = GOP.evaluatePopulation(acc.population, data, ctx)
		for _, c in ipairs(pr.holdings.collections) do
			if c.id == page and c.step then
				out[#out + 1] = frame:callParserFunction('#set', { 'Has collection step=' .. c.step })
			end
		end
	end
	if #rows > 0 then
		rows[#rows] = rows[#rows]:gsub('\n|%-$', function()
			return frame:extensionTag('templatestyles', '', { src = STYLES }) .. '\n|-'
		end)
	end
	return table.concat(out) .. table.concat(rows, '\n')
end

-- Infobox rows for a lot page.
local function infoboxRows(lot, fields)
	local o = {}
	local function row(label, value)
		if value and value ~= '' then
			o[#o + 1] = '|-\n! class="lw-infobox__label" | ' .. label .. '\n| class="lw-infobox__value" | ' .. value
		end
	end
	row('Accession', lot.accession and ('[[' .. lot.accession .. ']]') or "''not named''")
	row('Holder', lot.holder and link(lot.holder))
	row('Role', lot.lot_role)
	row('Backs up', lotLink(lot.duplicate_of))
	row('Storage site', lot.storage_site)
	row('Country', lot.storage_country and ('[[' .. lot.storage_country .. ']]'))
	if lot.backup_distance then
		local from = lotLink(lot.distance_measured_from)
		row('Distance', lot.backup_distance .. (from and (' from ' .. from) or ''))
	end
	row('Received', lot.received_date)
	row('Original sample kept', lot.original_kept and (lot.original_kept .. ' seeds'))
	row('Handed out', #lot.handed_out > 0 and table.concat(lot.handed_out, ', ') or nil)
	row('Backup', lot.backup_status)
	if lot.lot_status then
		row('Status', lot.lot_status .. (lot.gone_reason and (' (' .. lot.gone_reason .. ')') or '')
			.. (lot.lot_status_date and (', ' .. lot.lot_status_date) or ''))
	end
	row('Figures from', lot.lot_source)
	return table.concat(o, '\n')
end

local function reportTable(lot, kind)
	local list = lot.reports[kind] or {}
	if #list == 0 then return nil end
	local t
	if kind == 'inventory' then
		t = { '{| class="wikitable"', '! Date !! Seeds on hand !! Storage !! Temperature !! Moisture !! Reported by !! Source' }
		for _, e in ipairs(list) do
			t[#t + 1] = '|-\n| ' .. (e.date or "''undated''") .. ' || ' .. (e.quantity and (e.quantity .. ' ' .. (e.unit or 'seeds')) or '')
				.. (e.thousand_seed_weight and (' <small>(1,000 seeds = ' .. e.thousand_seed_weight .. ' g)</small>') or '')
				.. ' || ' .. (e.storage or '') .. ' || ' .. (e.storage_temperature and (e.storage_temperature .. ' °C') or '')
				.. ' || ' .. (e.seed_moisture and (e.seed_moisture .. '%' .. (e.moisture_basis and (' (' .. e.moisture_basis .. ')') or '')) or '')
				.. ' || ' .. (e.reported_by or '') .. ' || ' .. (e.source or '')
		end
	else
		t = { '{| class="wikitable"', '! Date !! Seeds tested !! Seedlings !! Germination !! Basis !! Method !! Dormancy check !! Days after harvest !! Reported by !! Source' }
		for _, e in ipairs(list) do
			local pc, only = percentOf(e)
			t[#t + 1] = '|-\n| ' .. (e.date or "''undated''") .. ' || ' .. (e.seeds_tested or '') .. ' || ' .. (e.seedlings or '')
				.. ' || ' .. (pc and (pc .. '%' .. (only and ' <small>(percentage only; counts needed)</small>' or '')) or '')
				.. ' || ' .. (e.basis or '') .. (e.basis == 'prediction' and ' <small>(predicted from age and storage)</small>' or '')
				.. ' || ' .. (e.method or '') .. (e.method_note and (': ' .. e.method_note) or '')
				.. ' || ' .. (e.dormancy_check or '') .. ' || ' .. (e.days_after_harvest or '')
				.. ' || ' .. (e.reported_by or '') .. ' || ' .. (e.source or '')
		end
	end
	t[#t + 1] = '|}'
	return table.concat(t, '\n')
end

-- A lot on its own page: {{Seed lot|accession=|holder=|…}} (Template:Seed lot).
function p.page(frame)
	local parent = frame:getParent()
	local a = parent and parent.args or frame.args
	local title = trim(frame.args.page) or mw.title.getCurrentTitle().prefixedText
	local fields = {}
	for _, f in ipairs(LOT_FIELDS) do fields[f[1]] = a[f[1]] end
	local reports = readReports(a.inventory)
	reports['germination test'] = readReports(a.germination_tests)['germination test']
	local lot = buildLot(title, fields, reports)
	local out = {}
	local r
	if lot.accession then
		local data = loadData(frame, lot, nil, asof())
		r = GOP.evaluateLot(lot.id, data, {})
	else
		r = { error = 'no accession named' }
	end
	out[#out + 1] = frame:callParserFunction('#set', lotArgs(lot, not r.error and r or nil))
	out[#out + 1] = storeReports(frame, lot, '')
	out[#out + 1] = frame:extensionTag('templatestyles', '', { src = STYLES })
	out[#out + 1] = '{| class="infobox lw-infobox lw-lot-infobox"\n|+ class="lw-infobox__title" | ' .. lotLabel(lot)
		.. (lot.accession and (' of ' .. lot.accession) or '') .. '\n' .. infoboxRows(lot, fields)
		.. '\n|-\n! colspan="2" class="lw-infobox__section-header" | Reading\n|-\n| colspan="2" class="lw-infobox__value" | '
		.. (r.error and ("''" .. r.error .. "''") or GOP.renderLot(r)) .. '\n|}'
	local inv, tests = reportTable(lot, 'inventory'), reportTable(lot, 'germination test')
	out[#out + 1] = '\n== Inventory ==\n' .. (inv or "''No inventory report recorded.''")
	out[#out + 1] = '\n== Germination tests ==\n' .. (tests or "''No germination test recorded.''")
	out[#out + 1] = '\n[[Category:Seed lots]]'
	return table.concat(out, '\n')
end

-- Page name for Form:Seed lot: Lot:<accession>/<site code>; " (2)", " (3)" … when that page exists,
-- so a second lot never overwrites the first. (The form's <unique number> still trails it as a
-- guard against two saves at once.)
function p.pagename(frame)
	local acc, site = trim(frame.args.accession), trim(frame.args.site)
	if not acc or not site then return '' end
	local base = 'Lot:' .. acc .. '/' .. site
	local name = base
	for i = 2, 9 do
		local t = mw.title.new(name)
		if not (t and t.exists) then return name end
		name = base .. ' (' .. i .. ')'
	end
	return name
end

return p