changeset 5917:fa8c10f10312 get-scu

merged default -> get-scu
author Alain Mazy <am@orthanc.team>
date Mon, 09 Dec 2024 19:41:24 +0100
parents 305d318f488d (diff) cc5a6f3b9bbe (current diff)
children
files NEWS OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h TODO
diffstat 72 files changed, 2064 insertions(+), 530 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Dec 04 18:16:44 2024 +0100
+++ b/NEWS	Mon Dec 09 19:41:24 2024 +0100
@@ -1,8 +1,20 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* DICOM:
+  - Added support for C-GET SCU.
+  - Added a configuration "AcceptedSopClasses" and "RejectedSopClasses" to limit 
+    the SOP classes accepted by Orthanc when acting as C-STORE SCP.
+  - New config option "DicomDefaultRetrieveMethod" to define wheter Orthanc uses C-MOVE or C-GET 
+    to retrieve a resource after a C-Find (when calling /queries/.../retrieve).
+    This configuration can be overriden for each modality in "DicomModalities->..->RetrieveMethod".
+    Default value: "C-MOVE" to keep the backward compatibility.
+
 REST API
------------
+--------
 
 * API version upgraded to 26
 * Support HTTP "Range" request header on "{...}/attachments/{...}/data" and
@@ -15,13 +27,21 @@
   standard https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html.
   This has no impact on StoneViewer and OHIF.
   https://discourse.orthanc-server.org/t/dicomwebplugin-does-not-return-series-metadata-properly/5195
-
+* /queries/../retrieve now accepts a new field in the payload: "RetrieveMethod" to define wheter 
+  Orthanc uses C-MOVE or C-GET to retrieve the resource.
+* improved progress reporting of DicomMoveScu jobs.
 
 Maintenance
 -----------
 
 * DICOM TLS: "DicomTlsTrustedCertificates" is not required anymore when issuing
   an outgoing SCU connexion when "DicomTlsRemoteCertificateRequired" is set to false.
+* DICOM negotiation:
+  - When opening a DICOM SCU connection, Orthanc now only proposes the contexts that it is
+    going to use in the connection and not all contexts as in previous versions.  E.g, when
+    performing a C-ECHO, Orthanc will not propose C-MOVE or C-FIND.
+* DICOM Get-SCP: Orthanc won't refuse anymore to send e.g. a LittleEndianExplicit file when
+  the accepted transfer syntax is a compressed one.
 * Introduced a new thread to update the statistics at regular interval for the
   DB plugins that are implementing the UpdateAndGetStatistics function (currently only
   PostgreSQL).  This avoids very long update times in case you don't call /statistics
--- a/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Resources/CodeGeneration/ErrorCodes.json	Mon Dec 09 19:41:24 2024 +0100
@@ -602,6 +602,11 @@
     "Name": "NoCGetHandler", 
     "Description": "No request handler factory for DICOM C-GET SCP"
   },
+  {
+    "Code": 2045, 
+    "Name": "DicomGetUnavailable", 
+    "Description": "DicomUserConnection: The C-GET command is not supported by the remote SCP"
+  },
 
 
 
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -1300,7 +1300,22 @@
       return value->CopyToString(result, allowBinary);
     }
   }
-    
+
+  bool DicomMap::LookupStringValues(std::set<std::string>& results,
+                                    const DicomTag& tag,
+                                    bool allowBinary) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, tag, allowBinary))
+    {
+      Toolbox::SplitString(results, tmp, '\\');
+      return true;
+    }
+
+    return false;
+  }
+
+
   bool DicomMap::ParseInteger32(int32_t& result,
                                 const DicomTag& tag) const
   {
--- a/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Mon Dec 09 19:41:24 2024 +0100
@@ -179,7 +179,11 @@
     bool LookupStringValue(std::string& result,
                            const DicomTag& tag,
                            bool allowBinary) const;
-    
+
+    bool LookupStringValues(std::set<std::string>& results,
+                           const DicomTag& tag,
+                           bool allowBinary) const;
+
     bool ParseInteger32(int32_t& result,
                         const DicomTag& tag) const;
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -173,7 +173,6 @@
 
   DicomAssociation::DicomAssociation()
   {
-    role_ = DicomAssociationRole_Default;
     isOpen_ = false;
     net_ = NULL; 
     params_ = NULL;
@@ -198,16 +197,6 @@
   }
 
 
-  void DicomAssociation::SetRole(DicomAssociationRole role)
-  {
-    if (role_ != role)
-    {
-      Close();
-      role_ = role;
-    }
-  }
-
-  
   void DicomAssociation::ClearPresentationContexts()
   {
     Close();
@@ -215,7 +204,26 @@
     proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
   }
 
-  
+
+  static T_ASC_SC_ROLE GetDcmtkRole(DicomAssociationRole role)
+  {
+    switch (role)
+    {
+      case DicomAssociationRole_Default:
+        return ASC_SC_ROLE_DEFAULT;
+
+      case DicomAssociationRole_Scu:
+        return ASC_SC_ROLE_SCU;
+
+      case DicomAssociationRole_Scp:
+        return ASC_SC_ROLE_SCP;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   void DicomAssociation::Open(const DicomAssociationParameters& parameters)
   {
     if (isOpen_)
@@ -240,24 +248,6 @@
       dcmConnectionTimeout.set(acseTimeout);
     }
       
-    T_ASC_SC_ROLE dcmtkRole;
-    switch (role_)
-    {
-      case DicomAssociationRole_Default:
-        dcmtkRole = ASC_SC_ROLE_DEFAULT;
-        break;
-
-      case DicomAssociationRole_Scu:
-        dcmtkRole = ASC_SC_ROLE_SCU;
-        break;
-
-      case DicomAssociationRole_Scp:
-        dcmtkRole = ASC_SC_ROLE_SCP;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
 
     assert(net_ == NULL &&
            params_ == NULL &&
@@ -291,7 +281,12 @@
                                   "no timeout") << ")";
 
     CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
+#if DCMTK_VERSION_NUMBER >= 368
+    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, parameters.GetMaximumPduLength(), acseTimeout));
+#else
+    // from 3.6.8, this version is obsolete
     CheckConnecting(parameters, ASC_createAssociationParameters(&params_, parameters.GetMaximumPduLength()));
+#endif
 
 #if ORTHANC_ENABLE_SSL == 1
     if (parameters.GetRemoteModality().IsDicomTlsEnabled())
@@ -351,12 +346,12 @@
       assert(presentationContextId <= 255);
       const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
 
-      const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
+      const std::list<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
           
       std::vector<const char*> transferSyntaxes;
       transferSyntaxes.reserve(source.size());
           
-      for (std::set<DicomTransferSyntax>::const_iterator
+      for (std::list<DicomTransferSyntax>::const_iterator
              it = source.begin(); it != source.end(); ++it)
       {
         transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
@@ -365,7 +360,7 @@
       assert(!transferSyntaxes.empty());
       CheckConnecting(parameters, ASC_addPresentationContext(
                         params_, presentationContextId, abstractSyntax,
-                        &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
+                        &transferSyntaxes[0], transferSyntaxes.size(), GetDcmtkRole(proposed_[i].role_)));
 
       presentationContextId += 2;
     }
@@ -456,36 +451,56 @@
     }
   }
 
+  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                                           DicomAssociationRole role)
+  {
+    std::list<DicomTransferSyntax> ts;
+    ts.push_back(DicomTransferSyntax_LittleEndianExplicit); // the most standard one first !
+    ts.push_back(DicomTransferSyntax_LittleEndianImplicit);
+    ts.push_back(DicomTransferSyntax_BigEndianExplicit);  // Retired but was historicaly proposed by Orthanc
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
     
   void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
   {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(DicomTransferSyntax_LittleEndianImplicit);
-    ts.insert(DicomTransferSyntax_LittleEndianExplicit);
-    ts.insert(DicomTransferSyntax_BigEndianExplicit);  // Retired
-    ProposePresentationContext(abstractSyntax, ts);
+    ProposeGenericPresentationContext(abstractSyntax, DicomAssociationRole_Default);
   }
 
     
   void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
                                                     DicomTransferSyntax transferSyntax)
   {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(transferSyntax);
-    ProposePresentationContext(abstractSyntax, ts);
+    ProposePresentationContext(abstractSyntax, transferSyntax, DicomAssociationRole_Default);
   }
 
-    
+
+  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
+                                                    DicomTransferSyntax transferSyntax,
+                                                    DicomAssociationRole role)
+  {
+    std::list<DicomTransferSyntax> ts;
+    ts.push_back(transferSyntax);
+    ProposePresentationContext(abstractSyntax, ts, role);
+  }
+
   size_t DicomAssociation::GetRemainingPropositions() const
   {
     assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
     return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
   }
     
+  void DicomAssociation::ProposePresentationContext(
+    const std::string& abstractSyntax,
+    const std::list<DicomTransferSyntax>& transferSyntaxes)
+  {
+    ProposePresentationContext(abstractSyntax, transferSyntaxes, DicomAssociationRole_Default);
+  }
+
 
   void DicomAssociation::ProposePresentationContext(
     const std::string& abstractSyntax,
-    const std::set<DicomTransferSyntax>& transferSyntaxes)
+    const std::list<DicomTransferSyntax>& transferSyntaxes,
+    DicomAssociationRole role)
   {
     if (transferSyntaxes.empty())
     {
@@ -507,6 +522,7 @@
     ProposedPresentationContext context;
     context.abstractSyntax_ = abstractSyntax;
     context.transferSyntaxes_ = transferSyntaxes;
+    context.role_ = role;
 
     proposed_.push_back(context);
   }
@@ -526,6 +542,35 @@
     }
   }
 
+  bool DicomAssociation::GetAssociationParameters(std::string& remoteAet,
+                                                  std::string& remoteIp,
+                                                  std::string& calledAet) const
+  {
+    T_ASC_Association& dcmtkAssoc = GetDcmtkAssociation();
+
+    DIC_AE remoteAet_C;
+    DIC_AE calledAet_C;
+    DIC_AE remoteIp_C;
+    DIC_AE calledIP_C;
+
+    if (
+#if DCMTK_VERSION_NUMBER >= 364
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).good()
+#else
+      ASC_getAPTitles(dcmtkAssoc.params, remoteAet_C, calledAet_C, NULL).good() &&
+      ASC_getPresentationAddresses(dcmtkAssoc.params, remoteIp_C, calledIP_C).good()
+#endif
+      )
+    {
+      remoteIp = std::string(/*OFSTRING_GUARD*/(remoteIp_C));
+      remoteAet = std::string(/*OFSTRING_GUARD*/(remoteAet_C));
+      calledAet = (/*OFSTRING_GUARD*/(calledAet_C));
+      return true;
+    }
+
+    return false;
+  }
     
   T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
   {
@@ -649,13 +694,12 @@
     DicomAssociation association;
 
     {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      std::list<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
 
-      association.SetRole(DicomAssociationRole_Scp);
       association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
+                                             transferSyntaxes, DicomAssociationRole_Scp);
     }
       
     association.Open(parameters);
@@ -828,13 +872,13 @@
     DicomAssociation association;
 
     {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      std::list<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
       
-      association.SetRole(DicomAssociationRole_Default);
+      // association.SetRole(DicomAssociationRole_Default);
       association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
+                                             transferSyntaxes, DicomAssociationRole_Default);
     }
       
     association.Open(parameters);
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Mon Dec 09 19:41:24 2024 +0100
@@ -44,6 +44,7 @@
 #include <stdint.h>   // For uint8_t
 #include <boost/noncopyable.hpp>
 #include <set>
+#include <list>
 
 namespace Orthanc
 {
@@ -58,13 +59,13 @@
     struct ProposedPresentationContext
     {
       std::string                    abstractSyntax_;
-      std::set<DicomTransferSyntax>  transferSyntaxes_;
+      std::list<DicomTransferSyntax> transferSyntaxes_;
+      DicomAssociationRole           role_;
     };
 
     typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >
     AcceptedPresentationContexts;
 
-    DicomAssociationRole                      role_;
     bool                                      isOpen_;
     std::vector<ProposedPresentationContext>  proposed_;
     AcceptedPresentationContexts              accepted_;
@@ -95,8 +96,6 @@
       return isOpen_;
     }
 
-    void SetRole(DicomAssociationRole role);
-
     void ClearPresentationContexts();
 
     void Open(const DicomAssociationParameters& parameters);
@@ -109,6 +108,13 @@
 
     void ProposeGenericPresentationContext(const std::string& abstractSyntax);
 
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax,
+                                           DicomAssociationRole role);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax,
+                                    DicomAssociationRole role);
+
     void ProposePresentationContext(const std::string& abstractSyntax,
                                     DicomTransferSyntax transferSyntax);
 
@@ -116,12 +122,21 @@
 
     void ProposePresentationContext(
       const std::string& abstractSyntax,
-      const std::set<DicomTransferSyntax>& transferSyntaxes);
-    
+      const std::list<DicomTransferSyntax>& transferSyntaxes);
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::list<DicomTransferSyntax>& transferSyntaxes,
+      DicomAssociationRole role);
+
     T_ASC_Association& GetDcmtkAssociation() const;
 
     T_ASC_Network& GetDcmtkNetwork() const;
 
+    bool GetAssociationParameters(std::string& remoteAet,
+                                  std::string& remoteIp,
+                                  std::string& calledAet) const;
+
     static void CheckCondition(const OFCondition& cond,
                                const DicomAssociationParameters& parameters,
                                const std::string& command);
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -234,15 +234,58 @@
 
 
 
