diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts index 0d25da2710a..5fddc3320ea 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleDefs.ts @@ -5,10 +5,16 @@ import type { BusinessRuleResult } from './businessRules'; import { COG_PRIMARY_KEY, COG_TOITSELF, + COJO_PRIMARY_DELETE_KEY, CURRENT_DETERMINATION_KEY, DETERMINATION_TAXON_KEY, ensureSingleCollectionObjectCheck, hasNoCurrentDetermination, + PREPARATION_DISPOSED_KEY, + PREPARATION_EXCHANGED_IN_KEY, + PREPARATION_EXCHANGED_OUT_KEY, + PREPARATION_GIFTED_KEY, + PREPARATION_LOANED_KEY, } from './businessRuleUtils'; import { cogTypes } from './helpers'; import type { AnySchema, CommonFields, TableFields } from './helperTypes'; @@ -332,7 +338,7 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { collection.related ?? cojo, cojo.specifyTable.field.parentCog, [resourcesText.deletePrimaryRecord()], - resourcesText.primaryDeletionErrorMessage() + COJO_PRIMARY_DELETE_KEY ); } if (collection?.related?.specifyTable === tables.CollectionObjectGroup) { @@ -596,4 +602,48 @@ export const businessRuleDefs: MappedBusinessRuleDefs = { }, }, }, + Preparation: { + onRemoved: (preparation, collection): void => { + if (preparation.get('isOnLoan') === true) { + setSaveBlockers( + collection.related ?? preparation, + preparation.specifyTable.field.isOnLoan, + [resourcesText.deleteLoanedPrep()], + PREPARATION_LOANED_KEY + ) + } + if (preparation.get('isOnGift') === true) { + setSaveBlockers( + collection.related ?? preparation, + preparation.specifyTable.field.isOnGift, + [resourcesText.deleteGiftedPrep()], + PREPARATION_GIFTED_KEY + ) + } + if (preparation.get('isOnDisposal') === true) { + setSaveBlockers( + collection.related ?? preparation, + preparation.specifyTable.field.isOnDisposal, + [resourcesText.deleteDisposedPrep()], + PREPARATION_DISPOSED_KEY + ) + } + if (preparation.get('isOnExchangeOut') === true) { + setSaveBlockers( + collection.related ?? preparation, + preparation.specifyTable.field.isOnExchangeOut, + [resourcesText.deleteExchangeOutPrep()], + PREPARATION_EXCHANGED_OUT_KEY + ) + } + if (preparation.get('isOnExchangeIn') === true) { + setSaveBlockers( + collection.related ?? preparation, + preparation.specifyTable.field.isOnExchangeIn, + [resourcesText.deleteExchangeInPrep()], + PREPARATION_EXCHANGED_IN_KEY + ) + } + } + } }; diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts index 554d4c7ac96..3b67d92bdf6 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRuleUtils.ts @@ -8,6 +8,12 @@ export const COG_TOITSELF = 'cog-toItself'; export const PARENTCOG_KEY = 'cog-parentCog'; export const COG_PRIMARY_KEY = 'cog-isPrimary'; export const DETERMINATION_TAXON_KEY = 'determination-Taxon'; +export const PREPARATION_LOANED_KEY = 'preparation-isLoaned'; +export const PREPARATION_GIFTED_KEY = 'preparation-isGifted'; +export const PREPARATION_DISPOSED_KEY = 'preparation-isDisposed'; +export const PREPARATION_EXCHANGED_OUT_KEY = 'preparation-isExchangedOut'; +export const PREPARATION_EXCHANGED_IN_KEY = 'preparation-isExchangedIn'; +export const COJO_PRIMARY_DELETE_KEY = 'primary-cojo-delete' /** * diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts b/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts index 00977172648..5a8af38f5d8 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/schemaExtras.ts @@ -330,6 +330,38 @@ export const schemaExtras: { indexed: false, unique: false, }), + new LiteralField(table, { + name: 'isOnGift', + required: false, + readOnly: true, + type: 'java.lang.Boolean', + indexed: false, + unique: false, + }), + new LiteralField(table, { + name: 'isOnDisposal', + required: false, + readOnly: true, + type: 'java.lang.Boolean', + indexed: false, + unique: false, + }), + new LiteralField(table, { + name: 'isOnExchangeOut', + required: false, + readOnly: true, + type: 'java.lang.Boolean', + indexed: false, + unique: false, + }), + new LiteralField(table, { + name: 'isOnExchangeIn', + required: false, + readOnly: true, + type: 'java.lang.Boolean', + indexed: false, + unique: false, + }), new LiteralField(table, { name: 'actualCountAmt', required: false, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts index af34a091780..28152becfc6 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/types.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/types.ts @@ -4386,6 +4386,10 @@ export type Preparation = { readonly integer1: number | null; readonly integer2: number | null; readonly isOnLoan: boolean | null; + readonly isOnGift: boolean | null; + readonly isOnDisposal: boolean | null; + readonly isOnExchangeOut: boolean | null; + readonly isOnExchangeIn: boolean | null; readonly number1: number | null; readonly number2: number | null; readonly preparedDate: string | null; diff --git a/specifyweb/frontend/js_src/lib/localization/resources.ts b/specifyweb/frontend/js_src/lib/localization/resources.ts index 2d4ec3bf1a6..c9335b9b8fd 100644 --- a/specifyweb/frontend/js_src/lib/localization/resources.ts +++ b/specifyweb/frontend/js_src/lib/localization/resources.ts @@ -849,9 +849,20 @@ export const resourcesText = createDictionary({ deletePrimaryRecord: { 'en-us': 'Primary record CO cannot be deleted.', }, - primaryDeletionErrorMessage: { - 'en-us': - 'This record cannot be deleted as it is the primary record of the Collection Object Group. Please reload the page, then assign another CO as the primary record if a change is desired.', + deleteLoanedPrep: { + 'en-us': 'A loaned preparation cannot be deleted', + }, + deleteGiftedPrep: { + 'en-us': 'A gifted preparation cannot be deleted', + }, + deleteDisposedPrep: { + 'en-us': 'A disposed preparation cannot be deleted', + }, + deleteExchangeOutPrep: { + 'en-us': 'A exchanged out preparation cannot be deleted', + }, + deleteExchangeInPrep: { + 'en-us': 'A exchanged in preparation cannot be deleted', }, invalidDeterminationTaxon: { 'en-us': diff --git a/specifyweb/specify/calculated_fields.py b/specifyweb/specify/calculated_fields.py index e433c952d3b..bbe27f511e1 100644 --- a/specifyweb/specify/calculated_fields.py +++ b/specifyweb/specify/calculated_fields.py @@ -60,6 +60,10 @@ def calculate_extra_fields(obj, data: Dict[str, Any]) -> Dict[str, Any]: extra["actualCountAmt"] = actual_count_amount extra["isonloan"] = obj.isonloan() + extra["isongift"] = obj.isongift() + extra["isondisposal"] = obj.isondisposal() + extra["isonexchangeout"] = obj.isonexchangeout() + extra["isonexchangein"] = obj.isonexchangein() elif isinstance(obj, Specifyuser): extra["isadmin"] = obj.is_admin() diff --git a/specifyweb/specify/model_extras.py b/specifyweb/specify/model_extras.py index 2e2a65e3ff6..49329694766 100644 --- a/specifyweb/specify/model_extras.py +++ b/specifyweb/specify/model_extras.py @@ -144,6 +144,70 @@ def isonloan(self): result = cursor.fetchone() return result[0] > 0 + def isongift(self): + # TODO: needs unit tests + from django.db import connection + cursor = connection.cursor() + + cursor.execute(""" + SELECT COALESCE( + SUM({GREATEST}(0, COALESCE(Quantity, 0))), + 0) + FROM giftpreparation + WHERE PreparationID = %s + """.format(GREATEST='MAX' if connection.vendor == 'sqlite' else 'GREATEST'), [self.id]) + + result = cursor.fetchone() + return result[0] > 0 + + def isondisposal(self): + # TODO: needs unit tests + from django.db import connection + cursor = connection.cursor() + + cursor.execute(""" + SELECT COALESCE( + SUM({GREATEST}(0, COALESCE(Quantity, 0))), + 0) + FROM disposalpreparation + WHERE PreparationID = %s + """.format(GREATEST='MAX' if connection.vendor == 'sqlite' else 'GREATEST'), [self.id]) + + result = cursor.fetchone() + return result[0] > 0 + + def isonexchangeout(self): + # TODO: needs unit tests + from django.db import connection + cursor = connection.cursor() + + cursor.execute(""" + SELECT COALESCE( + SUM({GREATEST}(0, COALESCE(Quantity, 0))), + 0) + FROM exchangeoutprep + WHERE PreparationID = %s + """.format(GREATEST='MAX' if connection.vendor == 'sqlite' else 'GREATEST'), [self.id]) + + result = cursor.fetchone() + return result[0] > 0 + + def isonexchangein(self): + # TODO: needs unit tests + from django.db import connection + cursor = connection.cursor() + + cursor.execute(""" + SELECT COALESCE( + SUM({GREATEST}(0, COALESCE(Quantity, 0))), + 0) + FROM exchangeinprep + WHERE PreparationID = %s + """.format(GREATEST='MAX' if connection.vendor == 'sqlite' else 'GREATEST'), [self.id]) + + result = cursor.fetchone() + return result[0] > 0 + class Meta: abstract = True