diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.js b/nbri_ehr/resources/queries/nbri_ehr/Conception.js
deleted file mode 100644
index 9dd9a9c..0000000
--- a/nbri_ehr/resources/queries/nbri_ehr/Conception.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (c) 2026 LabKey Corporation
- *
- * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
- */
-require("ehr/triggers").initScript(this);
-
-var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);
-
-// the shared trigger collects modified participants from row.Id, which this table does not have, so announce the dams
-// here or their cached activeConceptions keeps a stale Pregnant value
-var damsModified = [];
-
-function addDam(dam) {
- if (dam && damsModified.indexOf(dam) === -1) {
- damsModified.push(dam);
- }
-}
-
-EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'nbri_ehr', 'Conception', function(event, helper){
- // the script scope can outlive a single save, so never inherit dams from a prior one
- damsModified = [];
-});
-
-EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'nbri_ehr', 'Conception', function(helper, scriptErrors, row, oldRow) {
- if (helper.isValidateOnly())
- return;
-
- addDam(row.Dam);
-
- // a re-pointed conception frees the dam it used to belong to
- addDam(oldRow ? oldRow.Dam : null);
-});
-
-EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_DELETE, 'nbri_ehr', 'Conception', function(helper, scriptErrors, row) {
- // the row LabKey passes for a delete can carry keys only, and the record is still readable at this point
- addDam(row.Dam || triggerHelper.getConceptionDam(row.ConceptId));
-});
-
-EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.COMPLETE, 'nbri_ehr', 'Conception', function(event, errors, helper){
- if (damsModified.length) {
- triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsModified);
- damsModified = [];
- }
-});
diff --git a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml b/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml
deleted file mode 100644
index df0d1ca..0000000
--- a/nbri_ehr/resources/queries/nbri_ehr/Conception.query.xml
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml b/nbri_ehr/resources/queries/study/ConceptionsByDam.query.xml
similarity index 100%
rename from nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.query.xml
rename to nbri_ehr/resources/queries/study/ConceptionsByDam.query.xml
diff --git a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql b/nbri_ehr/resources/queries/study/ConceptionsByDam.sql
similarity index 83%
rename from nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql
rename to nbri_ehr/resources/queries/study/ConceptionsByDam.sql
index 599a1ed..cf371fd 100644
--- a/nbri_ehr/resources/queries/nbri_ehr/ConceptionsByDam.sql
+++ b/nbri_ehr/resources/queries/study/ConceptionsByDam.sql
@@ -4,11 +4,12 @@
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/
SELECT
- c.Dam AS Id,
- c.ConceptId,
- c.ConceptDate,
- c.Estimated,
- c.Sire,
+ c.Id,
+ c.conceptId,
+ c.date,
+ c.conceptionDays,
+ c.estimated,
+ c.sire,
c.isActive,
CASE
WHEN c.isActive = true THEN 'Unknown'
@@ -16,12 +17,12 @@ SELECT
ELSE COALESCE(po.result, 'Unknown')
END AS conceptionOutcome,
b.offspring,
- c.Remark,
- c.QCState AS qcstate
-FROM Conception c
+ c.remark,
+ c.qcstate
+FROM conception c
-- Both joins match isActive: a record claims its conception unless its QC state is explicitly non-public, so a null state counts as public.
-- The birth trigger blocks a duplicate conceptId, but ETL imports skip that check, so the aggregate guards against one.
LEFT JOIN (SELECT b.conceptId, MAX(b.Id) AS offspring FROM study.birth b WHERE b.conceptId IS NOT NULL AND (b.qcstate IS NULL OR b.qcstate.publicdata = true) GROUP BY b.conceptId) b
- ON b.conceptId = c.ConceptId
+ ON b.conceptId = c.conceptId
LEFT JOIN (SELECT p.conceptId, MAX(p.result.title) AS result FROM study.pregnancy p WHERE p.conceptId IS NOT NULL AND (p.qcstate IS NULL OR p.qcstate.publicdata = true) GROUP BY p.conceptId) po
- ON po.conceptId = c.ConceptId
+ ON po.conceptId = c.conceptId
diff --git a/nbri_ehr/resources/queries/study/activeConceptions.sql b/nbri_ehr/resources/queries/study/activeConceptions.sql
index 3fd6f1f..d124ca6 100644
--- a/nbri_ehr/resources/queries/study/activeConceptions.sql
+++ b/nbri_ehr/resources/queries/study/activeConceptions.sql
@@ -4,8 +4,8 @@
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/
SELECT
- c.Dam AS Id,
- c.ConceptId,
- c.ConceptDate
-FROM nbri_ehr.Conception c
-WHERE c.isActive = true AND c.Dam IS NOT NULL
+ c.Id,
+ c.conceptId,
+ c.date
+FROM study.conception c
+WHERE c.isActive = true AND c.Id IS NOT NULL
diff --git a/nbri_ehr/resources/queries/study/birth.js b/nbri_ehr/resources/queries/study/birth.js
index fa4f20a..2174306 100644
--- a/nbri_ehr/resources/queries/study/birth.js
+++ b/nbri_ehr/resources/queries/study/birth.js
@@ -87,7 +87,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
}
if (damsToSync.length) {
- triggerHelper.reportDataChange('nbri_ehr', 'Conception', damsToSync);
+ triggerHelper.reportDataChange('study', 'conception', damsToSync);
damsToSync = [];
}
});
@@ -104,7 +104,7 @@ EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Even
}
if (!helper.isETL() && row.conceptId) {
- if (triggerHelper.totalRecords('nbri_ehr', 'Conception', 'ConceptId', row.conceptId) === 0) {
+ if (triggerHelper.totalRecords('study', 'conception', 'conceptId', row.conceptId) === 0) {
EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id does not match any conception record', 'WARN');
}
diff --git a/nbri_ehr/resources/queries/study/birth.query.xml b/nbri_ehr/resources/queries/study/birth.query.xml
index fc7174f..329725d 100644
--- a/nbri_ehr/resources/queries/study/birth.query.xml
+++ b/nbri_ehr/resources/queries/study/birth.query.xml
@@ -16,9 +16,9 @@
Conception Id
- nbri_ehr
- Conception
- ConceptId
+ study
+ conception
+ conceptId
diff --git a/nbri_ehr/resources/queries/study/conception.js b/nbri_ehr/resources/queries/study/conception.js
new file mode 100644
index 0000000..64a0504
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/conception.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+require("ehr/triggers").initScript(this);
+
+var triggerHelper = new org.labkey.nbri_ehr.query.NBRI_EHRTriggerHelper(LABKEY.Security.currentUser.id, LABKEY.Security.currentContainer.id);
+
+// conception ids claimed by the rows of this save that have already been validated. Rows entered together are not in
+// study.conception yet when each one is checked, so this is the only way the one-record-per-conception rule can see them.
+var conceptIdsInSave = [];
+
+EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.INIT, 'study', 'conception', function(event, helper){
+ helper.setScriptOptions({
+ // gestation puts the conception date months behind the entry date, so a distant past date is the normal case
+ allowDatesInDistantPast: true,
+ allowDeadIds: true,
+ skipHousingCheck: true,
+ skipAssignmentCheck: true
+ });
+
+ // the script scope can outlive a single save, so never inherit ids from a prior one
+ conceptIdsInSave = [];
+});
+
+// conceptId was a unique constraint before this became a dataset, and the birth and pregnancy triggers still resolve a
+// conception by that id alone, so the rule is enforced here now. Unlike the sibling checks in birth.js and
+// pregnancy.js this one does not exempt ETL: getConceptionDam() reads the dam with getObject(), which throws on a
+// second match, so a duplicate from any write path breaks later birth and pregnancy saves.
+EHR.Server.TriggerManager.registerHandlerForQuery(EHR.Server.TriggerManager.Events.BEFORE_UPSERT, 'study', 'conception', function(helper, scriptErrors, row, oldRow) {
+ if (!row.conceptId)
+ return;
+
+ //when updating a record that already carries this conception id, the existing row accounts for one match
+ var conceptIdThreshold = (oldRow && oldRow.conceptId === row.conceptId) ? 1 : 0;
+ var claimedBySavedRow = triggerHelper.totalRecords('study', 'conception', 'conceptId', row.conceptId) > conceptIdThreshold;
+
+ // rows are validated one at a time and collected below, so this list holds the earlier rows of this save only
+ var claimedByEarlierRow = conceptIdsInSave.indexOf(row.conceptId) > -1;
+
+ if (claimedBySavedRow || claimedByEarlierRow) {
+ EHR.Server.Utils.addError(scriptErrors, 'conceptId', 'This conception Id is already used by another conception record', 'ERROR');
+ }
+
+ conceptIdsInSave.push(row.conceptId);
+});
diff --git a/nbri_ehr/resources/queries/study/conception.query.xml b/nbri_ehr/resources/queries/study/conception.query.xml
new file mode 100644
index 0000000..fab299b
--- /dev/null
+++ b/nbri_ehr/resources/queries/study/conception.query.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ Conception Records
+ /query/recordDetails.view?schemaName=study&query.queryName=conception&keyField=conceptId&key=${conceptId}
+
+ conceptId
+
+
+ Dam
+
+
+ Conception Date
+ Date
+
+
+ Conception Id
+ true
+
+
+ Estimated
+ Conception date is estimated rather than observed
+
+
+
+
+
+ Conception Records
+ Records a conception, filed against the dam. The outcome is claimed later by a birth or pregnancy outcome record carrying the same conception Id.
+
+
+ Dam
+ varchar
+ http://cpas.labkey.com/Study#ParticipantId
+
+ ptid
+ dam
+
+
+
+ Conception Date
+ timestamp
+ http://cpas.labkey.com/Study#VisitDate
+ http://cpas.labkey.com/Study#VisitDate
+
+ conceptDate
+
+
+
+ Conception Id
+ varchar
+
+
+ Estimated
+ boolean
+
+
+ Sire
+ varchar
+
+
+
DemographicsContains core demographic information for each primate, including species, sex, parentage, birth and death dates, and colony status.
diff --git a/nbri_ehr/resources/reports/additionalReports.tsv b/nbri_ehr/resources/reports/additionalReports.tsv
index efaa2ad..043dd05 100644
--- a/nbri_ehr/resources/reports/additionalReports.tsv
+++ b/nbri_ehr/resources/reports/additionalReports.tsv
@@ -49,4 +49,4 @@ behaviorRemarks Behavior query Behavior Remarks true study BehaviorClinRemarks
clinObsBehavior Behavior query Observations true study behaviorObservations date false false qcstate/publicdata This report contains one record for each encounter with each animal, including surergies, exams, procedures, etc.
clinremarks Clinical query Clinical Remarks true study ClinicalClinRemarks date false false qcstate/publicdata This report contains the clinical remarks entered about each animal
physicalExam Clinical query Exam History True study physicalExam date false false qcstate/publicdata This report displays physical exam data for the selected animal
-conceptionsByDam Reproductive Management query Conceptions by Dam true nbri_ehr ConceptionsByDam ConceptDate false false qcstate/publicdata This report displays conception records where the selected animal is the dam
\ No newline at end of file
+conceptionsByDam Reproductive Management query Conceptions by Dam true study ConceptionsByDam date false false qcstate/publicdata This report displays conception records where the selected animal is the dam
\ No newline at end of file
diff --git a/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.002-26.003.sql b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.002-26.003.sql
new file mode 100644
index 0000000..852e5fb
--- /dev/null
+++ b/nbri_ehr/resources/schemas/dbscripts/postgresql/nbri_ehr-26.002-26.003.sql
@@ -0,0 +1,8 @@
+/*
+ * Copyright (c) 2026 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
+ */
+-- Conception is now the study.conception dataset, filed against the dam on the conception date. Existing rows are not carried over.
+DROP TABLE IF EXISTS nbri_ehr.Conception;
+
diff --git a/nbri_ehr/resources/schemas/nbri_ehr.xml b/nbri_ehr/resources/schemas/nbri_ehr.xml
index 9c11a59..a58688d 100644
--- a/nbri_ehr/resources/schemas/nbri_ehr.xml
+++ b/nbri_ehr/resources/schemas/nbri_ehr.xml
@@ -1,28 +1,4 @@
-
-