-  void DicomControlUserConnection::SetupPresentationContexts()
+  void DicomControlUserConnection::SetupPresentationContexts(
+    ScuOperationFlags scuOperation,
+    const std::set<std::string>& acceptedStorageSopClasses,
+    const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes)
   {
     assert(association_.get() != NULL);
-    association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
-    association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+
+    if ((scuOperation & ScuOperationFlags_Echo) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindPatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_FindWorklist) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MovePatient) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_MoveStudy) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    }
+
+    if ((scuOperation & ScuOperationFlags_Get) != 0)
+    {
+      association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel);
+      association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel);
+
+      if (acceptedStorageSopClasses.size() == 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls); // the acceptedStorageSopClassUids should always be defined for a C-Get
+      }
+
+      for (std::set<std::string>::const_iterator it = acceptedStorageSopClasses.begin(); it != acceptedStorageSopClasses.end(); ++it)
+      {
+        association_->ProposePresentationContext(*it, proposedStorageTransferSyntaxes, DicomAssociationRole_Scp);
+      }
+    }
   }
     
 
@@ -350,6 +393,21 @@
     }
   }
 
+  void MoveProgressCallback(void *callbackData,
+                            T_DIMSE_C_MoveRQ *request,
+                            int responseCount, 
+                            T_DIMSE_C_MoveRSP *response)
+  {
+    DicomControlUserConnection::IProgressListener* listener = reinterpret_cast<DicomControlUserConnection::IProgressListener*>(callbackData);
+    if (listener)
+    {
+      listener->OnProgressUpdated(response->NumberOfRemainingSubOperations,
+                                  response->NumberOfCompletedSubOperations,
+                                  response->NumberOfFailedSubOperations,
+                                  response->NumberOfWarningSubOperations);
+    }
+  }
+
     
   void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
                                                 ResourceType level,
@@ -391,7 +449,8 @@
     DcmDataset* statusDetail = NULL;
     DcmDataset* responseIdentifiers = NULL;
     OFCondition cond = DIMSE_moveUser(
-      &association_->GetDcmtkAssociation(), presID, &request, dataset, /*moveCallback*/ NULL, NULL,
+      &association_->GetDcmtkAssociation(), presID, &request, dataset, 
+      (progressListener_ != NULL ? MoveProgressCallback : NULL), progressListener_,
       /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
       /*opt_dimse_timeout*/ parameters_.GetTimeout(),
       &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL,
@@ -413,6 +472,14 @@
       OFString str;
       CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl
                          << DIMSE_dumpMessage(str, response, DIMSE_INCOMING);
+
+      if (progressListener_ != NULL)
+      {
+        progressListener_->OnProgressUpdated(response.NumberOfRemainingSubOperations,
+                                             response.NumberOfCompletedSubOperations,
+                                             response.NumberOfFailedSubOperations,
+                                             response.NumberOfWarningSubOperations);
+      }
     }
     
     /**
@@ -445,11 +512,240 @@
   }
     
 
-  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
+  void DicomControlUserConnection::Get(const DicomMap& findResult,
+                                       CGetInstanceReceivedCallback instanceReceivedCallback,
+                                       void* callbackContext)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    std::unique_ptr<ParsedDicomFile> query(
+      ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* queryDataset = query->GetDcmtkObject().getDataset();
+
+    std::string remoteAet;
+    std::string remoteIp;
+    std::string calledAet;
+
+    association_->GetAssociationParameters(remoteAet, remoteIp, calledAet);
+
+    const char* sopClass = NULL;
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent();
+    ResourceType level = StringToResourceType(tmp.c_str());
+    switch (level)
+    {
+      case ResourceType_Patient:
+        sopClass = UID_GETPatientRootQueryRetrieveInformationModel;
+        break;
+      case ResourceType_Study:
+      case ResourceType_Series:
+      case ResourceType_Instance:
+        sopClass = UID_GETStudyRootQueryRetrieveInformationModel;
+        break;
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (cgetPresID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomGetUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_Message msgGetRequest;
+    memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest));
+    msgGetRequest.CommandField = DIMSE_C_GET_RQ;
+
+    T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ);
+    request->MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN);
+    request->Priority = DIMSE_PRIORITY_MEDIUM;
+    request->DataSetType = DIMSE_DATASET_PRESENT;
+
+    {
+      OFString str;
+      CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl
+                         << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID);
+    }
+    
+    OFCondition cond = DIMSE_sendMessageUsingMemoryData(
+          &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset,
+          NULL, NULL, NULL /* commandSet */);
+      
+    if (cond.bad())
+    {
+        OFString tempStr;
+        CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond);
+        // return cond;
+    }
+
+    // equivalent to handleCGETSession in DCMTK
+    bool continueSession = true;
+
+    // As long we want to continue (usually, as long as we receive more objects,
+    // i.e. the final C-GET response has not arrived yet)
+    while (continueSession)
+    {
+        T_DIMSE_Message rsp;
+        // Make sure everything is zeroed (especially options)
+        memset((char*)&rsp, 0, sizeof(rsp));
+
+        // DcmDataset* statusDetail = NULL;
+        T_ASC_PresentationContextID cmdPresId = 0;
+
+        OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &rsp,
+                                                  NULL /* statusDetail */,
+                                                  NULL /* not interested in the command set */);
+
+        if (result.bad())
+        {
+          OFString tempStr;
+          CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result);
+          // delete statusDetail;
+          break;  // TODO: return value
+        }
+        // Handle C-GET Response
+        if (rsp.CommandField == DIMSE_C_GET_RSP)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          if (progressListener_ != NULL)
+          {
+            progressListener_->OnProgressUpdated(rsp.msg.CGetRSP.NumberOfRemainingSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfCompletedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfFailedSubOperations,
+                                                  rsp.msg.CGetRSP.NumberOfWarningSubOperations);
+          }
+
+          if (rsp.msg.CGetRSP.DimseStatus == 0x0000)  // final success message
+          {
+            continueSession = false;
+          }
+        }
+        // Handle C-STORE Request
+        else if (rsp.CommandField == DIMSE_C_STORE_RQ)
+        {
+          {
+            OFString tempStr;
+            CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl
+              << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId);
+          }
+
+          T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ);
+
+          // Check if dataset is announced correctly
+          if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL)
+          {
+            CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset";
+          }
+
+          Uint16 desiredCStoreReturnStatus = 0;
+          DcmDataset* dataObject = NULL;
+
+          // Receive dataset
+          result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()),
+                                                  (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                                  parameters_.GetTimeout(),
+                                                  &cmdPresId,
+                                                  &dataObject,
+                                                  NULL, NULL);
+
+          if (result.bad())
+          {
+            LOG(WARNING) << "C-GET SCU handler: Failed to receive dataset: " << result.text();
+            desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand;
+          }
+          else
+          {
+            // callback the OrthancServer with the received data
+            if (instanceReceivedCallback != NULL)
+            {
+              desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet);
+            }
+
+            // send the Store response
+            T_DIMSE_Message storeResponse;
+            memset((char*)&storeResponse, 0, sizeof(storeResponse));
+            storeResponse.CommandField         = DIMSE_C_STORE_RSP;
+
+            T_DIMSE_C_StoreRSP& storeRsp       = storeResponse.msg.CStoreRSP;
+            storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID;
+            storeRsp.DimseStatus               = desiredCStoreReturnStatus;
+            storeRsp.DataSetType               = DIMSE_DATASET_NULL;
+
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID));
+            OFStandard::strlcpy(
+                storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID));
+            storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID;
+
+            result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), 
+                                                      cmdPresId, 
+                                                      &storeResponse, NULL /* statusDetail */, NULL /* dataObject */,
+                                                      NULL, NULL, NULL /* commandSet */);
+            if (result.bad())
+            {
+              continueSession = false;
+            }
+            else
+            {
+              OFString tempStr;
+              CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl
+                << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId);
+            }
+          }
+        }
+        // Handle other DIMSE command (error since other command than GET/STORE not expected)
+        else
+        {
+          CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x"
+                               << std::hex << std::setfill('0') << std::setw(4)
+                               << static_cast<unsigned int>(rsp.CommandField);
+          
+          result          = DIMSE_BADCOMMANDTYPE;
+          continueSession = false;
+        }
+
+        // delete statusDetail; // should be NULL if not existing or added to response list
+        // statusDetail = NULL;
+    }
+    /* All responses received or break signal occurred */
+
+    // return result;
+}
+
+
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation) :
     parameters_(params),
-    association_(new DicomAssociation)
+    association_(new DicomAssociation),
+    progressListener_(NULL)
   {
-    SetupPresentationContexts();
+    assert((scuOperation & ScuOperationFlags_Get) == 0);  // you must provide acceptedStorageSopClassUids for Get SCU
+    std::set<std::string> emptyStorageSopClasses;
+    std::list<DicomTransferSyntax> emptyStorageTransferSyntaxes;
+
+    SetupPresentationContexts(scuOperation, emptyStorageSopClasses, emptyStorageTransferSyntaxes);
+  }
+    
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                                         ScuOperationFlags scuOperation,
+                                                         const std::set<std::string>& acceptedStorageSopClasses,
+                                                         const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    progressListener_(NULL)
+  {
+    SetupPresentationContexts(scuOperation, acceptedStorageSopClasses, proposedStorageTransferSyntaxes);
   }
     
 
--- a/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Mon Dec 09 19:41:24 2024 +0100
@@ -37,13 +37,50 @@
 {
   class DicomAssociation;  // Forward declaration for PImpl design pattern
   
+  typedef uint16_t (*CGetInstanceReceivedCallback)(void *callbackContext,
+                                                   DcmDataset& dataset,
+                                                   const std::string& remoteAet,
+                                                   const std::string& remoteIp,
+                                                   const std::string& calledAet
+                                                   );
+
+
+  enum ScuOperationFlags
+  {
+    ScuOperationFlags_Echo = 1 << 0,
+    ScuOperationFlags_FindPatient = 1 << 1,
+    ScuOperationFlags_FindStudy = 1 << 2,
+    ScuOperationFlags_FindWorklist = 1 << 3,
+    ScuOperationFlags_MoveStudy = 1 << 4,
+    ScuOperationFlags_MovePatient = 1 << 5,
+    // C-Store is not using DicomControlUserConnection but DicomStoreUserConnection
+    ScuOperationFlags_Get = 1 << 6,
+
+    ScuOperationFlags_Find = ScuOperationFlags_FindPatient | ScuOperationFlags_FindStudy | ScuOperationFlags_FindWorklist,
+    ScuOperationFlags_Move = ScuOperationFlags_MoveStudy | ScuOperationFlags_MovePatient,
+    ScuOperationFlags_All = ScuOperationFlags_Echo | ScuOperationFlags_Find | ScuOperationFlags_Move | ScuOperationFlags_Get
+  };
+
   class DicomControlUserConnection : public boost::noncopyable
   {
+  public:
+    class IProgressListener
+    {
+    public:
+      virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                     uint16_t nbCompletedSubOperations,
+                                     uint16_t nbFailedSubOperations,
+                                     uint16_t nbWarningSubOperations) = 0;
+    };
+
   private:
     DicomAssociationParameters           parameters_;
     boost::shared_ptr<DicomAssociation>  association_;
+    IProgressListener*                   progressListener_;
 
-    void SetupPresentationContexts();
+    void SetupPresentationContexts(ScuOperationFlags scuOperation,
+                                   const std::set<std::string>& acceptedStorageSopClasses,
+                                   const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes);
 
     void FindInternal(DicomFindAnswers& answers,
                       DcmDataset* dataset,
@@ -56,8 +93,14 @@
                       const DicomMap& fields);
     
   public:
-    explicit DicomControlUserConnection(const DicomAssociationParameters& params);
-    
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, ScuOperationFlags scuOperation);
+
+    // specific constructor for CGet SCU
+    explicit DicomControlUserConnection(const DicomAssociationParameters& params, 
+                                        ScuOperationFlags scuOperation,
+                                        const std::set<std::string>& acceptedStorageSopClasses,
+                                        const std::list<DicomTransferSyntax>& proposedStorageTransferSyntaxes);
+
     const DicomAssociationParameters& GetParameters() const
     {
       return parameters_;
@@ -67,11 +110,20 @@
 
     bool Echo();
 
+    void SetProgressListener(IProgressListener* progressListener)
+    {
+      progressListener_ = progressListener;
+    }
+
     void Find(DicomFindAnswers& result,
               ResourceType level,
               const DicomMap& originalFields,
               bool normalize);
 
+    void Get(const DicomMap& getQuery,
+             CGetInstanceReceivedCallback instanceReceivedCallback,
+             void* callbackContext);
+
     void Move(const std::string& targetAet,
               ResourceType level,
               const DicomMap& findResult);
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -57,7 +57,7 @@
                                                      bool hasPreferred,
                                                      DicomTransferSyntax preferred)
   {
-    typedef std::list< std::set<DicomTransferSyntax> >  GroupsOfSyntaxes;
+    typedef std::list< std::list<DicomTransferSyntax> >  GroupsOfSyntaxes;
 
     GroupsOfSyntaxes  groups;
 
@@ -65,8 +65,8 @@
     for (std::set<DicomTransferSyntax>::const_iterator
            it = sourceSyntaxes.begin(); it != sourceSyntaxes.end(); ++it)
     {
-      std::set<DicomTransferSyntax> group;
-      group.insert(*it);
+      std::list<DicomTransferSyntax> group;
+      group.push_back(*it);
       groups.push_back(group);
     }
 
@@ -74,8 +74,8 @@
     if (hasPreferred &&
         sourceSyntaxes.find(preferred) == sourceSyntaxes.end())
     {
-      std::set<DicomTransferSyntax> group;
-      group.insert(preferred);
+      std::list<DicomTransferSyntax> group;
+      group.push_back(preferred);
       groups.push_back(group);
     }
 
@@ -89,7 +89,7 @@
         DicomTransferSyntax_BigEndianExplicit
       };
 
