From db745320be81d9d9f7f7566eecb3f0fcc0e0b63b Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 28 Sep 2026 11:43:31 -0500 Subject: [PATCH 01/21] [test]: create loan by choosing recordset --- .../backend/businessrules/tests/test_loan.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 261d8632438..f62e7eeb55c 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -16,3 +16,40 @@ def test_loan_number_unique_in_discipline(self): models.Loan.objects.create( loannumber='2', discipline=self.discipline) + + def test_create_loan_by_choosing_recordset(self): + self._create_prep_type() + prep = self._create_prep(self.collectionobjects[0], None) + + record_set = models.Recordset.objects.create( + collectionmemberid=self.collection.id, + dbtableid=models.Collectionobject.specify_model.tableId, + name='Loan Recordset', + type=0, + specifyuser=self.specifyuser, + ) + + record_set.recordsetitems.create( + recordid=self.collectionobjects[0].id, + ) + + loan = models.Loan.objects.create( + loannumber='LOAN-RECORDSET-001', + discipline=self.discipline, + ) + + loan_prep = loan.loanpreparations.create( + discipline=self.discipline, + preparation=prep, + ) + + fetched = models.Loanpreparation.objects.get(id=loan_prep.id) + + self.assertEqual( + fetched.loan, + loan, + ) + self.assertEqual( + fetched.preparation, + prep, + ) From 7cbc8dbdfe1cfe102818305b9c3881ee1510946b Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 28 Sep 2026 11:43:54 -0500 Subject: [PATCH 02/21] [test]: create loan by entering cat number --- .../backend/businessrules/tests/test_loan.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index f62e7eeb55c..68a86bd463d 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -53,3 +53,27 @@ def test_create_loan_by_choosing_recordset(self): fetched.preparation, prep, ) + + def test_create_loan_by_entering_cat_number(self): + self._create_prep_type() + self._create_prep(self.collectionobjects[0], None) + + self.collectionobjects[0].catalognumber = 'CAT-LOAN-1001' + self.collectionobjects[0].save() + + co = models.Collectionobject.objects.get(catalognumber='CAT-LOAN-1001') + prep = models.Preparation.objects.get(collectionobject=co) + + loan = models.Loan.objects.create( + loannumber='LOAN-CATNUM-001', + discipline=self.discipline, + ) + loan_prep = models.Loanpreparation.objects.create( + loan=loan, + discipline=self.discipline, + preparation=prep, + ) + + fetched = models.Loanpreparation.objects.get(id=loan_prep.id) + self.assertEqual(fetched.preparation.collectionobject.catalognumber, 'CAT-LOAN-1001') + self.assertEqual(fetched.loan.loannumber, 'LOAN-CATNUM-001') From a3f76e7c290d28eb000ce32809806c0d11f39876 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:11:49 -0500 Subject: [PATCH 03/21] [test]: create loan without preparations --- .../backend/businessrules/tests/test_loan.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 68a86bd463d..b164673f85f 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -77,3 +77,20 @@ def test_create_loan_by_entering_cat_number(self): fetched = models.Loanpreparation.objects.get(id=loan_prep.id) self.assertEqual(fetched.preparation.collectionobject.catalognumber, 'CAT-LOAN-1001') self.assertEqual(fetched.loan.loannumber, 'LOAN-CATNUM-001') + + def test_create_loan_without_preparations(self): + loan = models.Loan.objects.create( + loannumber='LOAN-NOPREPS-001', + discipline=self.discipline, + ) + + fetched_loan = models.Loan.objects.get(id=loan.id) + + self.assertEqual( + fetched_loan.loannumber, + 'LOAN-NOPREPS-001', + ) + self.assertEqual( + fetched_loan.loanpreparations.count(), + 0, + ) From 4126c10c424ffbecfcf38663e68d7ab0bf037757 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:12:20 -0500 Subject: [PATCH 04/21] [test]: fill loan number and dates --- .../backend/businessrules/tests/test_loan.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index b164673f85f..dbbff5ed8d4 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -1,3 +1,5 @@ +from django.utils import timezone + from specifyweb.specify import models from specifyweb.specify.tests.test_api import ApiTests from ..exceptions import BusinessRuleException @@ -94,3 +96,21 @@ def test_create_loan_without_preparations(self): fetched_loan.loanpreparations.count(), 0, ) + + def test_fill_loan_number_and_dates(self): + loan = models.Loan.objects.create( + loannumber='LOAN-DATES-001', + discipline=self.discipline, + loandate=timezone.now(), + originalduedate=timezone.now(), + currentduedate=timezone.now(), + datereceived=timezone.now(), + ) + + fetched_loan = models.Loan.objects.get(id=loan.id) + + self.assertEqual(fetched_loan.loannumber, 'LOAN-DATES-001') + self.assertIsNotNone(fetched_loan.loandate) + self.assertIsNotNone(fetched_loan.originalduedate) + self.assertIsNotNone(fetched_loan.currentduedate) + self.assertIsNotNone(fetched_loan.datereceived) From 433da8669692ce6c989013ef419ce282516472b7 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:12:53 -0500 Subject: [PATCH 05/21] [test]: add existing agent to loan --- .../backend/businessrules/tests/test_loan.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index dbbff5ed8d4..35b2e37c30f 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -114,3 +114,30 @@ def test_fill_loan_number_and_dates(self): self.assertIsNotNone(fetched_loan.originalduedate) self.assertIsNotNone(fetched_loan.currentduedate) self.assertIsNotNone(fetched_loan.datereceived) + + def test_add_existing_agent_to_loan(self): + loan = models.Loan.objects.create( + loannumber='LOAN-AGENT-001', + discipline=self.discipline, + ) + + loan_agent = loan.loanagents.create( + agent=self.agent, + role='Loan agent', + discipline=self.discipline, + ) + + fetched = models.Loanagent.objects.get(id=loan_agent.id) + + self.assertEqual( + fetched.agent, + self.agent, + ) + self.assertEqual( + fetched.loan, + loan, + ) + self.assertEqual( + fetched.role, + 'Loan agent', + ) From db19f74d4ccf5a2f4e953fde5cdaaa875430d981 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:13:16 -0500 Subject: [PATCH 06/21] [test]: create new agent for loan --- .../backend/businessrules/tests/test_loan.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 35b2e37c30f..ca5c7a2fbeb 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -141,3 +141,31 @@ def test_add_existing_agent_to_loan(self): fetched.role, 'Loan agent', ) + + def test_create_new_agent_for_loan(self): + loan = models.Loan.objects.create( + loannumber='LOAN-NEWAGENT-001', + discipline=self.discipline, + ) + + agent = models.Agent.objects.create( + agenttype=0, + division=self.division, + ) + + loan_agent = loan.loanagents.create( + agent=agent, + role='Loan agent', + discipline=self.discipline, + ) + + fetched = models.Loanagent.objects.get(id=loan_agent.id) + + self.assertEqual( + fetched.agent_id, + agent.id, + ) + self.assertEqual( + fetched.agent.agenttype, + 0, + ) From 831cd743720259f1fea5f779792cd759eb4db38e Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:14:23 -0500 Subject: [PATCH 07/21] [test]: add shipment and fill all fields --- .../backend/businessrules/tests/test_loan.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index ca5c7a2fbeb..24f0f3a7dd0 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -169,3 +169,42 @@ def test_create_new_agent_for_loan(self): fetched.agent.agenttype, 0, ) + + def test_add_shipment_and_fill_all_fields(self): + loan = models.Loan.objects.create( + loannumber='LOAN-SHIPMENT-001', + discipline=self.discipline, + ) + + shipment = loan.shipments.create( + shipmentnumber='SHIPMENT-001', + discipline=self.discipline, + shipmentdate=timezone.now(), + shipmentmethod='Courier', + numberofpackages=2, + insuredforamount='1000', + weight='5 kg', + remarks='Handle with care', + ) + + fetched = models.Shipment.objects.get(id=shipment.id) + + self.assertEqual( + fetched.loan, + loan, + ) + self.assertEqual( + fetched.shipmentnumber, + 'SHIPMENT-001', + ) + self.assertEqual( + fetched.shipmentmethod, + 'Courier', + ) + self.assertEqual( + fetched.numberofpackages, + 2, + ) + self.assertIsNotNone( + fetched.shipmentdate, + ) From 9c751c0a4937530a12b3d94f1a3fd70c8cc113da Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:14:57 -0500 Subject: [PATCH 08/21] [test]: add existing shipped by agent to shipment --- .../backend/businessrules/tests/test_loan.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 24f0f3a7dd0..c5716450ce1 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -208,3 +208,22 @@ def test_add_shipment_and_fill_all_fields(self): self.assertIsNotNone( fetched.shipmentdate, ) + + def test_add_existing_shipped_by_agent_to_shipment(self): + loan = models.Loan.objects.create( + loannumber='LOAN-SHIPPEDBY-001', + discipline=self.discipline, + ) + + shipment = loan.shipments.create( + shipmentnumber='SHIPMENT-SHIPPEDBY-001', + discipline=self.discipline, + shippedby=self.agent, + ) + + fetched = models.Shipment.objects.get(id=shipment.id) + + self.assertEqual( + fetched.shippedby, + self.agent, + ) From 4fbada050bbd428f2fbcd11b205f307487027c17 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:15:57 -0500 Subject: [PATCH 09/21] [test]: create new shipped by agent for shipment --- .../backend/businessrules/tests/test_loan.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index c5716450ce1..5db209e93b4 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -227,3 +227,27 @@ def test_add_existing_shipped_by_agent_to_shipment(self): fetched.shippedby, self.agent, ) + + def test_create_new_shipped_by_agent_for_shipment(self): + loan = models.Loan.objects.create( + loannumber='LOAN-NEWSHIPPEDBY-001', + discipline=self.discipline, + ) + + agent = models.Agent.objects.create( + agenttype=0, + division=self.division, + ) + + shipment = loan.shipments.create( + shipmentnumber='SHIPMENT-NEWSHIPPEDBY-001', + discipline=self.discipline, + shippedby=agent, + ) + + fetched = models.Shipment.objects.get(id=shipment.id) + + self.assertEqual( + fetched.shippedby_id, + agent.id, + ) From 35fe8c489077132e8e17dd2c6bbc299346ddcf93 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:16:36 -0500 Subject: [PATCH 10/21] [test]: add existing shipped to agent to shipment --- .../backend/businessrules/tests/test_loan.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 5db209e93b4..96fdf427e33 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -251,3 +251,22 @@ def test_create_new_shipped_by_agent_for_shipment(self): fetched.shippedby_id, agent.id, ) + + def test_add_existing_shipped_to_agent_to_shipment(self): + loan = models.Loan.objects.create( + loannumber='LOAN-SHIPPEDTO-001', + discipline=self.discipline, + ) + + shipment = loan.shipments.create( + shipmentnumber='SHIPMENT-SHIPPEDTO-001', + discipline=self.discipline, + shippedto=self.agent, + ) + + fetched = models.Shipment.objects.get(id=shipment.id) + + self.assertEqual( + fetched.shippedto, + self.agent, + ) From f9fb85e6505859971d62d8576429d25fb2454751 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:17:04 -0500 Subject: [PATCH 11/21] [test]: create new shipped to agent for shipment --- .../backend/businessrules/tests/test_loan.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 96fdf427e33..5ae2948c10a 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -270,3 +270,27 @@ def test_add_existing_shipped_to_agent_to_shipment(self): fetched.shippedto, self.agent, ) + + def test_create_new_shipped_to_agent_for_shipment(self): + loan = models.Loan.objects.create( + loannumber='LOAN-NEWSHIPPEDTO-001', + discipline=self.discipline, + ) + + agent = models.Agent.objects.create( + agenttype=0, + division=self.division, + ) + + shipment = loan.shipments.create( + shipmentnumber='SHIPMENT-NEWSHIPPEDTO-001', + discipline=self.discipline, + shippedto=agent, + ) + + fetched = models.Shipment.objects.get(id=shipment.id) + + self.assertEqual( + fetched.shippedto_id, + agent.id, + ) From dd4fec682175e47e4557d6052d3abc53b4300a6c Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:18:00 -0500 Subject: [PATCH 12/21] [test]: add multiple loan agents preps and shipments --- .../backend/businessrules/tests/test_loan.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 5ae2948c10a..5b1cbe72000 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -294,3 +294,55 @@ def test_create_new_shipped_to_agent_for_shipment(self): fetched.shippedto_id, agent.id, ) + + def test_add_multiple_loan_agents_preps_and_shipments(self): + self._create_prep_type() + + loan = models.Loan.objects.create( + loannumber='LOAN-MULTIPLE-001', + discipline=self.discipline, + ) + + loan.loanagents.create( + agent=self.agent, + role='Loan agent', + discipline=self.discipline, + ) + loan.loanagents.create( + agent=self.agent, + role='Shipped by', + discipline=self.discipline, + ) + + loan.loanpreparations.create( + discipline=self.discipline, + preparation=self._create_prep(self.collectionobjects[0], None), + ) + loan.loanpreparations.create( + discipline=self.discipline, + preparation=self._create_prep(self.collectionobjects[1], None), + ) + + loan.shipments.create( + shipmentnumber='SHIPMENT-MULTI-001', + discipline=self.discipline, + ) + loan.shipments.create( + shipmentnumber='SHIPMENT-MULTI-002', + discipline=self.discipline, + ) + + fetched_loan = models.Loan.objects.get(id=loan.id) + + self.assertEqual( + fetched_loan.loanagents.count(), + 2, + ) + self.assertEqual( + fetched_loan.loanpreparations.count(), + 2, + ) + self.assertEqual( + fetched_loan.shipments.count(), + 2, + ) From 6c2c4b801ca39d3adeeb22a0dfc74d9d18dbea14 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:18:33 -0500 Subject: [PATCH 13/21] [test]: fill remaining loan fields --- .../backend/businessrules/tests/test_loan.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 5b1cbe72000..c5f51127284 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -1,3 +1,4 @@ +from decimal import Decimal from django.utils import timezone from specifyweb.specify import models @@ -346,3 +347,44 @@ def test_add_multiple_loan_agents_preps_and_shipments(self): fetched_loan.shipments.count(), 2, ) + + def test_fill_remaining_loan_fields(self): + loan = models.Loan.objects.create( + loannumber='LOAN-REMAINING-001', + discipline=self.discipline, + purposeofloan='Research', + status='Active', + remarks='Loan remarks', + specialconditions='Keep dry', + contents='Plant specimens', + srcgeography='South Africa', + srctaxonomy='Proteaceae', + receivedcomments='Received in good condition', + text1='Text 1', + text2='Text 2', + text3='Text 3', + text4='Text 4', + text5='Text 5', + yesno1=True, + yesno2=False, + number1=Decimal('1.5'), + number2=Decimal('2.5'), + integer1=1, + integer2=2, + integer3=3, + isfinancialresponsibility=True, + ) + + fetched_loan = models.Loan.objects.get(id=loan.id) + + self.assertEqual(fetched_loan.purposeofloan, 'Research') + self.assertEqual(fetched_loan.status, 'Active') + self.assertEqual(fetched_loan.remarks, 'Loan remarks') + self.assertEqual(fetched_loan.specialconditions, 'Keep dry') + self.assertEqual(fetched_loan.text1, 'Text 1') + self.assertEqual(fetched_loan.text5, 'Text 5') + self.assertEqual(fetched_loan.number1, Decimal('1.5')) + self.assertEqual(fetched_loan.integer1, 1) + self.assertEqual(fetched_loan.integer3, 3) + self.assertTrue(fetched_loan.yesno1) + self.assertFalse(fetched_loan.yesno2) From f3acd60a4c22d6250b09ee5c9b50763aebf1174e Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:19:09 -0500 Subject: [PATCH 14/21] [test]: save newly created loan --- .../backend/businessrules/tests/test_loan.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index c5f51127284..d2c05509cd8 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -388,3 +388,52 @@ def test_fill_remaining_loan_fields(self): self.assertEqual(fetched_loan.integer3, 3) self.assertTrue(fetched_loan.yesno1) self.assertFalse(fetched_loan.yesno2) + + def test_save_newly_created_loan(self): + self._create_prep_type() + + loan = models.Loan.objects.create( + loannumber='LOAN-SAVE-001', + discipline=self.discipline, + loandate=timezone.now(), + status='Active', + ) + + agent = models.Agent.objects.create( + agenttype=0, + division=self.division, + ) + loan.loanagents.create( + agent=agent, + role='Loan agent', + discipline=self.discipline, + ) + + loan.loanpreparations.create( + discipline=self.discipline, + preparation=self._create_prep(self.collectionobjects[0], None), + ) + + loan.shipments.create( + shipmentnumber='SHIPMENT-SAVE-001', + discipline=self.discipline, + ) + + fetched_loan = models.Loan.objects.get(id=loan.id) + + self.assertEqual( + fetched_loan.loannumber, + 'LOAN-SAVE-001', + ) + self.assertEqual( + fetched_loan.loanagents.count(), + 1, + ) + self.assertEqual( + fetched_loan.loanpreparations.count(), + 1, + ) + self.assertEqual( + fetched_loan.shipments.count(), + 1, + ) From cf1095a3395b0278249d4eb2e3eeec07e8b1e269 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:19:36 -0500 Subject: [PATCH 15/21] [test]: check collection object is loaned --- .../backend/businessrules/tests/test_loan.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index d2c05509cd8..63d8142a5b7 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -437,3 +437,28 @@ def test_save_newly_created_loan(self): fetched_loan.shipments.count(), 1, ) + + def test_check_collection_object_is_loaned(self): + self._create_prep_type() + + prep = self._create_prep(self.collectionobjects[0], None) + + loan = models.Loan.objects.create( + loannumber='LOAN-LOANED-001', + discipline=self.discipline, + ) + loan.loanpreparations.create( + discipline=self.discipline, + preparation=prep, + ) + + fetched_prep = models.Preparation.objects.get(id=prep.id) + + self.assertEqual( + fetched_prep.loanpreparations.count(), + 1, + ) + self.assertEqual( + fetched_prep.loanpreparations.first().loan.loannumber, + 'LOAN-LOANED-001', + ) From ea7a4a612d1b3b85d18f802e96df4b36e5c7a1c8 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:20:36 -0500 Subject: [PATCH 16/21] [test]: add attachment to loan --- .../backend/businessrules/tests/test_loan.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index 63d8142a5b7..d59b648ca5d 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -462,3 +462,31 @@ def test_check_collection_object_is_loaned(self): fetched_prep.loanpreparations.first().loan.loannumber, 'LOAN-LOANED-001', ) + + def test_add_attachment_to_loan(self): + loan = models.Loan.objects.create( + loannumber='LOAN-ATTACH-001', + discipline=self.discipline, + ) + + attachment = models.Attachment.objects.create( + origfilename='loan_document.pdf', + tableid=loan.specify_model.tableId, + title='Loan Document', + ) + + loan.loanattachments.create( + attachment=attachment, + ordinal=0, + ) + + fetched = models.Loanattachment.objects.get(attachment=attachment) + + self.assertEqual( + fetched.loan, + loan, + ) + self.assertEqual( + fetched.ordinal, + 0, + ) From b93eb6998b5707ba3b8748a4ca89a38dd1818b16 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:21:32 -0500 Subject: [PATCH 17/21] [test]: delete attachment from loan --- .../backend/businessrules/tests/test_loan.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_loan.py b/specifyweb/backend/businessrules/tests/test_loan.py index d59b648ca5d..13dd1f81764 100644 --- a/specifyweb/backend/businessrules/tests/test_loan.py +++ b/specifyweb/backend/businessrules/tests/test_loan.py @@ -490,3 +490,33 @@ def test_add_attachment_to_loan(self): fetched.ordinal, 0, ) + + def test_delete_attachment_from_loan(self): + loan = models.Loan.objects.create( + loannumber='LOAN-ATTACH-DELETE-001', + discipline=self.discipline, + ) + + attachment = models.Attachment.objects.create( + origfilename='loan_delete_doc.pdf', + tableid=loan.specify_model.tableId, + title='Loan Delete Document', + ) + + loan_attachment = loan.loanattachments.create( + attachment=attachment, + ordinal=0, + ) + + loan_attachment_id = loan_attachment.id + loan_attachment.delete() + + self.assertFalse( + models.Loanattachment.objects.filter( + id=loan_attachment_id, + ).exists(), + ) + self.assertEqual( + loan.loanattachments.count(), + 0, + ) From ef4b50f6ff4c6325cbbea23a32f0e14efb419cab Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:38:55 -0500 Subject: [PATCH 18/21] [test]: verify loan preparations are not carried over when cloning --- .../businessrules/tests/test_clone_previously_created.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_clone_previously_created.py b/specifyweb/backend/businessrules/tests/test_clone_previously_created.py index e60709f582f..c57813174a8 100644 --- a/specifyweb/backend/businessrules/tests/test_clone_previously_created.py +++ b/specifyweb/backend/businessrules/tests/test_clone_previously_created.py @@ -461,6 +461,12 @@ def test_clone_accession(self): def test_clone_loan(self): original = self.loans[0] + self._create_prep_type() + original.loanpreparations.create( + discipline=self.discipline, + preparation=self._create_prep(self.collectionobjects[0], None), + ) + clone = self._clone_resource( 'loan', { @@ -475,6 +481,8 @@ def test_clone_loan(self): self.assertEqual(clone.discipline_id, original.discipline_id) self.assertEqual(clone.remarks, original.remarks) self.assertNotEqual(clone.loannumber, original.loannumber) + self.assertEqual(original.loanpreparations.count(), 1) + self.assertEqual(clone.loanpreparations.count(), 0) original.refresh_from_db() self.assertEqual(original.loannumber, 'Test loan') From 69e04f3b113f047269768bb8ed0543ab7c590331 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 11:39:25 -0500 Subject: [PATCH 19/21] [test]: verify collection objects are not loaned after deleting loan --- .../tests/test_delete_previously_created.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_delete_previously_created.py b/specifyweb/backend/businessrules/tests/test_delete_previously_created.py index bd2b30b6b4b..3af424e62b2 100644 --- a/specifyweb/backend/businessrules/tests/test_delete_previously_created.py +++ b/specifyweb/backend/businessrules/tests/test_delete_previously_created.py @@ -420,7 +420,22 @@ def test_delete_accession_created_in_previous_version(self): ) def test_delete_loan_created_in_previous_version(self): + self._create_prep_type() + loan = self.loans[0] + + first_prep = self._create_prep(self.collectionobjects[0], None) + second_prep = self._create_prep(self.collectionobjects[1], None) + + loan.loanpreparations.create( + discipline=self.discipline, + preparation=first_prep, + ) + loan.loanpreparations.create( + discipline=self.discipline, + preparation=second_prep, + ) + object_id = loan.id loan.delete() @@ -429,6 +444,22 @@ def test_delete_loan_created_in_previous_version(self): models.Loan.objects.filter(id=object_id).count(), 0, ) + self.assertEqual( + models.Loanpreparation.objects.filter(loan_id=object_id).count(), + 0, + ) + + first_prep.refresh_from_db() + second_prep.refresh_from_db() + + self.assertEqual( + first_prep.loanpreparations.count(), + 0, + ) + self.assertEqual( + second_prep.loanpreparations.count(), + 0, + ) models.Loan.objects.create( loannumber='Test loan', From 19069d46210c9824b1fba6fa2aadea17f52b80c0 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 29 Sep 2026 13:04:40 -0500 Subject: [PATCH 20/21] [test]: prep availability with unknown and prepless catalog numbers --- .../tests/test_preps_available_ids.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/specifyweb/backend/interactions/tests/test_preps_available_ids.py b/specifyweb/backend/interactions/tests/test_preps_available_ids.py index 9527d4668d4..a64b152b9e6 100644 --- a/specifyweb/backend/interactions/tests/test_preps_available_ids.py +++ b/specifyweb/backend/interactions/tests/test_preps_available_ids.py @@ -1,4 +1,6 @@ from specifyweb.backend.interactions.tests.test_preps_available_context import TestPrepsAvailableContext +from specifyweb.backend.interactions.tests.utils import _create_interaction_prep_generic +from specifyweb.specify import models import json @@ -67,3 +69,42 @@ def test_preps_available_interacted_isloan(self): returned_counts = json.loads(response.content.decode()) self.assertEqual(returned_counts, [expected_counts[i] for i in range(1, 10, 2)]) + + def test_preps_available_mixed_catalog_numbers(self): + expected_response = self._preps_available_simple() + + # Loan out the first prep completely so it becomes unavailable. + _create_interaction_prep_generic( + self, + self.loan, + self._prep_list[0], + None, + quantity=5, + quantityresolved=0, + quantityreturned=0, + ) + expected_response[0][7] = "5" + expected_response[0][10] = "0" + + # A collection object with no preparations at all. + models.Collectionobject.objects.create( + collection=self.collection, + catalognumber="no-preps-1", + collectionobjecttype=self.collectionobjecttype, + ) + + response = self.client.post( + f'/interactions/preparations_available_ids/', + data={ + 'id_fld': 'CatalogNumber', + 'co_ids': json.dumps( + [co.catalognumber for co in self.collectionobjects] + + ["no-preps-1", "does-not-exist-999"] + ), + } + ) + + self.assertEqual(response.status_code, 200) + + returned_counts = json.loads(response.content.decode()) + self.assertEqual(returned_counts, expected_response) From 05004efa764e1c855ff561ed7d267921319bf91c Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:50:49 +0000 Subject: [PATCH 21/21] Lint code with ESLint and Prettier Triggered by 4d6ab3108098b98366ecb165a8e2203ad3c9a1c1 on branch refs/heads/issue-8588 --- .../__tests__/BatchEditFromQuery.test.tsx | 81 +++++++++++++------ .../BatchEdit/__tests__/MissingRanks.test.tsx | 4 +- .../__tests__/missingRanksUtils.test.ts | 6 +- .../WbUtils/__tests__/datasetVariants.test.ts | 4 +- 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/BatchEditFromQuery.test.tsx b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/BatchEditFromQuery.test.tsx index 82d257b381f..66a8832f56c 100644 --- a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/BatchEditFromQuery.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/BatchEditFromQuery.test.tsx @@ -35,9 +35,13 @@ async function withoutActWarnings( } overrideAjax('/api/workbench/dataset/?isupdate=1', []); -overrideAjax('/stored_query/batch_edit/', { id: datasetId }, { - method: 'POST', -}); +overrideAjax( + '/stored_query/batch_edit/', + { id: datasetId }, + { + method: 'POST', + } +); const queryField = (mappingPath: MappingPath): QueryField => ({ id: 0, @@ -73,8 +77,7 @@ function render({ void promise; }; return mount( - + @@ -128,7 +131,9 @@ describe('the Batch Edit button', () => { test('creates the data set and opens it', async () => { const { getByRole, findByText, user } = render(); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); expect(await findByText('Data set opened')).toBeInTheDocument(); }); }); @@ -155,7 +160,9 @@ describe('the unsaved query guard', () => { test('the warning can be dismissed', async () => { const { getByRole, queryByRole, user } = render({ saveRequired: true }); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); await user.click(getByRole('button', { name: commonText.close() })); expect(queryByRole('dialog')).toBeNull(); }); @@ -165,16 +172,22 @@ describe('the unsaved query guard', () => { // Enable raltionships in user preferences. function mockEnableRelationships(enabled: boolean): void { const realGet = userPreferences.get.bind(userPreferences); - jest.spyOn(userPreferences, 'get').mockImplementation((( - category: string, - subcategory: string, - item: string - ) => - category === 'batchEdit' && - subcategory === 'editor' && - item === 'enableRelationships' - ? enabled - : realGet(category as never, subcategory as never, item as never)) as never); + jest + .spyOn(userPreferences, 'get') + .mockImplementation((( + category: string, + subcategory: string, + item: string + ) => + category === 'batchEdit' && + subcategory === 'editor' && + item === 'enableRelationships' + ? enabled + : realGet( + category as never, + subcategory as never, + item as never + )) as never); } describe('the enable relationships preference', () => { @@ -192,13 +205,17 @@ describe('the enable relationships preference', () => { const ajax = jest.spyOn(ajaxModule, 'ajax'); const { getByRole, findByText, user } = render(); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); expect(await findByText('Data set opened')).toBeInTheDocument(); }); expect(ajax).toHaveBeenCalledWith( '/stored_query/batch_edit/', expect.objectContaining({ - body: expect.objectContaining({ omitrelationships: omitRelationships }), + body: expect.objectContaining({ + omitrelationships: omitRelationships, + }), }) ); } @@ -217,9 +234,12 @@ const missingRank = (rank: string): string => describe('the missing rank dialog', () => { test('appears when ranks are missing', async () => { - const { getByRole, findByRole, queryByText, user } = renderTaxonQuery('Genus'); + const { getByRole, findByRole, queryByText, user } = + renderTaxonQuery('Genus'); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); const dialog = await findByRole('dialog'); expect(dialog).toHaveTextContent(batchEditText.missingRanksInQuery()); expect( @@ -235,19 +255,26 @@ describe('the missing rank dialog', () => { const { getByRole, findByText, queryByRole, user } = renderTaxonQuery('Subspecies'); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); expect(await findByText('Data set opened')).toBeInTheDocument(); expect(queryByRole('dialog')).toBeNull(); }); }); test('continuing creates the data set', async () => { - const { getByRole, findByRole, findByText, user } = renderTaxonQuery('Genus'); + const { getByRole, findByRole, findByText, user } = + renderTaxonQuery('Genus'); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); const dialog = await findByRole('dialog'); await user.click( - within(dialog).getByRole('button', { name: interactionsText.continue() }) + within(dialog).getByRole('button', { + name: interactionsText.continue(), + }) ); expect(await findByText('Data set opened')).toBeInTheDocument(); }); @@ -257,7 +284,9 @@ describe('the missing rank dialog', () => { const { getByRole, findByRole, queryByRole, queryByText, user } = renderTaxonQuery('Genus'); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); const dialog = await findByRole('dialog'); await user.click( within(dialog).getByRole('button', { name: commonText.close() }) diff --git a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/MissingRanks.test.tsx b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/MissingRanks.test.tsx index 2fbbf353b9f..7707ce9091e 100644 --- a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/MissingRanks.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/MissingRanks.test.tsx @@ -140,7 +140,9 @@ describe('picking trees in the missing rank dialog', () => { test('offers a checkbox for each tree the rank is in', async () => { const { getByRole, findByRole, user } = renderGenusQuery(); await withoutActWarnings(async () => { - await user.click(getByRole('button', { name: batchEditText.batchEdit() })); + await user.click( + getByRole('button', { name: batchEditText.batchEdit() }) + ); const dialog = await findByRole('dialog'); expect(dialog).toHaveTextContent(batchEditText.pickTreesToFilter()); expect( diff --git a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/missingRanksUtils.test.ts b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/missingRanksUtils.test.ts index 3889d2b0eb4..37eca013587 100644 --- a/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/missingRanksUtils.test.ts +++ b/specifyweb/frontend/js_src/lib/components/BatchEdit/__tests__/missingRanksUtils.test.ts @@ -25,7 +25,7 @@ describe('findAllMissing', () => { Taxon: { Taxonomy: missing('Subgenus', 'Species', 'Subspecies') }, }); }); - + test('a query at the lowest rank is missing nothing', () => { expect(findAllMissing(taxonQuery(atRank('Subspecies')))).toEqual({}); }); @@ -40,7 +40,9 @@ describe('findAllMissing', () => { test('a rank queried without its name counts as missing', () => { expect(findAllMissing(taxonQuery(atRank('Genus', 'author')))).toEqual({ - Taxon: { Taxonomy: missing('Genus', 'Subgenus', 'Species', 'Subspecies') }, + Taxon: { + Taxonomy: missing('Genus', 'Subgenus', 'Species', 'Subspecies'), + }, }); }); diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts index aae95d8f326..75325f872d0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/__tests__/datasetVariants.test.ts @@ -21,7 +21,7 @@ const setBatchEditPreferences = ( userPreferences.set('batchEdit', 'editor', 'showRollback', showRollback); }; -// Drop every explicitly set preference so lookups fall back to the defaults +// Drop every explicitly set preference so lookups fall back to the defaults const resetPreferences = (): void => userPreferences.setRaw({}); beforeAll(() => { @@ -127,4 +127,4 @@ describe('batch edit validation availability', () => { mockedHasPermission.mockReturnValue(true); expect(datasetVariants.batchEdit.canValidate()).toBe(true); }); -}); \ No newline at end of file +});