-      std::set<DicomTransferSyntax> group;
+      std::list<DicomTransferSyntax> group;
 
       for (size_t i = 0; i < N; i++)
       {
@@ -97,7 +97,7 @@
         if (sourceSyntaxes.find(syntax) == sourceSyntaxes.end() &&
             (!hasPreferred || preferred != syntax))
         {
-          group.insert(syntax);
+          group.push_back(syntax);
         }
       }
 
--- a/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Mon Dec 09 19:41:24 2024 +0100
@@ -28,6 +28,7 @@
 
 #include <boost/noncopyable.hpp>
 #include <string>
+#include <list>
 
 namespace Orthanc
 {
@@ -47,13 +48,23 @@
                                   const std::string& calledAet,
                                   DicomRequestType type) = 0;
 
+    // Get the set of TransferSyntaxes that are accepted when negotiation a C-Store association, acting as SCP when it has been initiated by the C-Store SCU.
     virtual void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& target,
                                              const std::string& remoteIp,
                                              const std::string& remoteAet,
                                              const std::string& calledAet) = 0;
-    
+
+    // Get the list of TransferSyntaxes that are proposed when initiating a C-Store SCP which actually only happens in a C-Get SCU
+    virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target,
+                                                    const std::string& remoteIp,
+                                                    const std::string& remoteAet,
+                                                    const std::string& calledAet) = 0;
+
     virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                            const std::string& remoteAet,
                                            const std::string& calledAet) = 0;
+
+    virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses,
+                                       size_t maxCount) = 0;
   };
 }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -502,13 +502,21 @@
           }
           else                                         // see dcmqrsrv.cc lines 839 - 876
           {
+            std::set<std::string> acceptedStorageClasses;
+
+            if (server.HasApplicationEntityFilter())
+            {
+              server.GetApplicationEntityFilter().GetAcceptedSopClasses(acceptedStorageClasses, 0);
+            }
+
             /* accept storage syntaxes with proposed role */
             int npc = ASC_countPresentationContexts(assoc->params);
             for (int i = 0; i < npc; i++)
             {
               T_ASC_PresentationContext pc;
               ASC_getPresentationContext(assoc->params, i, &pc);
-              if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
+              if (acceptedStorageClasses.find(pc.abstractSyntax) != acceptedStorageClasses.end()
+                 || (!server.HasApplicationEntityFilter() && dcmIsaStorageSOPClassUID(pc.abstractSyntax)))  // previous behavior kept for compatibility in case the server does not have an ApplicationEntityFilter
               {
                 /**
                  * We are prepared to accept whatever role the caller
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -50,6 +50,7 @@
 static const char* KEY_USE_DICOM_TLS = "UseDicomTls";
 static const char* KEY_LOCAL_AET = "LocalAet";
 static const char* KEY_TIMEOUT = "Timeout";
+static const char* KEY_RETRIEVE_METHOD = "RetrieveMethod";
 
 
 namespace Orthanc
@@ -72,6 +73,7 @@
     useDicomTls_ = false;
     localAet_.clear();
     timeout_ = 0;
+    retrieveMethod_ = RetrieveMethod_SystemDefault;
   }
 
 
@@ -308,6 +310,17 @@
     {
       timeout_ = SerializationToolbox::ReadUnsignedInteger(serialized, KEY_TIMEOUT);
     }
+
+    if (serialized.isMember(KEY_RETRIEVE_METHOD))
+    {
+      retrieveMethod_ = StringToRetrieveMethod
+        (SerializationToolbox::ReadString(serialized, KEY_RETRIEVE_METHOD));
+    }   
+    else
+    {
+      retrieveMethod_ = RetrieveMethod_SystemDefault;
+    }
+
   }
 
 
@@ -427,6 +440,7 @@
       target[KEY_USE_DICOM_TLS] = useDicomTls_;
       target[KEY_LOCAL_AET] = localAet_;
       target[KEY_TIMEOUT] = timeout_;
+      target[KEY_RETRIEVE_METHOD] = EnumerationToString(retrieveMethod_);
     }
     else
     {
@@ -521,4 +535,14 @@
   {
     return timeout_ != 0;
   }
+
+  RetrieveMethod RemoteModalityParameters::GetRetrieveMethod() const
+  {
+    return retrieveMethod_;
+  }
+
+  void RemoteModalityParameters::SetRetrieveMethod(RetrieveMethod retrieveMethod)
+  {
+    retrieveMethod_ = retrieveMethod;
+  }
 }
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Mon Dec 09 19:41:24 2024 +0100
@@ -51,6 +51,7 @@
     bool                  useDicomTls_;
     std::string           localAet_;
     uint32_t              timeout_;
+    RetrieveMethod        retrieveMethod_;   // New in Orthanc 1.12.6
     
     void Clear();
 
@@ -118,5 +119,10 @@
     uint32_t GetTimeout() const;
 
     bool HasTimeout() const;    
+
+    RetrieveMethod GetRetrieveMethod() const;
+
+    void SetRetrieveMethod(RetrieveMethod retrieveMethod);
+
   };
 }
--- a/OrthancFramework/Sources/Enumerations.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/Enumerations.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -369,6 +369,9 @@
       case ErrorCode_NoCGetHandler:
         return "No request handler factory for DICOM C-GET SCP";
 
+      case ErrorCode_DicomGetUnavailable:
+        return "DicomUserConnection: The C-GET command is not supported by the remote SCP";
+
       case ErrorCode_UnsupportedMediaType:
         return "Unsupported media type";
 
@@ -2490,6 +2493,44 @@
     }
   }
 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str)
+  {
+    if (str == "C-MOVE")
+    {
+      return RetrieveMethod_Move;
+    }
+    else if (str == "C-GET")
+    {
+      return RetrieveMethod_Get;
+    }
+    else if (str == "SystemDefault")
+    {
+      return RetrieveMethod_SystemDefault;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "RetrieveMethod can be \"C-MOVE\", \"C-GET\" or \"SystemDefault\": " + str);
+    }    
+  }
+
+  const char* EnumerationToString(RetrieveMethod method)
+  {
+    switch (method)
+    {
+      case RetrieveMethod_Get:
+        return "C-GET";
+
+      case RetrieveMethod_Move:
+        return "C-MOVE";
+
+      case RetrieveMethod_SystemDefault:
+        return "SystemDefault";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
 
 
--- a/OrthancFramework/Sources/Enumerations.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/Enumerations.h	Mon Dec 09 19:41:24 2024 +0100
@@ -233,6 +233,7 @@
     ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     ErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     ErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    ErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
     ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
     ErrorCode_START_PLUGINS = 1000000
   };
@@ -792,6 +793,14 @@
     ResourceType_Instance = 4
   };
 
+  enum RetrieveMethod                         // new in Orthanc 1.12.6
+  {
+    RetrieveMethod_Move = 1,
+    RetrieveMethod_Get = 2,
+
+    RetrieveMethod_SystemDefault = 65535
+  };
+
 
   ORTHANC_PUBLIC
   const char* EnumerationToString(ErrorCode code);
@@ -848,6 +857,9 @@
   const char* EnumerationToString(DicomToJsonFormat format);
 
   ORTHANC_PUBLIC
+  const char* EnumerationToString(RetrieveMethod method);
+
+  ORTHANC_PUBLIC
   Encoding StringToEncoding(const char* encoding);
 
   ORTHANC_PUBLIC
@@ -946,4 +958,7 @@
 
   ORTHANC_PUBLIC
   void GetAllDicomTransferSyntaxes(std::set<DicomTransferSyntax>& target);
+
+  ORTHANC_PUBLIC 
+  RetrieveMethod StringToRetrieveMethod(const std::string& str);
 }
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -49,13 +49,18 @@
     // For pausing/canceling/ending jobs: This method must release allocated resources
     virtual void Stop(JobStopReason reason) = 0;
 
-    virtual float GetProgress() = 0;
+    virtual float GetProgress() const = 0;
 
-    virtual void GetJobType(std::string& target) = 0;
+    virtual bool NeedsProgressUpdateBetweenSteps() const // only for jobs whose progress is updated by outside events (like C-Move and C-Get)
+    {
+      return false;
+    }
+
+    virtual void GetJobType(std::string& target) const = 0;
     
-    virtual void GetPublicContent(Json::Value& value) = 0;
+    virtual void GetPublicContent(Json::Value& value) const = 0;
 
-    virtual bool Serialize(Json::Value& value) = 0;
+    virtual bool Serialize(Json::Value& value) const = 0;
 
     // This function can only be called if the job has reached its
     // "success" state
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -54,7 +54,8 @@
                    const JobStatus& status,
                    const boost::posix_time::ptime& creationTime,
                    const boost::posix_time::ptime& lastStateChangeTime,
-                   const boost::posix_time::time_duration& runtime) :
+                   const boost::posix_time::time_duration& runtime,
+                   const IJob& job) :
     id_(id),
     priority_(priority),
     state_(state),
@@ -68,11 +69,16 @@
     if (state_ == JobState_Running)
     {
       float ms = static_cast<float>(runtime_.total_milliseconds());
+      if (job.NeedsProgressUpdateBetweenSteps())
+      {
+        status_.UpdateProgress(job);
+      }
 
-      if (status_.GetProgress() > 0.01f &&
+      float progress = status_.GetProgress();
+
+      if (progress > 0.01f &&
           ms > 0.01f)
       {
-        float progress = status_.GetProgress();
         long long remaining = boost::math::llround(ms / progress * (1.0f - progress));
         eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
         hasEta_ = true;
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.h	Mon Dec 09 19:41:24 2024 +0100
@@ -51,7 +51,8 @@
             const JobStatus& status,
             const boost::posix_time::ptime& creationTime,
             const boost::posix_time::ptime& lastStateChangeTime,
-            const boost::posix_time::time_duration& runtime) ORTHANC_LOCAL;
+            const boost::posix_time::time_duration& runtime,
+            const IJob& job) ORTHANC_LOCAL;
 
     JobInfo();
 
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -41,7 +41,7 @@
   
   JobStatus::JobStatus(ErrorCode code,
                        const std::string& details,
-                       IJob& job) :
+                       const IJob& job) :
     errorCode_(code),
     progress_(job.GetProgress()),
     publicContent_(Json::objectValue),
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h	Mon Dec 09 19:41:24 2024 +0100
@@ -44,7 +44,7 @@
 
     JobStatus(ErrorCode code,
               const std::string& details,
-              IJob& job);
+              const IJob& job);
 
     ErrorCode GetErrorCode() const
     {
@@ -61,6 +61,11 @@
       return progress_;
     }
 
+    void UpdateProgress(const IJob& job)
+    {
+      progress_ = job.GetProgress();
+    }
+
     const std::string& GetJobType() const
     {
       return jobType_;
--- a/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -132,7 +132,10 @@
 
       if (running.IsValid())
       {
-        CLOG(INFO, JOBS) << "Executing job with priority " << running.GetPriority()
+        std::string jobType;
+        running.GetJob().GetJobType(jobType);
+
+        CLOG(INFO, JOBS) << "Executing " << jobType << " job with priority " << running.GetPriority()
                          << " in worker thread " << workerIndex << ": " << running.GetId();
 
         while (engine->IsRunning())
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -223,8 +223,14 @@
       lastStateChangeTime_ = time;
     }
 
-    const boost::posix_time::time_duration& GetRuntime() const
+    boost::posix_time::time_duration GetRuntime() const
     {
+      if (state_ == JobState_Running)
+      {
+        const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+        return now - lastStateChangeTime_;
+      }
+
       return runtime_;
     }
 
@@ -644,7 +650,8 @@
                        handler.GetLastStatus(),
                        handler.GetCreationTime(),
                        handler.GetLastStateChangeTime(),
-                       handler.GetRuntime());
+                       handler.GetRuntime(),
+                       handler.GetJob());
       return true;
     }
   }
@@ -792,7 +799,10 @@
         }
       }
 
-      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+      std::string jobType;
+      handler->GetJob().GetJobType(jobType);
+
+      LOG(INFO) << "New " << jobType << " job submitted with priority " << priority << ": " << id;
 
       if (observer_ != NULL)
       {
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -398,7 +398,7 @@
   }
 
 
-  float SequenceOfOperationsJob::GetProgress()
+  float SequenceOfOperationsJob::GetProgress() const
   {
     boost::mutex::scoped_lock lock(mutex_);
       
@@ -406,13 +406,13 @@
             static_cast<float>(operations_.size() + 1));
   }
 
-  void SequenceOfOperationsJob::GetJobType(std::string& target)
+  void SequenceOfOperationsJob::GetJobType(std::string& target) const
   {
     target = "SequenceOfOperations";
   }
 
 
-  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value)
+  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value) const
   {
     boost::mutex::scoped_lock lock(mutex_);
 
@@ -421,7 +421,7 @@
   }
 
 
-  bool SequenceOfOperationsJob::Serialize(Json::Value& value)
+  bool SequenceOfOperationsJob::Serialize(Json::Value& value) const
   {
     boost::mutex::scoped_lock lock(mutex_);
 
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -54,7 +54,7 @@
 
     std::string                       description_;
     bool                              done_;
-    boost::mutex                      mutex_;
+    mutable boost::mutex              mutex_;
     std::vector<Operation*>           operations_;
     size_t                            current_;
     boost::condition_variable         operationAdded_;
@@ -117,13 +117,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE;
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -137,7 +137,7 @@
   }
 
 
-  float SetOfCommandsJob::GetProgress()
+  float SetOfCommandsJob::GetProgress() const
   {
     if (commands_.empty())
     {
@@ -237,13 +237,13 @@
   static const char* KEY_COMMANDS = "Commands";
 
   
-  void SetOfCommandsJob::GetPublicContent(Json::Value& value)
+  void SetOfCommandsJob::GetPublicContent(Json::Value& value) const
   {
     value[KEY_DESCRIPTION] = GetDescription();
   }    
 
 
-  bool SetOfCommandsJob::Serialize(Json::Value& target)
+  bool SetOfCommandsJob::Serialize(Json::Value& target) const
   {
     target = Json::objectValue;
 
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -92,7 +92,7 @@
     
     virtual void Start() ORTHANC_OVERRIDE;
     
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
     bool IsStarted() const;
 
@@ -100,9 +100,9 @@
       
     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -209,7 +209,7 @@
   static const char* KEY_FAILED_INSTANCES = "FailedInstances";
   static const char* KEY_PARENT_RESOURCES = "ParentResources";
 
-  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
+  void SetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     SetOfCommandsJob::GetPublicContent(target);
     target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
@@ -222,7 +222,7 @@
   }
 
 
-  bool SetOfInstancesJob::Serialize(Json::Value& target) 
+  bool SetOfInstancesJob::Serialize(Json::Value& target) const 
   {
     if (SetOfCommandsJob::Serialize(target))
     {
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -77,8 +77,8 @@
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
-    virtual void GetPublicContent(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& target) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancFramework/Sources/Toolbox.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/Sources/Toolbox.h	Mon Dec 09 19:41:24 2024 +0100
@@ -262,7 +262,7 @@
       }
     }
 
-    // returns true if all element of 'needles' are found in 'haystack'
+    // returns the elements that are both in a and b
     template <typename T> static void GetIntersection(std::set<T>& target, const std::set<T>& a, const std::set<T>& b)
     {
       target.clear();
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -103,24 +103,24 @@
     {
     }
 
-    virtual float GetProgress() ORTHANC_OVERRIDE
+    virtual float GetProgress() const ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
@@ -199,7 +199,7 @@
     {
     }
 
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
--- a/OrthancServer/CMakeLists.txt	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/CMakeLists.txt	Mon Dec 09 19:41:24 2024 +0100
@@ -130,6 +130,8 @@
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/ArchiveJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/CleaningInstancesJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomModalityStoreJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp
+  ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomGetScuJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/DicomMoveScuJob.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/LuaJobManager.cpp
   ${CMAKE_SOURCE_DIR}/Sources/ServerJobs/MergeStudyJob.cpp
@@ -175,6 +177,7 @@
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/DatabaseLookupTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/LuaServerTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/PluginsTests.cpp
+  ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerConfigTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerIndexTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/ServerJobsTests.cpp
   ${CMAKE_SOURCE_DIR}/UnitTestsSources/SizeOfTests.cpp
--- a/OrthancServer/Plugins/Engine/PluginsJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -147,7 +147,7 @@
     }
   }
 
-  float PluginsJob::GetProgress()
+  float PluginsJob::GetProgress() const
   {
     return parameters_.getProgress(parameters_.job);
   }
@@ -194,7 +194,7 @@
     };
   }
   
-  void PluginsJob::GetPublicContent(Json::Value& value)
+  void PluginsJob::GetPublicContent(Json::Value& value) const
   {
     if (parameters_.getContent != NULL)
     {
@@ -232,7 +232,7 @@
     }
   }
 
-  bool PluginsJob::Serialize(Json::Value& value)
+  bool PluginsJob::Serialize(Json::Value& value) const
   {
     if (parameters_.getSerialized != NULL)
     {
--- a/OrthancServer/Plugins/Engine/PluginsJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -58,16 +58,16 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = type_;
     }
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Mon Dec 09 19:41:24 2024 +0100
@@ -323,6 +323,7 @@
     OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     OrthancPluginErrorCode_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
     OrthancPluginErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    OrthancPluginErrorCode_DicomGetUnavailable = 2045    /*!< DicomUserConnection: The C-GET command is not supported by the remote SCP */,
     OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -29,10 +29,11 @@
 #include "../../../../OrthancFramework/Sources/OrthancException.h"
 
 #include "../Common/OrthancPluginCppWrapper.h"
-
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
 
 DicomFilter::DicomFilter() :
-  hasAcceptedTransferSyntaxes_(false)
+  hasAcceptedTransferSyntaxes_(false),
+  hasAcceptedStorageClasses_(false)
 {
   {
     OrthancPlugins::OrthancConfiguration config;
@@ -200,6 +201,18 @@
 }
 
 
+void DicomFilter::GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target,
+                                                     const std::string& remoteIp,
+                                                     const std::string& remoteAet,
+                                                     const std::string& calledAet)
+{
+  // default TS
+  target.push_back(Orthanc::DicomTransferSyntax_LittleEndianExplicit);
+  target.push_back(Orthanc::DicomTransferSyntax_LittleEndianImplicit);
+  target.push_back(Orthanc::DicomTransferSyntax_BigEndianExplicit);
+}
+
+
 bool DicomFilter::IsUnknownSopClassAccepted(const std::string& remoteIp,
                                             const std::string& remoteAet,
                                             const std::string& calledAet)
@@ -207,3 +220,47 @@
   boost::shared_lock<boost::shared_mutex>  lock(mutex_);
   return unknownSopClassAccepted_;
 }
+
+
+void DicomFilter::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount)
+{
+  boost::unique_lock<boost::shared_mutex>  lock(mutex_);
+
+  if (!hasAcceptedStorageClasses_)
+  {
+    Json::Value jsonSopClasses;
+
+    if (!OrthancPlugins::RestApiGet(jsonSopClasses, "/tools/accepted-sop-classes", false) ||
+        jsonSopClasses.type() != Json::arrayValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+    else
+    {
+      for (Json::Value::ArrayIndex i = 0; i < jsonSopClasses.size(); i++)
+      {
+        if (jsonSopClasses[i].type() != Json::stringValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+        else
+        {
+          acceptedStorageClasses_.insert(jsonSopClasses[i].asString());
+        }
+      }
+    }
+
+    hasAcceptedStorageClasses_ = true;
+  }
+
+  std::set<std::string>::const_iterator it = acceptedStorageClasses_.begin();
+    size_t count = 0;
+
+  while (it != acceptedStorageClasses_.end() && (maxCount == 0 || count < maxCount))
+  {
+    sopClasses.insert(*it);
+    count++;
+    it++;
+  }
+
+}
\ No newline at end of file
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/DicomFilter.h	Mon Dec 09 19:41:24 2024 +0100
@@ -44,6 +44,8 @@
 
   bool hasAcceptedTransferSyntaxes_;
   std::set<Orthanc::DicomTransferSyntax>  acceptedTransferSyntaxes_;
+  bool hasAcceptedStorageClasses_;
+  std::set<std::string>                   acceptedStorageClasses_;
 
 public:
   DicomFilter();
@@ -62,7 +64,15 @@
                                            const std::string& remoteAet,
                                            const std::string& calledAet) ORTHANC_OVERRIDE;
 
+  virtual void GetProposedStorageTransferSyntaxes(std::list<Orthanc::DicomTransferSyntax>& target,
+                                                  const std::string& remoteIp,
+                                                  const std::string& remoteAet,
+                                                  const std::string& calledAet) ORTHANC_OVERRIDE;
+
+
   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                          const std::string& remoteAet,
                                          const std::string& calledAet) ORTHANC_OVERRIDE;
+  
+  virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) ORTHANC_OVERRIDE;
 };
--- a/OrthancServer/Resources/Configuration.json	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Resources/Configuration.json	Mon Dec 09 19:41:24 2024 +0100
@@ -202,6 +202,32 @@
   // SOP classes (aka. "promiscuous mode")
   "UnknownSopClassAccepted" : false,
 
+  // The list of accepted Storage SOP classes.
+  // If empty or not defined, this list defaults
+  // to all storage classes defined in DCMTK in case of 
+  // C-STORE SCP and to a reduced list of 120 common storage
+  // classes in case of C-GET SCU.
+  // Each entry can contain wildcards ("?" or "*") to add
+  // subsets of SOP classes that are defined in DCMTK.
+  // If you want to add a a SOP class that is not defined in
+  // DCMTK, you must add it explicitely.
+  // (new in Orthanc 1.12.6)
+  // "AcceptedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
+
+  // The list of rejected Storage SOP classes.
+  // This configuration is only meaningful if
+  // "AcceptedSopClasses" is using regular expressions
+  // or if it is not defined.
+  // Each entry can contain wildcards ("?" or "*").
+  // (new in Orthanc 1.12.6)
+  // "RejectedSopClasses" : [
+  //   "1.2.840.10008.5.1.4.1.1.2",
+  //   "1.2.840.10008.5.1.4.1.1.4"
+  // ]
+
   // Set the timeout (in seconds) after which the DICOM associations
   // are closed by the Orthanc SCP (server) if no further DIMSE
   // command is received from the SCU (client).
@@ -459,6 +485,10 @@
      * for Orthanc when initiating an SCU to this very specific
      * modality. Similarly, "Timeout" allows one to overwrite the
      * global value "DicomScuTimeout" on a per-modality basis.
+     *
+     * The "RetrieveMethod" option allows one to overwrite the global
+     * "DicomDefaultRetrieveMethod" configuration option for this
+     * specific modality. (Allowed values: "C-MOVE" or "C-GET").
      **/
     //"untrusted" : {
     //  "AET" : "ORTHANC",
@@ -475,7 +505,8 @@
     //  "AllowTranscoding" : true,         // new in 1.7.0
     //  "UseDicomTls" : false,             // new in 1.9.0
     //  "LocalAet" : "HELLO",              // new in 1.9.0
-    //  "Timeout" : 60                     // new in 1.9.1
+    //  "Timeout" : 60,                    // new in 1.9.1
+    //  "RetrieveMethod": "C-MOVE"         // new in 1.12.6
     //}
   },
 
@@ -489,6 +520,13 @@
   // accept C-FIND requests from Orthanc (new in Orthanc 1.8.1).
   "DicomEchoChecksFind" : false,
 
+  // Wheter Orthanc uses C-MOVE or C-GET to retrieve a resource after
+  // a C-Find (when calling /queries/.../retrieve).
+  // This configuration can be overriden for each modality by providing
+  // "RetrieveMethod" in the "DicomModalities" entry.
+  // (new in Orthanc 1.12.6)
+  "DicomDefaultRetrieveMethod" : "C-MOVE",
+
   // The timeout (in seconds) after which the DICOM associations are
   // considered as closed by the Orthanc SCU (client) if the remote
   // DICOM SCP (server) does not answer.
--- a/OrthancServer/Resources/DicomConformanceStatement.txt	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Resources/DicomConformanceStatement.txt	Mon Dec 09 19:41:24 2024 +0100
@@ -209,6 +209,16 @@
   MOVEStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.2
 
 
+-------------------
+Get SCU Conformance
+-------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Get:
+
+  GETPatientRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.1.3
+  GETStudyRootQueryRetrieveInformationModel      | 1.2.840.10008.5.1.4.1.2.2.3
+
+
 -----------------
 Transfer Syntaxes
 -----------------
--- a/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -165,6 +165,7 @@
      * 2. Select the preferred transfer syntaxes, which corresponds to
      * the source transfer syntax, plus all the uncompressed transfer
      * syntaxes if transcoding is enabled.
+     * This way, we minimize the transcoding on our side.
      **/
     
     std::list<DicomTransferSyntax> preferred;
@@ -208,7 +209,16 @@
       }
     }
 
-    // No preferred syntax was accepted
+    // No preferred syntax was accepted but, if a PC has been accepted, it means that we have accepted a TS.
+    // This maybe means that we need to transcode twice on our side (from a compressed format to another compressed format).
+    if (allowTranscoding && accepted.size() >  0)
+    {
+      Accepted::const_iterator it = accepted.begin();
+      selectedPresentationId = it->second;
+      selectedSyntax = it->first;
+      return true;
+    }
+
     return false;
   }                                                           
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -36,6 +36,7 @@
 #include "../ServerContext.h"
 #include "../ServerJobs/DicomModalityStoreJob.h"
 #include "../ServerJobs/DicomMoveScuJob.h"
+#include "../ServerJobs/DicomGetScuJob.h"
 #include "../ServerJobs/OrthancPeerStoreJob.h"
 #include "../ServerToolbox.h"
 #include "../StorageCommitmentReports.h"
@@ -56,6 +57,7 @@
   static const char* const KEY_CHECK_FIND = "CheckFind";
   static const char* const SOP_CLASS_UID = "SOPClassUID";
   static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
+  static const char* const KEY_RETRIEVE_METHOD = "RetrieveMethod";
   
   static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
   {
@@ -155,24 +157,31 @@
                           const DicomAssociationParameters& parameters,
                           const Json::Value& body)
   {
-    DicomControlUserConnection connection(parameters);
-
+    bool checkFind = false;
+    
+    if (body.type() == Json::objectValue &&
+        body.isMember(KEY_CHECK_FIND))
+    {
+      checkFind = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+      checkFind = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
+    }
+
+    ScuOperationFlags operations = ScuOperationFlags_Echo;
+    
+    if (checkFind)
+    {
+      operations = static_cast<ScuOperationFlags>(operations | ScuOperationFlags_Find);
+    }
+
+    DicomControlUserConnection connection(parameters, operations);
     if (connection.Echo())
     {
-      bool find = false;
-      
-      if (body.type() == Json::objectValue &&
-          body.isMember(KEY_CHECK_FIND))
-      {
-        find = SerializationToolbox::ReadBoolean(body, KEY_CHECK_FIND);
-      }
-      else
-      {
-        OrthancConfiguration::ReaderLock lock;
-        find = lock.GetConfiguration().GetBooleanParameter("DicomEchoChecksFind", false);
-      }
-
-      if (find)
+
+      if (checkFind)
       {
         // Issue a C-FIND request at the study level about a random Study Instance UID
         const std::string studyInstanceUid = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study);
@@ -385,7 +394,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindPatient);
       FindPatient(answers, connection, fields);
     }
 
@@ -428,7 +437,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindStudy(answers, connection, fields);
     }
 
@@ -472,7 +481,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindSeries(answers, connection, fields);
     }
 
@@ -517,7 +526,7 @@
     DicomFindAnswers answers(false);
 
     {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
+      DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_FindStudy);
       FindInstance(answers, connection, fields);
     }
 
@@ -566,7 +575,7 @@
       return;
     }
  
-    DicomControlUserConnection connection(GetAssociationParameters(call));
+    DicomControlUserConnection connection(GetAssociationParameters(call), ScuOperationFlags_Find);
     
     DicomFindAnswers patients(false);
     FindPatient(patients, connection, m);
@@ -914,12 +923,25 @@
 
     std::string targetAet;
     int timeout = -1;
-    
+
+    QueryAccessor query(call);
+
+    RetrieveMethod retrieveMethod = query.GetHandler().GetRemoteModality().GetRetrieveMethod();
+
     Json::Value body;
     if (call.ParseJsonRequest(body))
     {
+      OrthancConfiguration::ReaderLock lock;
+
       targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
       timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
+      
+      std::string strRetrieveMethod = SerializationToolbox::ReadString(body, KEY_RETRIEVE_METHOD, "");
+      
+      if (!strRetrieveMethod.empty())
+      {
+        retrieveMethod = StringToRetrieveMethod(strRetrieveMethod);
+      }
     }
     else
     {
@@ -934,45 +956,67 @@
       }
     }
     
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
-    
+    if (retrieveMethod == RetrieveMethod_SystemDefault)
+    {
+      retrieveMethod = context.GetDefaultDicomRetrieveMethod();
+    }
+
+    std::unique_ptr<DicomRetrieveScuBaseJob> job;
+
+
+    switch (retrieveMethod)
     {
-      QueryAccessor query(call);
-      job->SetTargetAet(targetAet);
-      job->SetLocalAet(query.GetHandler().GetLocalAet());
-      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
-
-      if (timeout >= 0)
+      case RetrieveMethod_Move:
       {
-        // New in Orthanc 1.7.0
-        job->SetTimeout(static_cast<uint32_t>(timeout));
-      }
-      else if (query.GetHandler().HasTimeout())
+        job.reset(new DicomMoveScuJob(context));
+        (dynamic_cast<DicomMoveScuJob*>(job.get()))->SetTargetAet(targetAet);
+
+        LOG(WARNING) << "Driving C-Move SCU on remote modality "
+                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
+                    << " to target modality " << targetAet;
+      }; break;
+      case RetrieveMethod_Get:
       {
-        // New in Orthanc 1.9.1
-        job->SetTimeout(query.GetHandler().GetTimeout());
-      }
-
-      LOG(WARNING) << "Driving C-Move SCU on remote modality "
-                   << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle()
-                   << " to target modality " << targetAet;
-
-      if (allAnswers)
+        job.reset(new DicomGetScuJob(context));
+
+        LOG(WARNING) << "Driving C-Get SCU on remote modality "
+                    << query.GetHandler().GetRemoteModality().GetApplicationEntityTitle();
+      }; break;
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    job->SetQueryFormat(OrthancRestApi::GetDicomFormat(body, DicomToJsonFormat_Short));
+
+    job->SetLocalAet(query.GetHandler().GetLocalAet());
+    job->SetRemoteModality(query.GetHandler().GetRemoteModality());
+
+    if (timeout >= 0)
+    {
+      // New in Orthanc 1.7.0
+      job->SetTimeout(static_cast<uint32_t>(timeout));
+    }
+    else if (query.GetHandler().HasTimeout())
+    {
+      // New in Orthanc 1.9.1
+      job->SetTimeout(query.GetHandler().GetTimeout());
+    }
+
+    if (allAnswers)
+    {
+      for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
       {
-        for (size_t i = 0; i < query.GetHandler().GetAnswersCount(); i++)
-        {
-          job->AddFindAnswer(query.GetHandler(), i);
-        }
+        job->AddFindAnswer(query.GetHandler(), i);
       }
-      else
-      {
-        job->AddFindAnswer(query.GetHandler(), index);
-      }
+    }
+    else
+    {
+      job->AddFindAnswer(query.GetHandler(), index);
     }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, body);
+
   }
 
 
@@ -989,6 +1033,10 @@
                        "`DicomAet` configuration option.", false)
       .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
                        "Timeout for the C-MOVE command, in seconds", false)
+      .SetRequestField(KEY_RETRIEVE_METHOD, RestApiCallDocumentation::Type_String,
+                        "Force usage of C-MOVE or C-GET to retrieve the resource.  If note defined in the payload, "
+                        "the retrieve method is defined in the DicomDefaultRetrieveMethod configuration or in "
+                        "DicomModalities->..->RetrieveMethod", false)
       .AddRequestType(MimeType_PlainText, "AET of the target modality");
   }
   
@@ -999,8 +1047,8 @@
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve one answer")
-        .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve one answer associated with the "
+        .SetSummary("Retrieve one answer with a C-MOVE or a C-GET SCU")
+        .SetDescription("Start a C-MOVE or a C-GET SCU command as a job, in order to retrieve one answer associated with the "
                         "query/retrieve operation whose identifiers are provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move")
         .SetUriArgument("index", "Index of the answer");
@@ -1012,13 +1060,15 @@
   }
 
 
+
+
   static void RetrieveAllAnswers(RestApiPostCall& call)
   {
     if (call.IsDocumentation())
     {
       DocumentRetrieveShared(call);
       call.GetDocumentation()
-        .SetSummary("Retrieve all answers")
+        .SetSummary("Retrieve all answers with C-MOVE SCU")
         .SetDescription("Start a C-MOVE SCU command as a job, in order to retrieve all the answers associated with the "
                         "query/retrieve operation whose identifier is provided in the URL: "
                         "https://orthanc.uclouvain.be/book/users/rest.html#performing-retrieve-c-move");
@@ -1536,6 +1586,48 @@
     call.GetOutput().AnswerJson(answer);
   }
 
+  void ParseMoveGetJob(DicomRetrieveScuBaseJob& job, Json::Value& request, RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue ||
+        !request.isMember(KEY_RESOURCES) ||
+        !request.isMember(KEY_LEVEL) ||
+        request[KEY_RESOURCES].type() != Json::arrayValue ||
+        request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
+                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
+    }
+
+    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+    
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+
+    const RemoteModalityParameters source =
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    job.SetQueryFormat(DicomToJsonFormat_Short);
+    job.SetLocalAet(localAet);
+    job.SetRemoteModality(source);
+
+    if (request.isMember(KEY_TIMEOUT))
+    {
+      job.SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
+    {
+      DicomMap resource;
+      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
+
+      resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
+
+      job.AddQuery(resource);      
+    }
+  }
 
   /***************************************************************************
    * DICOM C-Move SCU
@@ -1569,53 +1661,15 @@
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-
     Json::Value request;
 
-    if (!call.ParseJsonRequest(request) ||
-        request.type() != Json::objectValue ||
-        !request.isMember(KEY_RESOURCES) ||
-        !request.isMember(KEY_LEVEL) ||
-        request[KEY_RESOURCES].type() != Json::arrayValue ||
-        request[KEY_LEVEL].type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
-                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
-    }
-
-    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
-    
-    std::string localAet = Toolbox::GetJsonStringField
-      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
+
+    ParseMoveGetJob(*job, request, call);
+
     std::string targetAet = Toolbox::GetJsonStringField
       (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
-
-    const RemoteModalityParameters source =
-      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-
-    job->SetQueryFormat(DicomToJsonFormat_Short);
-    
-    // QueryAccessor query(call);
     job->SetTargetAet(targetAet);
-    job->SetLocalAet(localAet);
-    job->SetRemoteModality(source);
-
-    if (request.isMember(KEY_TIMEOUT))
-    {
-      job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
-    {
-      DicomMap resource;
-      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i], "Resources elements");
-
-      resource.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, std::string(ResourceTypeToDicomQueryRetrieveLevel(level)), false);
-
-      job->AddQuery(resource);      
-    }
 
     OrthancRestApi::GetApi(call).SubmitCommandsJob
       (call, job.release(), true /* synchronous by default */, request);
@@ -1623,6 +1677,51 @@
   }
 
 
+  /***************************************************************************
+   * DICOM C-Get SCU
+   ***************************************************************************/
+  
+  static void DicomGet(RestApiPostCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      OrthancRestApi::DocumentSubmitCommandsJob(call);
+      call.GetDocumentation()
+        .SetTag("Networking")
+        .SetSummary("Trigger C-GET SCU")
+        .SetDescription("Start a C-GET SCU command as a job, in order to retrieve DICOM resources "
+                        "from a remote DICOM modality whose identifier is provided in the URL: ")
+                        // "https://orthanc.uclouvain.be/book/users/rest.html#performing-c-move")   // TODO-GET
+        .SetRequestField(KEY_RESOURCES, RestApiCallDocumentation::Type_JsonListOfObjects,
+                         "List of queries identifying all the DICOM resources to be sent.  "
+                         "Usage of wildcards is prohibited and the query shall only contain DICOM ID tags.  "
+                         "Additionally, you may provide SOPClassesInStudy to limit the scope of the DICOM "
+                         "negotiation to certain SOPClassUID or to present uncommon SOPClassUID during "
+                         "the DICOM negotation.  By default, "
+                         "Orhanc will propose the most 120 common SOPClassUIDs.", true)
+        .SetRequestField(KEY_QUERY, RestApiCallDocumentation::Type_JsonObject,
+                         "A query object identifying all the DICOM resources to be retrieved", true)
+        .SetRequestField(KEY_LOCAL_AET, RestApiCallDocumentation::Type_String,
+                         "Local AET that is used for this commands, defaults to `DicomAet` configuration option. "
+                         "Ignored if `DicomModalities` already sets `LocalAet` for this modality.", false)
+        .SetRequestField(KEY_TIMEOUT, RestApiCallDocumentation::Type_Number,
+                         "Timeout for the C-GET command, in seconds", false)
+        .SetUriArgument("id", "Identifier of the modality of interest");
+      return;
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    Json::Value request;
+
+    std::unique_ptr<DicomGetScuJob> job(new DicomGetScuJob(context));
+
+    ParseMoveGetJob(*job, request, call);
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+    return;
+  }
+
 
   /***************************************************************************
    * Orthanc Peers => Store client
@@ -2213,7 +2312,7 @@
       DicomFindAnswers answers(true);
 
       {
-        DicomControlUserConnection connection(GetAssociationParameters(call, json));
+        DicomControlUserConnection connection(GetAssociationParameters(call, json), ScuOperationFlags_FindWorklist);
         connection.FindWorklist(answers, *query);
       }
 
@@ -2544,6 +2643,7 @@
     Register("/modalities/{id}/store", DicomStore);
     Register("/modalities/{id}/store-straight", DicomStoreStraight);  // New in 1.6.1
     Register("/modalities/{id}/move", DicomMove);
+    Register("/modalities/{id}/get", DicomGet);
     Register("/modalities/{id}/configuration", GetModalityConfiguration);  // New in 1.8.1
 
     // For Query/Retrieve
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -466,6 +466,33 @@
     AnswerAcceptedTransferSyntaxes(call);
   }
 
+  static void GetAcceptedSopClasses(RestApiGetCall& call)
+  {
+    if (call.IsDocumentation())
+    {
+      call.GetDocumentation()
+        .SetTag("System")
+        .SetSummary("Get accepted SOPClassUID")
+        .SetDescription("Get the list of SOP Class UIDs that are accepted "
+                        "by Orthanc C-STORE SCP. This corresponds to the configuration options "
+                        "`AcceptedSopClasses` and `RejectedSopClasses`.")
+        .AddAnswerType(MimeType_Json, "JSON array containing the SOP Class UIDs");
+      return;
+    }
+
+    std::set<std::string> sopClasses;
+    OrthancRestApi::GetContext(call).GetAcceptedSopClasses(sopClasses, 0);
+    
+    Json::Value json = Json::arrayValue;
+    for (std::set<std::string>::const_iterator
+           sop = sopClasses.begin(); sop != sopClasses.end(); ++sop)
+    {
+      json.append(*sop);
+    }
+    
+    call.GetOutput().AnswerJson(json);
+  }
+
 
   static void GetUnknownSopClassAccepted(RestApiGetCall& call)
   {
@@ -1188,5 +1215,8 @@
     Register("/tools/unknown-sop-class-accepted", SetUnknownSopClassAccepted);
 
     Register("/tools/labels", ListAllLabels);  // New in Orthanc 1.12.0
+
+    Register("/tools/accepted-sop-classes", GetAcceptedSopClasses);
+
   }
 }
--- a/OrthancServer/Sources/QueryRetrieveHandler.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/QueryRetrieveHandler.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -79,7 +79,7 @@
           params.SetTimeout(timeout_);
         }
         
-        DicomControlUserConnection connection(params);
+        DicomControlUserConnection connection(params, static_cast<ScuOperationFlags>(ScuOperationFlags_Find));
         connection.Find(answers_, level_, fixed, findNormalized_);
       }
 
--- a/OrthancServer/Sources/ServerContext.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -49,6 +49,9 @@
 
 #include <dcmtk/dcmdata/dcfilefo.h>
 #include <dcmtk/dcmnet/dimse.h>
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
+
+#include <boost/regex.hpp>
 
 #if HAVE_MALLOC_TRIM == 1
 #  include <malloc.h>
@@ -485,6 +488,15 @@
         lock.GetConfiguration().GetAcceptedTransferSyntaxes(acceptedTransferSyntaxes_);
 
         isUnknownSopClassAccepted_ = lock.GetConfiguration().GetBooleanParameter("UnknownSopClassAccepted", false);
+
+        // New options in Orthanc 1.12.6
+        std::list<std::string> acceptedSopClasses;
+        std::set<std::string> rejectedSopClasses;
+        lock.GetConfiguration().GetListOfStringsParameter(acceptedSopClasses, "AcceptedSopClasses");
+        lock.GetConfiguration().GetSetOfStringsParameter(rejectedSopClasses, "RejectSopClasses");
+        SetAcceptedSopClasses(acceptedSopClasses, rejectedSopClasses);
+
+        defaultDicomRetrieveMethod_ = StringToRetrieveMethod(lock.GetConfiguration().GetStringParameter("DicomDefaultRetrieveMethod", "C-MOVE"));
       }
 
       jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
@@ -2102,8 +2114,118 @@
     }
   }
 
-
-  void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes)
+  void ServerContext::SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                                            const std::set<std::string>& rejectedSopClasses)
+  {
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+    acceptedSopClasses_.clear();
+
+    size_t count = 0;
+    std::set<std::string> allDcmtkSopClassUids;
+    std::set<std::string> shortDcmtkSopClassUids;
+
+    // we actually take a list of default 120 most common storage SOP classes defined in DCMTK
+    while (dcmLongSCUStorageSOPClassUIDs[count] != NULL)
+    {
+      shortDcmtkSopClassUids.insert(dcmLongSCUStorageSOPClassUIDs[count++]);
+    }
+
+    count = 0;
+    while (dcmAllStorageSOPClassUIDs[count] != NULL)
+    {
+      allDcmtkSopClassUids.insert(dcmAllStorageSOPClassUIDs[count++]);
+    }
+
+    if (acceptedSopClasses.size() == 0)
+    {
+      // by default, include the short list first and then all the others
+      for (std::set<std::string>::const_iterator it = shortDcmtkSopClassUids.begin(); it != shortDcmtkSopClassUids.end(); ++it)
+      {
+        acceptedSopClasses_.push_back(*it);
+      }
+
+      for (std::set<std::string>::const_iterator it = allDcmtkSopClassUids.begin(); it != allDcmtkSopClassUids.end(); ++it)
+      {
+        if (shortDcmtkSopClassUids.find(*it) == shortDcmtkSopClassUids.end()) // don't add the classes that we have already added
+        {
+          acceptedSopClasses_.push_back(*it);
+        }
+      }
+    }
+    else
+    {
+      std::set<std::string> addedSopClasses;
+
+      for (std::list<std::string>::const_iterator it = acceptedSopClasses.begin(); it != acceptedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, add all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern) && addedSopClasses.find(*itall) == addedSopClasses.end())
+            {
+              acceptedSopClasses_.push_back(*itall);
+              addedSopClasses.insert(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, add it without checking if it is known by DCMTK
+          acceptedSopClasses_.push_back(*it);
+          addedSopClasses.insert(*it);
+        }
+      }
+    }
+    
+    // now remove all rejected syntaxes
+    if (rejectedSopClasses.size() > 0)
+    {
+      for (std::set<std::string>::const_iterator it = rejectedSopClasses.begin(); it != rejectedSopClasses.end(); ++it)
+      {
+        if (it->find('*') != std::string::npos || it->find('?') != std::string::npos)
+        {
+          // if it contains wildcard, get all the matching SOP classes known by DCMTK
+          boost::regex pattern(Toolbox::WildcardToRegularExpression(*it));
+
+          for (std::set<std::string>::const_iterator itall = allDcmtkSopClassUids.begin(); itall != allDcmtkSopClassUids.end(); ++itall)
+          {
+            if (regex_match(*itall, pattern))
+            {
+              acceptedSopClasses_.remove(*itall);
+            }
+          }
+        }
+        else
+        {
+          // if it is a SOP Class UID, remove it without checking if it is known by DCMTK
+          acceptedSopClasses_.remove(*it);
+        }
+      }
+    }
+  }
+
+  void ServerContext::GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const
+  {
+    sopClasses.clear();
+
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+
+    size_t count = 0;
+    std::list<std::string>::const_iterator it = acceptedSopClasses_.begin();
+
+    while (it != acceptedSopClasses_.end() && (maxCount == 0 || count < maxCount))
+    {
+      sopClasses.insert(*it);
+      count++;
+      it++;
+    }
+  }
+
+  void ServerContext::GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     syntaxes = acceptedTransferSyntaxes_;
@@ -2117,7 +2239,25 @@
   }
 
 
-  bool ServerContext::IsUnknownSopClassAccepted()
+  void ServerContext::GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const
+  {
+    boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
+    
+    // // TODO: investigate: actually, neither Orthanc 1.12.4 nor DCM4CHEE will accept to send a LittleEndianExplicit file
+    // //                    while e.g., Jpeg-LS has been presented (and accepted) as the preferred TS for the C-Store SCP.
+    // // if we have defined IngestTranscoding, let's propose this TS first to avoid any unnecessary transcoding
+    // if (isIngestTranscoding_)
+    // {
+    //   syntaxes.push_back(ingestTransferSyntax_);
+    // }
+    
+    // then, propose the default ones
+    syntaxes.push_back(DicomTransferSyntax_LittleEndianExplicit);
+    syntaxes.push_back(DicomTransferSyntax_LittleEndianImplicit);
+  }
+  
+
+  bool ServerContext::IsUnknownSopClassAccepted() const
   {
     boost::mutex::scoped_lock lock(dynamicOptionsMutex_);
     return isUnknownSopClassAccepted_;
--- a/OrthancServer/Sources/ServerContext.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Mon Dec 09 19:41:24 2024 +0100
@@ -252,6 +252,7 @@
         
     std::unique_ptr<SharedArchive>  queryRetrieveArchive_;
     std::string defaultLocalAet_;
+    RetrieveMethod defaultDicomRetrieveMethod_;
     OrthancHttpHandler  httpHandler_;
     bool saveJobs_;
     FindStorageAccessMode findStorageAccessMode_;
@@ -276,9 +277,10 @@
 
     // New in Orthanc 1.9.0
     DicomTransferSyntax preferredTransferSyntax_;
-    boost::mutex dynamicOptionsMutex_;
+    mutable boost::mutex dynamicOptionsMutex_;
     bool isUnknownSopClassAccepted_;
     std::set<DicomTransferSyntax>  acceptedTransferSyntaxes_;
+    std::list<std::string>         acceptedSopClasses_;  // ordered; the most 120 common ones first
 
     StoreResult StoreAfterTranscoding(std::string& resultPublicId,
                                       DicomInstanceToStore& dicom,
@@ -436,6 +438,11 @@
       return defaultLocalAet_;
     }
 
+    RetrieveMethod GetDefaultDicomRetrieveMethod() const
+    {
+      return defaultDicomRetrieveMethod_;
+    }
+
     LuaScripting& GetLuaScripting()
     {
       return mainLua_;
@@ -595,11 +602,18 @@
 
     const std::string& GetDeidentifiedContent(const DicomElement& element) const;
 
-    void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes);
+    void GetAcceptedTransferSyntaxes(std::set<DicomTransferSyntax>& syntaxes) const;
 
     void SetAcceptedTransferSyntaxes(const std::set<DicomTransferSyntax>& syntaxes);
 
-    bool IsUnknownSopClassAccepted();
+    void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& syntaxes) const;
+
+    void SetAcceptedSopClasses(const std::list<std::string>& acceptedSopClasses,
+                               const std::set<std::string>& rejectedSopClasses);
+
+    void GetAcceptedSopClasses(std::set<std::string>& sopClasses, size_t maxCount) const;
+
+    bool IsUnknownSopClassAccepted() const;
 
     void SetUnknownSopClassAccepted(bool accepted);
 
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -1478,7 +1478,7 @@
   }
 
 
-  float ArchiveJob::GetProgress()
+  float ArchiveJob::GetProgress() const
   {
     if (writer_.get() == NULL ||
         writer_->GetStepsCount() == 0)
@@ -1493,7 +1493,7 @@
   }
 
     
-  void ArchiveJob::GetJobType(std::string& target)
+  void ArchiveJob::GetJobType(std::string& target) const
   {
     if (isMedia_)
     {
@@ -1506,7 +1506,7 @@
   }
 
 
-  void ArchiveJob::GetPublicContent(Json::Value& value)
+  void ArchiveJob::GetPublicContent(Json::Value& value) const
   {
     value = Json::objectValue;
     value[KEY_DESCRIPTION] = description_;
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -107,13 +107,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       return false;  // Cannot serialize this kind of job
     }
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -83,7 +83,7 @@
   }
 
   
-  bool CleaningInstancesJob::Serialize(Json::Value& target)
+  bool CleaningInstancesJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -62,7 +62,7 @@
     
     void SetKeepSource(bool keep);
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual void Start() ORTHANC_OVERRIDE;
   };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -0,0 +1,121 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomGetScuJob.h"
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
+static const char* const REMOTE = "Remote";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+
+
+  static uint16_t InstanceReceivedHandler(void* callbackContext,
+                                          DcmDataset& dataset,
+                                          const std::string& remoteAet,
+                                          const std::string& remoteIp,
+                                          const std::string& calledAet)
+  {
+    // this code is equivalent to OrthancStoreRequestHandler
+    ServerContext* context = reinterpret_cast<ServerContext*>(callbackContext);
+
+    std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromDcmDataset(dataset));
+    
+    if (toStore->GetBufferSize() > 0)
+    {
+      toStore->SetOrigin(DicomInstanceOrigin::FromDicomProtocol
+                         (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
+
+      std::string id;
+      ServerContext::StoreResult result = context->Store(id, *toStore, StoreInstanceMode_Default);
+      return result.GetCStoreStatusCode();
+    }
+
+    return STATUS_STORE_Error_CannotUnderstand;
+  }
+
+  void DicomGetScuJob::Retrieve(const DicomMap& findAnswer)
+  {
+    if (connection_.get() == NULL)
+    {
+      std::set<std::string> sopClassesToPropose;
+      std::set<std::string> acceptedSopClasses;
+      std::list<DicomTransferSyntax> proposedTransferSyntaxes;
+
+      if (sopClassesFromResourcesToRetrieve_.size() > 0)
+      {
+        context_.GetAcceptedSopClasses(acceptedSopClasses, 0); 
+
+        // keep the sop classes from the resources to retrieve only if they are accepted by Orthanc
+        Toolbox::GetIntersection(sopClassesToPropose, sopClassesFromResourcesToRetrieve_, acceptedSopClasses);
+      }
+      else
+      {
+        // when we don't know what SOP Classes to use, we include the 120 most common SOP Classes because 
+        // there are only 128 presentation contexts available
+        context_.GetAcceptedSopClasses(sopClassesToPropose, 120); 
+      }
+
+      if (sopClassesToPropose.size() == 0)
+      {
+        throw OrthancException(ErrorCode_NoPresentationContext, "Cannot perform C-Get, no SOPClassUID have been accepted by Orthanc.");        
+      }
+
+      context_.GetProposedStorageTransferSyntaxes(proposedTransferSyntaxes);
+
+      connection_.reset(new DicomControlUserConnection(parameters_, 
+                                                       ScuOperationFlags_Get, 
+                                                       sopClassesToPropose,
+                                                       proposedTransferSyntaxes));
+    }
+
+    connection_->SetProgressListener(this);
+    connection_->Get(findAnswer, InstanceReceivedHandler, &context_);
+  }
+
+  void DicomGetScuJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomRetrieveScuBaseJob::AddFindAnswer(answer);
+
+    std::set<std::string> sopClassesInStudy;
+    if (answer.HasTag(DICOM_TAG_SOP_CLASSES_IN_STUDY) 
+        && answer.LookupStringValues(sopClassesInStudy, DICOM_TAG_SOP_CLASSES_IN_STUDY, false))
+    {
+      for (std::set<std::string>::const_iterator it = sopClassesInStudy.begin(); it != sopClassesInStudy.end(); ++it)
+      {
+        sopClassesFromResourcesToRetrieve_.insert(*it);
+      }
+    }
+  }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomGetScuJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
+#include "DicomRetrieveScuBaseJob.h"
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class DicomGetScuJob : public DicomRetrieveScuBaseJob
+  {
+  private:
+    std::set<std::string> sopClassesFromResourcesToRetrieve_;
+
+    virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE;
+    
+  public:
+    explicit DicomGetScuJob(ServerContext& context) :
+      DicomRetrieveScuBaseJob(context)
+    {
+    }
+
+    DicomGetScuJob(ServerContext& context,
+                   const Json::Value& serialized);
+
+
+    virtual void AddFindAnswer(const DicomMap &answer) ORTHANC_OVERRIDE;
+
+
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
+    {
+      target = "DicomGetScu";
+    }
+  };
+}
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -242,7 +242,7 @@
   }
   
 
-  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
+  void DicomModalityStoreJob::GetPublicContent(Json::Value& value) const
   {
     SetOfInstancesJob::GetPublicContent(value);
     
@@ -281,7 +281,7 @@
   }
 
 
-  bool DicomModalityStoreJob::Serialize(Json::Value& target)
+  bool DicomModalityStoreJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -88,14 +88,14 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "DicomModalityStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -36,123 +36,21 @@
 
 namespace Orthanc
 {
-  class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand
-  {
-  private:
-    DicomMoveScuJob&           that_;
-    std::unique_ptr<DicomMap>  findAnswer_;
 
-  public:
-    Command(DicomMoveScuJob& that,
-            const DicomMap&  findAnswer) :
-      that_(that),
-      findAnswer_(findAnswer.Clone())
-    {
-    }
-
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      that_.Retrieve(*findAnswer_);
-      return true;
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      findAnswer_->Serialize(target);
-    }
-  };
-
-
-  class DicomMoveScuJob::Unserializer :
-    public SetOfCommandsJob::ICommandUnserializer
-  {
-  private:
-    DicomMoveScuJob&   that_;
-
-  public:
-    explicit Unserializer(DicomMoveScuJob&  that) :
-      that_(that)
-    {
-    }
-
-    virtual ICommand* Unserialize(const Json::Value& source) const ORTHANC_OVERRIDE
-    {
-      DicomMap findAnswer;
-      findAnswer.Unserialize(source);
-      return new Command(that_, findAnswer);
-    }
-  };
 
 
   void DicomMoveScuJob::Retrieve(const DicomMap& findAnswer)
   {
     if (connection_.get() == NULL)
     {
-      connection_.reset(new DicomControlUserConnection(parameters_));
+      connection_.reset(new DicomControlUserConnection(parameters_, ScuOperationFlags_Move));
     }
     
+    connection_->SetProgressListener(this);
     connection_->Move(targetAet_, findAnswer);
   }
 
 
-  static void AddToQuery(DicomFindAnswers& query,
-                         const DicomMap& item)
-  {
-    query.Add(item);
-
-    /**
-     * Compatibility with Orthanc <= 1.9.4: Remove the
-     * "SpecificCharacterSet" (0008,0005) tag that is automatically
-     * added if creating a ParsedDicomFile object from a DicomMap.
-     **/
-    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-  }
-
-  // this method is used to implement the retrieve part of a Q&R 
-  // it keeps only the main dicom tags from the C-Find answer
-  void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
-  {
-    DicomMap item;
-    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
-    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
-    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
-    AddToQuery(query_, item);
-    
-    AddCommand(new Command(*this, answer));
-  }
-
-  // this method is used to implement a C-Move
-  // it keeps all tags from the C-Move query
-  void DicomMoveScuJob::AddQuery(const DicomMap& query)
-  {
-    AddToQuery(query_, query);
-    AddCommand(new Command(*this, query));
-  }
-  
-  void DicomMoveScuJob::AddFindAnswer(QueryRetrieveHandler& query,
-                                      size_t i)
-  {
-    DicomMap answer;
-    query.GetAnswer(answer, i);
-    AddFindAnswer(answer);
-  }    
-
-
-  void DicomMoveScuJob::SetLocalAet(const std::string& aet)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetLocalApplicationEntityTitle(aet);
-    }
-  }
-
   
   void DicomMoveScuJob::SetTargetAet(const std::string& aet)
   {
@@ -167,109 +65,33 @@
   }
 
   
-  void DicomMoveScuJob::SetRemoteModality(const RemoteModalityParameters& remote)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetRemoteModality(remote);
-    }
-  }
 
 
-  void DicomMoveScuJob::SetTimeout(uint32_t seconds)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetTimeout(seconds);
-    }
-  }
-
-  
-  void DicomMoveScuJob::Stop(JobStopReason reason)
-  {
-    connection_.reset();
-  }
-  
-
-  void DicomMoveScuJob::SetQueryFormat(DicomToJsonFormat format)
+  void DicomMoveScuJob::GetPublicContent(Json::Value& value) const
   {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      queryFormat_ = format;
-    }
-  }
+    DicomRetrieveScuBaseJob::GetPublicContent(value);
 
-
-  void DicomMoveScuJob::GetPublicContent(Json::Value& value)
-  {
-    SetOfCommandsJob::GetPublicContent(value);
-
-    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
-    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-
-    value[QUERY] = Json::objectValue;
-    query_.ToJson(value[QUERY], queryFormat_);
+    value[TARGET_AET] = targetAet_;
   }
 
 
   DicomMoveScuJob::DicomMoveScuJob(ServerContext& context,
                                    const Json::Value& serialized) :
-    SetOfCommandsJob(new Unserializer(*this), serialized),
-    context_(context),
-    parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
-    targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET)),
-    query_(true),
-    queryFormat_(DicomToJsonFormat_Short)
+    DicomRetrieveScuBaseJob(context, serialized),
+    targetAet_(SerializationToolbox::ReadString(serialized, TARGET_AET))
   {
-    if (serialized.isMember(QUERY))
-    {
-      const Json::Value& query = serialized[QUERY];
-      if (query.type() == Json::arrayValue)
-      {
-        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
-        {
-          DicomMap item;
-          FromDcmtkBridge::FromJson(item, query[i]);
-          AddToQuery(query_, item);
-        }
-      }
-    }
-
-    if (serialized.isMember(QUERY_FORMAT))
-    {
-      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
-    }
   }
 
   
-  bool DicomMoveScuJob::Serialize(Json::Value& target)
+  bool DicomMoveScuJob::Serialize(Json::Value& target) const
   {
-    if (!SetOfCommandsJob::Serialize(target))
+    if (!DicomRetrieveScuBaseJob::Serialize(target))
     {
       return false;
     }
     else
     {
-      parameters_.SerializeJob(target);
       target[TARGET_AET] = targetAet_;
-
-      // "Short" is for compatibility with Orthanc <= 1.9.4
-      target[QUERY] = Json::objectValue;
-      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
-
-      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
       
       return true;
     }
--- a/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -25,7 +25,8 @@
 
 #include "../../../OrthancFramework/Sources/Compatibility.h"
 #include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
-#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+// #include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+#include "DicomRetrieveScuBaseJob.h"
 
 #include "../QueryRetrieveHandler.h"
 
@@ -33,51 +34,22 @@
 {
   class ServerContext;
   
-  class DicomMoveScuJob : public SetOfCommandsJob
+  class DicomMoveScuJob : public DicomRetrieveScuBaseJob
   {
   private:
-    class Command;
-    class Unserializer;
+    std::string                 targetAet_;
     
-    ServerContext&              context_;
-    DicomAssociationParameters  parameters_;
-    std::string                 targetAet_;
-    DicomFindAnswers            query_;
-    DicomToJsonFormat           queryFormat_;  // New in 1.9.5
-
-    std::unique_ptr<DicomControlUserConnection>  connection_;
-    
-    void Retrieve(const DicomMap& findAnswer);
+    virtual void Retrieve(const DicomMap& findAnswer) ORTHANC_OVERRIDE;
     
   public:
     explicit DicomMoveScuJob(ServerContext& context) :
-      context_(context),
-      query_(false  /* this is not for worklists */),
-      queryFormat_(DicomToJsonFormat_Short)
+      DicomRetrieveScuBaseJob(context)
     {
     }
 
     DicomMoveScuJob(ServerContext& context,
                     const Json::Value& serialized);
 
-    void AddFindAnswer(const DicomMap& answer);
-    
-    void AddQuery(const DicomMap& query);
-
-    void AddFindAnswer(QueryRetrieveHandler& query,
-                       size_t i);
-
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-    
-    void SetLocalAet(const std::string& aet);
-
-    void SetRemoteModality(const RemoteModalityParameters& remote);
-
-    void SetTimeout(uint32_t timeout);
-
     const std::string& GetTargetAet() const
     {
       return targetAet_;
@@ -85,22 +57,13 @@
     
     void SetTargetAet(const std::string& aet);
 
-    void SetQueryFormat(DicomToJsonFormat format);
-
-    DicomToJsonFormat GetQueryFormat() const
-    {
-      return queryFormat_;
-    }
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "DicomMoveScu";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -0,0 +1,238 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomGetScuJob.h"
+
+#include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
+#include "../../../OrthancFramework/Sources/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include <dcmtk/dcmnet/dimse.h>
+#include <algorithm>
+#include "../../../OrthancFramework/Sources/Logging.h"
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const QUERY = "Query";
+static const char* const QUERY_FORMAT = "QueryFormat";  // New in 1.9.5
+static const char* const REMOTE = "Remote";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+
+  static void AddToQuery(DicomFindAnswers& query,
+                         const DicomMap& item)
+  {
+    query.Add(item);
+
+    /**
+     * Compatibility with Orthanc <= 1.9.4: Remove the
+     * "SpecificCharacterSet" (0008,0005) tag that is automatically
+     * added if creating a ParsedDicomFile object from a DicomMap.
+     **/
+    query.GetAnswer(query.GetSize() - 1).Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+  }
+
+  // this method is used to implement the retrieve part of a Q&R 
+  // it keeps only the main dicom tags from the C-Find answer
+  void DicomRetrieveScuBaseJob::AddFindAnswer(const DicomMap& answer)
+  {
+    DicomMap item;
+    item.CopyTagIfExists(answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    item.CopyTagIfExists(answer, DICOM_TAG_PATIENT_ID);
+    item.CopyTagIfExists(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_SOP_INSTANCE_UID);
+    item.CopyTagIfExists(answer, DICOM_TAG_ACCESSION_NUMBER);
+    AddToQuery(query_, item);
+    
+    AddCommand(new Command(*this, answer));
+  }
+
+  void DicomRetrieveScuBaseJob::AddFindAnswer(QueryRetrieveHandler& query,
+                                              size_t i)
+  {
+    DicomMap answer;
+    query.GetAnswer(answer, i);
+    AddFindAnswer(answer);
+  }    
+
+  // this method is used to implement a C-Move
+  // it keeps all tags from the C-Move query
+  void DicomRetrieveScuBaseJob::AddQuery(const DicomMap& query)
+  {
+    AddToQuery(query_, query);
+    AddCommand(new Command(*this, query));
+  }
+ 
+
+  void DicomRetrieveScuBaseJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetLocalApplicationEntityTitle(aet);
+    }
+  }
+
+  
+  void DicomRetrieveScuBaseJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetRemoteModality(remote);
+    }
+  }
+
+
+  void DicomRetrieveScuBaseJob::SetTimeout(uint32_t seconds)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetTimeout(seconds);
+    }
+  }
+
+  
+  void DicomRetrieveScuBaseJob::Stop(JobStopReason reason)
+  {
+    connection_.reset();
+  }
+  
+
+  void DicomRetrieveScuBaseJob::SetQueryFormat(DicomToJsonFormat format)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      queryFormat_ = format;
+    }
+  }
+
+
+  void DicomRetrieveScuBaseJob::GetPublicContent(Json::Value& value) const
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+
+    value[LOCAL_AET] = parameters_.GetLocalApplicationEntityTitle();
+    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
+
+    value[QUERY] = Json::objectValue;
+    query_.ToJson(value[QUERY], queryFormat_);
+  }
+
+
+  DicomRetrieveScuBaseJob::DicomRetrieveScuBaseJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context),
+    parameters_(DicomAssociationParameters::UnserializeJob(serialized)),
+    query_(true),
+    queryFormat_(DicomToJsonFormat_Short),
+    nbRemainingSubOperations_(0),
+    nbCompletedSubOperations_(0),
+    nbFailedSubOperations_(0),
+    nbWarningSubOperations_(0)  
+  {
+    if (serialized.isMember(QUERY))
+    {
+      const Json::Value& query = serialized[QUERY];
+      if (query.type() == Json::arrayValue)
+      {
+        for (Json::Value::ArrayIndex i = 0; i < query.size(); i++)
+        {
+          DicomMap item;
+          FromDcmtkBridge::FromJson(item, query[i]);
+          AddToQuery(query_, item);
+        }
+      }
+    }
+
+    if (serialized.isMember(QUERY_FORMAT))
+    {
+      queryFormat_ = StringToDicomToJsonFormat(SerializationToolbox::ReadString(serialized, QUERY_FORMAT));
+    }
+  }
+
+  
+  bool DicomRetrieveScuBaseJob::Serialize(Json::Value& target) const
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      parameters_.SerializeJob(target);
+
+      // "Short" is for compatibility with Orthanc <= 1.9.4
+      target[QUERY] = Json::objectValue;
+      query_.ToJson(target[QUERY], DicomToJsonFormat_Short);
+
+      target[QUERY_FORMAT] = EnumerationToString(queryFormat_);
+      
+      return true;
+    }
+  }
+
+  void DicomRetrieveScuBaseJob::OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                                  uint16_t nbCompletedSubOperations,
+                                                  uint16_t nbFailedSubOperations,
+                                                  uint16_t nbWarningSubOperations)
+  {
+    boost::mutex::scoped_lock lock(progressMutex_);
+
+    nbRemainingSubOperations_ = nbRemainingSubOperations;
+    nbCompletedSubOperations_ = nbCompletedSubOperations;
+    nbFailedSubOperations_ = nbFailedSubOperations;
+    nbWarningSubOperations_ = nbWarningSubOperations;
+  }
+
+  float DicomRetrieveScuBaseJob::GetProgress() const
+  {
+    boost::mutex::scoped_lock lock(progressMutex_);
+    
+    uint32_t totalOperations = nbRemainingSubOperations_ + nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_;
+    if (totalOperations == 0)
+    {
+      return 0.0f;
+    }
+
+    LOG(INFO) << "---------" << nbRemainingSubOperations_ << "  " << nbCompletedSubOperations_;
+
+    return float(nbCompletedSubOperations_ + nbFailedSubOperations_ + nbWarningSubOperations_) / float(totalOperations);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomRetrieveScuBaseJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -0,0 +1,154 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include "../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h"
+#include "../../../OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h"
+#include <boost/thread/mutex.hpp>
+
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  class DicomRetrieveScuBaseJob : public SetOfCommandsJob, public DicomControlUserConnection::IProgressListener
+  {
+  protected:
+    class Command : public SetOfCommandsJob::ICommand
+    {
+    private:
+      DicomRetrieveScuBaseJob &that_;
+      std::unique_ptr<DicomMap> findAnswer_;
+
+    public:
+      Command(DicomRetrieveScuBaseJob &that,
+              const DicomMap &findAnswer) :
+      that_(that),
+      findAnswer_(findAnswer.Clone())
+      {
+      }
+
+      virtual bool Execute(const std::string &jobId) ORTHANC_OVERRIDE
+      {
+        that_.Retrieve(*findAnswer_);
+        return true;
+      }
+
+      virtual void Serialize(Json::Value &target) const ORTHANC_OVERRIDE
+      {
+        findAnswer_->Serialize(target);
+      }
+    };
+
+    class Unserializer : public SetOfCommandsJob::ICommandUnserializer
+    {
+    protected:
+      DicomRetrieveScuBaseJob &that_;
+
+    public:
+      explicit Unserializer(DicomRetrieveScuBaseJob &that) : 
+      that_(that)
+      {
+      }
+
+      virtual ICommand *Unserialize(const Json::Value &source) const ORTHANC_OVERRIDE
+      {
+        DicomMap findAnswer;
+        findAnswer.Unserialize(source);
+        return new Command(that_, findAnswer);
+      }
+    };
+
+    ServerContext &context_;
+    DicomAssociationParameters parameters_;
+    DicomFindAnswers query_;
+    DicomToJsonFormat queryFormat_; // New in 1.9.5
+
+    std::unique_ptr<DicomControlUserConnection> connection_;
+
+    mutable boost::mutex progressMutex_;
+    uint16_t nbRemainingSubOperations_;
+    uint16_t nbCompletedSubOperations_;
+    uint16_t nbFailedSubOperations_;
+    uint16_t nbWarningSubOperations_;
+
+    virtual void Retrieve(const DicomMap &findAnswer) = 0;
+
+    explicit DicomRetrieveScuBaseJob(ServerContext &context) : 
+    context_(context),
+    query_(false /* this is not for worklists */),
+    queryFormat_(DicomToJsonFormat_Short)
+    {
+    }
+
+    DicomRetrieveScuBaseJob(ServerContext &context,
+                            const Json::Value &serialized);
+
+  public:
+    virtual void AddFindAnswer(const DicomMap &answer);
+
+    void AddQuery(const DicomMap& query);
+
+    void AddFindAnswer(QueryRetrieveHandler &query,
+                       size_t i);
+
+    const DicomAssociationParameters &GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void SetLocalAet(const std::string &aet);
+
+    void SetRemoteModality(const RemoteModalityParameters &remote);
+
+    void SetTimeout(uint32_t timeout);
+
+    void SetQueryFormat(DicomToJsonFormat format);
+
+    DicomToJsonFormat GetQueryFormat() const
+    {
+      return queryFormat_;
+    }
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
+
+    virtual void GetPublicContent(Json::Value &value) const ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value &target) const ORTHANC_OVERRIDE;
+
+    virtual void OnProgressUpdated(uint16_t nbRemainingSubOperations,
+                                    uint16_t nbCompletedSubOperations,
+                                    uint16_t nbFailedSubOperations,
+                                    uint16_t nbWarningSubOperations) ORTHANC_OVERRIDE;
+
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
+
+    virtual bool NeedsProgressUpdateBetweenSteps() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+  };
+}
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -353,7 +353,7 @@
   }
   
 
-  void MergeStudyJob::GetPublicContent(Json::Value& value)
+  void MergeStudyJob::GetPublicContent(Json::Value& value) const
   {
     CleaningInstancesJob::GetPublicContent(value);
     value["TargetStudy"] = targetStudy_;
@@ -386,7 +386,7 @@
   }
 
   
-  bool MergeStudyJob::Serialize(Json::Value& target)
+  bool MergeStudyJob::Serialize(Json::Value& target) const
   {
     if (!CleaningInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/MergeStudyJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -89,13 +89,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "MergeStudy";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -220,7 +220,7 @@
   }
 
 
-  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value)
+  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value) const
   {
     SetOfInstancesJob::GetPublicContent(value);
     
@@ -285,7 +285,7 @@
   }
 
 
-  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
+  bool OrthancPeerStoreJob::Serialize(Json::Value& target) const
   {
     if (!SetOfInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -92,13 +92,13 @@
 
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;   // For pausing jobs
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "OrthancPeerStore";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -542,7 +542,7 @@
   }
 
 
-  void ResourceModificationJob::GetPublicContent(Json::Value& value)
+  void ResourceModificationJob::GetPublicContent(Json::Value& value) const
   {
     boost::recursive_mutex::scoped_lock lock(outputMutex_);
 
@@ -636,7 +636,7 @@
     }
   }
   
-  bool ResourceModificationJob::Serialize(Json::Value& value)
+  bool ResourceModificationJob::Serialize(Json::Value& value) const
   {
     if (modification_.get() == NULL)
     {
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -116,14 +116,14 @@
     // Only possible if "IsSingleResourceModification()"
     ResourceType GetOutputLevel() const;
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "ResourceModification";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE;
 
     virtual void Reset() ORTHANC_OVERRIDE;
 
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -306,7 +306,7 @@
   }
   
     
-  void SplitStudyJob::GetPublicContent(Json::Value& value)
+  void SplitStudyJob::GetPublicContent(Json::Value& value) const
   {
     CleaningInstancesJob::GetPublicContent(value);
 
@@ -351,7 +351,7 @@
   }
 
   
-  bool SplitStudyJob::Serialize(Json::Value& target)
+  bool SplitStudyJob::Serialize(Json::Value& target) const
   {
     if (!CleaningInstancesJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/SplitStudyJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -113,13 +113,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "SplitStudy";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -404,7 +404,7 @@
   }
 
 
-  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value)
+  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value) const
   {
     SetOfCommandsJob::GetPublicContent(value);
       
@@ -434,7 +434,7 @@
   }
   
 
-  bool StorageCommitmentScpJob::Serialize(Json::Value& target)
+  bool StorageCommitmentScpJob::Serialize(Json::Value& target) const
   {
     if (!SetOfCommandsJob::Serialize(target))
     {
--- a/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -89,13 +89,13 @@
     {
     }
 
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& target) const ORTHANC_OVERRIDE
     {
       target = "StorageCommitmentScp";
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
 
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -376,7 +376,7 @@
   static const char* KEY_WORKERS_COUNT = "WorkersCount";
 
 
-  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target)
+  void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
@@ -391,7 +391,7 @@
   }
 
 
-  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target)
+  bool ThreadedSetOfInstancesJob::Serialize(Json::Value& target) const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
@@ -478,7 +478,7 @@
     return keepSource_;
   }
 
-  float ThreadedSetOfInstancesJob::GetProgress()
+  float ThreadedSetOfInstancesJob::GetProgress() const
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Mon Dec 09 19:41:24 2024 +0100
@@ -140,15 +140,15 @@
     
     virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
 
-    virtual float GetProgress() ORTHANC_OVERRIDE;
+    virtual float GetProgress() const ORTHANC_OVERRIDE;
 
     bool IsStarted() const;
 
     virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
     
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE;
     
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+    virtual bool Serialize(Json::Value& target) const ORTHANC_OVERRIDE;
 
     virtual bool GetOutput(std::string& output,
                            MimeType& mime,
--- a/OrthancServer/Sources/main.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/Sources/main.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -494,6 +494,13 @@
     context_.GetAcceptedTransferSyntaxes(target);
   }
 
+  virtual void GetProposedStorageTransferSyntaxes(std::list<DicomTransferSyntax>& target,
+                                                  const std::string& remoteIp,
+                                                  const std::string& remoteAet,
+                                                  const std::string& calledAet) ORTHANC_OVERRIDE
+  {
+    context_.GetProposedStorageTransferSyntaxes(target);
+  }
   
   virtual bool IsUnknownSopClassAccepted(const std::string& remoteIp,
                                          const std::string& remoteAet,
@@ -501,6 +508,12 @@
   {
     return context_.IsUnknownSopClassAccepted();
   }
+
+  virtual void GetAcceptedSopClasses(std::set<std::string>& sopClasses,
+                                     size_t maxCount) ORTHANC_OVERRIDE
+  {
+    context_.GetAcceptedSopClasses(sopClasses, maxCount);
+  }
 };
 
 
@@ -886,6 +899,7 @@
     PrintErrorCode(ErrorCode_AlreadyExistingTag, "Cannot override the value of a tag that already exists");
     PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
     PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
+    PrintErrorCode(ErrorCode_DicomGetUnavailable, "DicomUserConnection: The C-GET command is not supported by the remote SCP");
     PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
   }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ServerConfigTests.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -0,0 +1,150 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2023 Osimis S.A., Belgium
+ * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersUnitTests.h"
+#include <gtest/gtest.h>
+
+#include "../../OrthancFramework/Sources/Compatibility.h"
+#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Sources/Database/SQLiteDatabaseWrapper.h"
+#include "../Sources/ServerContext.h"
+
+using namespace Orthanc;
+
+TEST(ServerConfig, AcceptedSopClasses)
+{
+  const std::string path = "UnitTestsStorage";
+
+  MemoryStorageArea storage;
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+  ServerContext context(db, storage, true /* running unit tests */, 10);
+
+  { // default config -> all SOP Classes should be accepted
+    std::set<std::string> s;
+
+    context.GetAcceptedSopClasses(s, 0);
+    ASSERT_LE(100u, s.size());
+
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+    ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+    context.GetAcceptedSopClasses(s, 1);
+    ASSERT_EQ(1u, s.size());
+  }
+
+  {
+    std::list<std::string> acceptedStorageClasses;
+    std::set<std::string> rejectedStorageClasses;
+
+    std::set<std::string> s;
+    context.GetAcceptedSopClasses(s, 0);
+    size_t allSize = s.size();
+
+    { // default config but reject one class
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.4");
+
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(allSize - 1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") != s.end());
+
+      context.GetAcceptedSopClasses(s, 1);
+      ASSERT_EQ(1u, s.size());
+    }
+
+    { // default config but reject one regex
+      acceptedStorageClasses.clear();
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept a single - no rejection
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.1.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+    }
+
+    { // accept from regex - reject one
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.1");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept from regex - reject from regex
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.840.10008.5.1.4.1.*");
+      rejectedStorageClasses.clear();
+      rejectedStorageClasses.insert("1.2.840.10008.5.1.4.1.1.12.1.*");
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_LE(10, s.size());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.4") != s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.1.1") == s.end());
+      ASSERT_TRUE(s.find("1.2.840.10008.5.1.4.1.1.12.2.1") != s.end());
+    }
+
+    { // accept one that is unknown form DCMTK
+      acceptedStorageClasses.clear();
+      acceptedStorageClasses.push_back("1.2.3.4");
+      rejectedStorageClasses.clear();
+      context.SetAcceptedSopClasses(acceptedStorageClasses, rejectedStorageClasses);
+
+      context.GetAcceptedSopClasses(s, 0);
+      ASSERT_EQ(1, s.size());
+      ASSERT_TRUE(s.find("1.2.3.4") != s.end());
+    }
+
+  }
+
+  context.Stop();
+  db.Close();
+}
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Wed Dec 04 18:16:44 2024 +0100
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Mon Dec 09 19:41:24 2024 +0100
@@ -106,24 +106,24 @@
     {
     }
 
-    virtual float GetProgress() ORTHANC_OVERRIDE
+    virtual float GetProgress() const ORTHANC_OVERRIDE
     {
       return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
     }
 
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
     {
       type = "DummyJob";
     }
 
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value = Json::objectValue;
       value["Type"] = "DummyJob";
       return true;
     }
 
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
     {
       value["hello"] = "world";
     }
@@ -202,7 +202,7 @@
     {
     }
 
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    virtual void GetJobType(std::string& s) const ORTHANC_OVERRIDE
     {
       s = "DummyInstancesJob";
     }
--- a/TODO	Wed Dec 04 18:16:44 2024 +0100
+++ b/TODO	Mon Dec 09 19:41:24 2024 +0100
@@ -1,3 +1,11 @@
+current work on C-Get SCU:
+- for the negotiation, limit SOPClassUID to the ones listed in a C-Find response or to a list provided in the Rest API ?
+- SetupPresentationContexts
+- handle progress
+- handle cancellation when the job is cancelled ?
+
+
+
 =======================
 === Orthanc Roadmap ===
 =======================
@@ -32,6 +40,11 @@
 * Add configurations to enable/disable warnings:
   - Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!
   - Modifying a study while keeping its original StudyInstanceUID: This should be avoided!
+  In order to be able to disable/enable warnings in both the server and the framework, we should add a map of
+  enabled warnings in the logging classes directly and have something like:
+  LOG_WARNING_IF_ENABLED("Warning_ID") << ...
+  ENABLE_WARNING("Warning_ID", true)
+  Warnings from Framework should have a separate range like W999_ or WF001_ ...
 * Store the job registry in a dedicatd table in DB ?
   https://discourse.orthanc-server.org/t/performance-issue-when-adding-a-lot-of-jobs-in-the-queue/3915/2
   Note: that might also be the right time to have a central jobs registry when working
@@ -173,9 +186,12 @@
 Mid-term
 --------
 
-* Support C-GET SCU (note that C-GET SCP was introduced in Orthanc 1.7.0)
-* Configure the list of accepted SOP Classes
-  https://discourse.orthanc-server.org/t/can-you-limit-the-sop-classes-accepted-as-store-scp/4606
+* Check how Orthanc shall behave wrt to AcceptedSopClasses in these situations (consider Orthanc
+  accepts CT but not PT)
+  - What shall we log/warn if an external modality tries to send a PT/CT
+  - What shall we log/warn if we try to C-GET a PT/CT
+  Should the rejected files be logged as Failed, Warning, Refused, ...
+  Note: some tentative work has been initiated in the get-scu-test branch.
 * Support "Retrieve AE Title" (0008,0054) in C-FIND:
   - On SCP side: done by https://orthanc.uclouvain.be/hg/orthanc/rev/1ec3e1e18f50
   - On SCU side:
@@ -185,9 +201,6 @@
 * Strict hierarchical C-FIND:
   https://groups.google.com/d/msg/orthanc-users/VBHpeGVSNKM/tkaVvjWFBwAJ
 * report DIMSE error codes in Rest API and job status for /store /query /move /retrieve
-* report progress report of C-Move operation in jop progress.  There are 
-  progress callbacks available in DIMSE_moveUser
-  https://groups.google.com/g/orthanc-users/c/c8cGnA7FzsE/m/BSs66D8wBwAJ
 * Log outgoing C-Find queries
 * Support other Transfer Syntaxes in the Worklist plugin:
   https://discourse.orthanc-server.org/t/could-you-please-create-an-option-to-set-the-transfer-syntax-in-the-worklist-plugin-currently-little-endian-explicit-is-fixed/4871