Repository: pydicom/pynetdicom

Issue:
Add support for Protocol Approval Q/R

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 5d9158184..ca1aeabcc 100644
--- a/README.rst
+++ b/README.rst
@@ -68,6 +68,7 @@ Supported Service Classes
 
   - `Instance and Frame Level Retrieve <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Y>`_
   - `Composite Instance Retrieve Without Bulk Data <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Z>`_
+- `Modality Performed Procedure Step Management <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_F>`_
 - `Basic Worklist Management Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_K>`_
 - `Relevant Patient Information Query Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Q>`_
 - `Hanging Protocol Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_U>`_
@@ -77,7 +78,7 @@ Supported Service Classes
 - `Display System Management Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_EE>`_
 - `Non-Patient Object Storage Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_GG>`_
 - `Defined Procedure Protocol Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_HH>`_
-- `Modality Performed Procedure Step Management <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_F>`_
+- `Protocol Approval Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
 
 
 Supported DIMSE SCU Services
diff --git a/docs/changelog/v1.4.0.rst b/docs/changelog/v1.4.0.rst
index f07fe0993..e3a4fa84b 100644
--- a/docs/changelog/v1.4.0.rst
+++ b/docs/changelog/v1.4.0.rst
@@ -7,19 +7,22 @@
 Fixes
 .....
 
-* Fixed Query Retrieve service class not responded with 0x0000 Success if no
-  matches were yielded by the C-FIND request handler.
+* Fixed Query Retrieve service class not responded with ``0x0000`` (Success)
+  if no matches were yielded by the C-FIND request handler.
+
 
 Enhancements
 ............
 
+* Added support for Protocol Approval Query Retrieve service class
+  (:issue:`327`)
+
 
 
 Changes
 .......
 
 * The following have been removed as per planned deprecation:
-
   * ``AE.on_c_store()``, ``AE.on_c_echo()``, ``AE.on_c_find()``,
     ``AE.on_c_get()``, ``AE.on_c_move()``, ``AE.on_n_get()``,
     ``AE.on_async_ops_window()``, ``AE.on_sop_class_common_extended()``,
diff --git a/docs/index.rst b/docs/index.rst
index f9807a837..36ae82063 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -43,6 +43,7 @@ Supported Service Classes
    implant_template_service_class
    display_system_service_class
    modality_performed_procedure_step
+   protocol_approval_service_class
 
 
 User Guide
diff --git a/docs/protocol_approval_service_class.rst b/docs/protocol_approval_service_class.rst
new file mode 100644
index 000000000..ad145866e
--- /dev/null
+++ b/docs/protocol_approval_service_class.rst
@@ -0,0 +1,273 @@
+.. _pa_service:
+
+Protocol Approval Query/Retrieve Service Class
+==============================================
+The `Protocol Approval Query/Retrieve Service Class <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
+defines a service that facilitates access to Protocol Approval composite objects.
+
+Supported SOP Classes
+---------------------
+
+.. _pa_sops:
+
+Protocol Approval Query/Retrieve SOP Classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++-------------------------------+---------------------------------+
+| UID                           | SOP Class                       |
++===============================+=================================+
+| 1.2.840.10008.5.1.4.1.1.200.4 | ProtocolApprovalInformationFind |
++-------------------------------+---------------------------------+
+| 1.2.840.10008.5.1.4.1.1.200.5 | ProtocolApprovalInformationMove |
++-------------------------------+---------------------------------+
+| 1.2.840.10008.5.1.4.1.1.200.6 | ProtocolApprovalInformationGet  |
++-------------------------------+---------------------------------+
+
+.. _pa_statuses:
+
+Statuses
+--------
+
+.. _pa_find_statuses:
+
+C-FIND Statuses
+~~~~~~~~~~~~~~~~
+
++------------+----------+----------------------------------+
+| Code (hex) | Category | Description                      |
++============+==========+==================================+
+| 0x0000     | Success  | Success                          |
++------------+----------+----------------------------------+
+| 0x0122     | Failure  | SOP Class not supported          |
++------------+----------+----------------------------------+
+| 0xFE00     | Cancel   | Processing has been terminated   |
++------------+----------+----------------------------------+
+
+Query/Retrieve (Find) Service Statuses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++------------------+----------+----------------------------------------------+
+| Code (hex)       | Category | Description                                  |
++==================+==========+==============================================+
+| 0xA700           | Failure  | Out of resources                             |
++------------------+----------+----------------------------------------------+
+| 0xA900           | Failure  | Dataset does not match SOP Class             |
++------------------+----------+----------------------------------------------+
+| 0xC000 to 0xCFFF | Failure  | Unable to process                            |
++------------------+----------+----------------------------------------------+
+| 0xFF00           | Pending  | Matches are continuing                       |
++------------------+----------+----------------------------------------------+
+| 0xFF01           | Pending  | Matches are continuing; one or more Optional |
+|                  |          | keys was not supported                       |
++------------------+----------+----------------------------------------------+
+
+pynetdicom Protocol Approval Query/Retrieve (Find) Statuses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When pynetdicom is acting as a Protocol Approval Query/Retrieve (Find) SCP it
+uses the following status codes values to indicate the corresponding issue
+has occurred to help aid in debugging.
+
++------------------+----------+-----------------------------------------------+
+| Code (hex)       | Category | Description                                   |
++==================+==========+===============================================+
+| 0xC001           | Failure  | Handler bound to ``evt.EVT_C_FIND`` yielded a |
+|                  |          | status Dataset with no (0000,0900) *Status*   |
+|                  |          | element                                       |
++------------------+----------+-----------------------------------------------+
+| 0xC002           | Failure  | Handler bound to ``evt.EVT_C_FIND`` yielded an|
+|                  |          | invalid status object (not a pydicom Dataset  |
+|                  |          | or an int)                                    |
++------------------+----------+-----------------------------------------------+
+| 0xC310           | Failure  | Failed to decode the dataset received from    |
+|                  |          | the peer                                      |
++------------------+----------+-----------------------------------------------+
+| 0xC311           | Failure  | Unhandled exception raised by the handler     |
+|                  |          | bound to ``evt.EVT_C_FIND``                   |
++------------------+----------+-----------------------------------------------+
+| 0xC312           | Failure  | Failed to encode the dataset received from    |
+|                  |          | the handler bound to ``evt.EVT_C_FIND``       |
++------------------+----------+-----------------------------------------------+
+
+
+.. _pa_get_statuses:
+
+C-GET Statuses
+~~~~~~~~~~~~~~
+
++------------+----------+----------------------------------+
+| Code (hex) | Category | Description                      |
++============+==========+==================================+
+| 0x0000     | Success  | Success                          |
++------------+----------+----------------------------------+
+| 0x0122     | Failure  | SOP Class not supported          |
++------------+----------+----------------------------------+
+| 0x0124     | Failure  | Not authorised                   |
++------------+----------+----------------------------------+
+| 0x0210     | Failure  | Duplicate invocation             |
++------------+----------+----------------------------------+
+| 0x0212     | Failure  | Mistyped argument                |
++------------+----------+----------------------------------+
+| 0xFE00     | Cancel   | Sub-operations terminated        |
++------------+----------+----------------------------------+
+
+Protocol Approval Query/Retrieve (Get) Service Statuses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++------------------+----------+----------------------------------------------+
+| Code (hex)       | Category | Description                                  |
++==================+==========+==============================================+
+| 0xA701           | Failure  | Out of resources; unable to calculate number |
+|                  |          | of matches                                   |
++------------------+----------+----------------------------------------------+
+| 0xA702           | Failure  | Out of resources; unable to perform          |
+|                  |          | sub-operations                               |
++------------------+----------+----------------------------------------------+
+| 0xA900           | Failure  | Dataset does not match SOP Class             |
++------------------+----------+----------------------------------------------+
+| 0xAA00           | Failure  | None of the frames requested were found in   |
+|                  |          | the SOP Instance                             |
++------------------+----------+----------------------------------------------+
+| 0xAA01           | Failure  | Unable to create new object for this SOP     |
+|                  |          | class                                        |
++------------------+----------+----------------------------------------------+
+| 0xAA02           | Failure  | Unable to extract frames                     |
++------------------+----------+----------------------------------------------+
+| 0xAA03           | Failure  | Time-based request received for a            |
+|                  |          | non-time-based original SOP Instance         |
++------------------+----------+----------------------------------------------+
+| 0xAA04           | Failure  | Invalid request                              |
++------------------+----------+----------------------------------------------+
+| 0xB000           | Warning  | Sub-operations complete, one or more         |
+|                  |          | or warnings                                  |
++------------------+----------+----------------------------------------------+
+| 0xC000 to 0xCFFF | Failure  | Unable to process                            |
++------------------+----------+----------------------------------------------+
+| 0xFF00           | Pending  | Sub-operations are continuing                |
++------------------+----------+----------------------------------------------+
+
+pynetdicom Protocol Approval Query/Retrieve (Get) Statuses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++------------------+----------+-----------------------------------------------+
+| Code (hex)       | Category | Description                                   |
++==================+==========+===============================================+
+| 0xC001           | Failure  | Handler bound to ``evt.EVT_C_GET`` yielded a  |
+|                  |          | status Dataset with no (0000,0900) *Status*   |
+|                  |          | element                                       |
++------------------+----------+-----------------------------------------------+
+| 0xC002           | Failure  | Handler bound to ``evt.EVT_C_GET`` yielded an |
+|                  |          | invalid status object (not a pydicom Dataset  |
+|                  |          | or an int)                                    |
++------------------+----------+-----------------------------------------------+
+| 0xC410           | Failure  | Failed to decode the dataset received from    |
+|                  |          | the peer                                      |
++------------------+----------+-----------------------------------------------+
+| 0xC411           | Failure  | Unhandled exception raised by the handler     |
+|                  |          | bound to ``evt.EVT_C_GET``                    |
++------------------+----------+-----------------------------------------------+
+| 0xC413           | Failure  | The handler bound to ``evt.EVT_C_GET``        |
+|                  |          | yielded an invalid number of sub-operations   |
++------------------+----------+-----------------------------------------------+
+
+
+.. _pa_move_statuses:
+
+C-MOVE Statuses
+~~~~~~~~~~~~~~~
+
++------------+----------+----------------------------------+
+| Code (hex) | Category | Description                      |
++============+==========+==================================+
+| 0x0000     | Success  | Success                          |
++------------+----------+----------------------------------+
+| 0x0122     | Failure  | SOP Class not supported          |
++------------+----------+----------------------------------+
+| 0x0124     | Failure  | Not authorised                   |
++------------+----------+----------------------------------+
+| 0x0210     | Failure  | Duplicate invocation             |
++------------+----------+----------------------------------+
+| 0x0211     | Failure  | Unrecognised operation           |
++------------+----------+----------------------------------+
+| 0x0212     | Failure  | Mistyped argument                |
++------------+----------+----------------------------------+
+| 0xFE00     | Cancel   | Sub-operations terminated        |
++------------+----------+----------------------------------+
+
+Protocol Approval Query/Retrieve (Move) Service Statuses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++------------------+----------+----------------------------------------------+
+| Code (hex)       | Category | Description                                  |
++==================+==========+==============================================+
+| 0xA701           | Failure  | Out of resources; unable to calculate number |
+|                  |          | of matches                                   |
++------------------+----------+----------------------------------------------+
+| 0xA702           | Failure  | Out of resources; unable to perform          |
+|                  |          | sub-operations                               |
++------------------+----------+----------------------------------------------+
+| 0xA801           | Failure  | Move destination unknown                     |
++------------------+----------+----------------------------------------------+
+| 0xA900           | Failure  | Dataset does not match SOP Class             |
++------------------+----------+----------------------------------------------+
+| 0xAA00           | Failure  | None of the frames requested were found in   |
+|                  |          | the SOP Instance                             |
++------------------+----------+----------------------------------------------+
+| 0xAA01           | Failure  | Unable to create new object for this SOP     |
+|                  |          | class                                        |
++------------------+----------+----------------------------------------------+
+| 0xAA02           | Failure  | Unable to extract frames                     |
++------------------+----------+----------------------------------------------+
+| 0xAA03           | Failure  | Time-based request received for a            |
+|                  |          | non-time-based original SOP Instance         |
++------------------+----------+----------------------------------------------+
+| 0xAA04           | Failure  | Invalid request                              |
++------------------+----------+----------------------------------------------+
+| 0xB000           | Warning  | Sub-operations complete, one or more         |
+|                  |          | or warnings                                  |
++------------------+----------+----------------------------------------------+
+| 0xC000 to 0xCFFF | Failure  | Unable to process                            |
++------------------+----------+----------------------------------------------+
+| 0xFF00           | Pending  | Sub-operations are continuing                |
++------------------+----------+----------------------------------------------+
+
+pynetdicom Protocol Approval Query/Retrieve (Move) Statuses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
++------------------+----------+-----------------------------------------------+
+| Code (hex)       | Category | Description                                   |
++==================+==========+===============================================+
+| 0xC001           | Failure  | Handler bound to ``evt.EVT_C_MOVE`` yielded a |
+|                  |          | status Dataset with no (0000,0900) *Status*   |
+|                  |          | element                                       |
++------------------+----------+-----------------------------------------------+
+| 0xC002           | Failure  | Handler bound to ``evt.EVT_C_MOVE`` yielded an|
+|                  |          | invalid status object (not a pydicom Dataset  |
+|                  |          | or an int)                                    |
++------------------+----------+-----------------------------------------------+
+| 0xC510           | Failure  | Failed to decode the dataset received from    |
+|                  |          | the peer                                      |
++------------------+----------+-----------------------------------------------+
+| 0xC511           | Failure  | Unhandled exception raised by the handler     |
+|                  |          | bound to ``evt.EVT_C_MOVE``                   |
++------------------+----------+-----------------------------------------------+
+| 0xC513           | Failure  | The handler bound to ``evt.EVT_C_MOVE``       |
+|                  |          | yielded an invalid number of sub-operations   |
++------------------+----------+-----------------------------------------------+
+| 0xC514           | Failure  | The handler bound to ``evt.EVT_C_MOVE``       |
+|                  |          | failed to yield the (address, port)           |
+|                  |          | and/or the number of sub-operations           |
++------------------+----------+-----------------------------------------------+
+| 0xC515           | Failure  | The handler bound to ``evt.EVT_C_MOVE``       |
+|                  |          | failed to yield a valid (address, port) pair  |
++------------------+----------+-----------------------------------------------+
+
+
+References
+----------
+
+* DICOM Standard, Part 4, `Annex II <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
+* DICOM Standard, Part 7, Sections
+  `9.1.2.1.5 <http://dicom.nema.org/medical/dicom/current/output/chtml/part07/chapter_9.html#sect_9.1.2.1.5>`_,
+  `9.1.3.1.6 <http://dicom.nema.org/medical/dicom/current/output/chtml/part07/chapter_9.html#sect_9.1.3.1.6>`_ and
+  `9.1.4.1.7 <http://dicom.nema.org/medical/dicom/current/output/chtml/part07/chapter_9.html#sect_9.1.4.1.7>`_
diff --git a/pynetdicom/association.py b/pynetdicom/association.py
index 5440fa46a..620d849be 100644
--- a/pynetdicom/association.py
+++ b/pynetdicom/association.py
@@ -67,6 +67,9 @@ from pynetdicom.sop_class import (
     ImplantTemplateGroupInformationModelFind,
     ImplantTemplateGroupInformationModelGet,
     ImplantTemplateGroupInformationModelMove,
+    ProtocolApprovalInformationModelFind,
+    ProtocolApprovalInformationModelGet,
+    ProtocolApprovalInformationModelMove,
 )
 from pynetdicom.pdu_primitives import (
     UserIdentityNegotiation,
@@ -989,6 +992,8 @@ class Association(threading.Thread):
               *Implant Assembly Template Information Model - FIND*
             - ``IT`` - 1.2.840.10008.5.1.4.44.2 -
               *Implant Template Group Information Model - FIND*
+            - ``PA`` - 1.2.840.10008.5.1.4.1.1.200.4 -
+              *Protocol Approval Information Model - FIND*
 
         Yields
         ------
@@ -1019,10 +1024,11 @@ class Association(threading.Thread):
             *Query/Retrieve Service, Basic Worklist Management Service,
             Hanging Protocol Query/Retrieve Service, Defined Procedure Protocol
             Query/Retrieve Service, Substance Administration Query Service,
-            Color Palette Query/Retrieve Service* and *Implant Template
-            Query/Retrieve Service* specific
+            Color Palette Query/Retrieve Service*, *Implant Template
+            Query/Retrieve Service* and *Protocol Approval Query/Retrieve
+            Service* specific
             (DICOM Standard Part 4, Annexes C.4.1, K.4.1.1.4, U.4.1, HH,
-            V.4.1.1.4, X and BB):
+            V.4.1.1.4, X, BB and II):
 
             Failure
               | ``0xA700`` - Out of resources
@@ -1077,6 +1083,7 @@ class Association(threading.Thread):
         service_class.DefinedProcedureProtocolQueryRetrieveServiceClass
         service_class.ColorPaletteQueryRetrieveServiceClass
         service_class.ImplantTemplateQueryRetrieveServiceClass
+        service_class.ProtocolApprovalQueryRetrieveServiceClass
 
         References
         ----------
@@ -1088,6 +1095,7 @@ class Association(threading.Thread):
         * DICOM Standard Part 4, `Annex X <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_X>`_
         * DICOM Standard Part 4, `Annex BB <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_BB>`_
         * DICOM Standard Part 4, `Annex HH <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_HH>`_
+        * DICOM Standard Part 4, `Annex II <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
         * DICOM Standard Part 7, Sections
           `9.1.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.2>`_,
           `9.3.2 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.2>`_,
@@ -1115,6 +1123,7 @@ class Association(threading.Thread):
             "IG" : GenericImplantTemplateInformationModelFind,
             "IA" : ImplantAssemblyTemplateInformationModelFind,
             "IT" : ImplantTemplateGroupInformationModelFind,
+            "PA" : ProtocolApprovalInformationModelFind,
         }
 
         try:
@@ -1221,6 +1230,8 @@ class Association(threading.Thread):
               *Implant Assembly Template Information Model - GET*
             - ``IT`` - 1.2.840.10008.5.1.4.44.4 -
               *Implant Template Group Information Model - GET*
+            - ``PA`` - 1.2.840.10008.5.1.4.1.1.200.6 -
+              *Protocol Approval Information Model - GET*
 
         Yields
         ------
@@ -1250,9 +1261,10 @@ class Association(threading.Thread):
 
             *Query/Retrieve Service, Hanging Protocol Query/Retrieve Service,
             Defined Procedure Protocol Query/Retrieve Service, Color Palette
-            Query/Retrieve Service* and *Implant Template Query/Retrieve
-            Service* specific (DICOM Standard Part 4, Annexes C.4.3,
-            Y.C.4.2.1.4, Z.4.2.1.4, U.4.3, X, BB and HH):
+            Query/Retrieve Service*, *Implant Template Query/Retrieve
+            Service* and *Protocol Approval Query/Retrieve Service* specific
+            (DICOM Standard Part 4, Annexes C.4.3,
+            Y.C.4.2.1.4, Z.4.2.1.4, U.4.3, X, BB, HH and II):
 
             Pending
               | ``0xFF00`` - Sub-operations are continuing
@@ -1300,6 +1312,7 @@ class Association(threading.Thread):
         service_class.DefinedProcedureProtocolQueryRetrieveServiceClass
         service_class.ColorPaletteQueryRetrieveServiceClass
         service_class.ImplantTemplateQueryRetrieveServiceClass
+        service_class.ProtocolApprovalQueryRetrieveServiceClass
         dimse_primitives.C_GET
 
         References
@@ -1312,6 +1325,7 @@ class Association(threading.Thread):
         * DICOM Standard Part 4, `Annex Z <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Z>`_
         * DICOM Standard Part 4, `Annex BB <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_BB>`_
         * DICOM Standard Part 4, `Annex HH <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_HH>`_
+        * DICOM Standard Part 4, `Annex II <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
         * DICOM Standard Part 7, Sections
           `9.1.3 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.3>`_,
           `9.3.3 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.3>`_
@@ -1334,6 +1348,7 @@ class Association(threading.Thread):
             "IG" : GenericImplantTemplateInformationModelGet,
             "IA" : ImplantAssemblyTemplateInformationModelGet,
             "IT" : ImplantTemplateGroupInformationModelGet,
+            "PA" : ProtocolApprovalInformationModelGet,
         }
 
         try:
@@ -1442,6 +1457,8 @@ class Association(threading.Thread):
               *Implant Assembly Template Information Model - MOVE*
             - ``IT`` - 1.2.840.10008.5.1.4.44.3 -
               *Implant Template Group Information Model - MOVE*
+            - ``PA`` - 1.2.840.10008.5.1.4.1.1.200.5 -
+              *Protocol Approval Information Model - MOVE*
 
         Yields
         ------
@@ -1470,8 +1487,8 @@ class Association(threading.Thread):
 
             *Query/Retrieve Service, Hanging Protocol Query/Retrieve Service,
             Defined Procedure Protocol Query/Retrieve Service, Color Palette
-            Query/Retrieve Service* and *Implant Template Query/Retreive
-            Service*
+            Query/Retrieve Service* , *Implant Template Query/Retreive
+            Service* and *Protocol Approval Query/Retrieve Service*
             specific (DICOM Standard Part 4, Annexes C, U, Y, X, BB and HH):
 
             Failure
@@ -1509,6 +1526,7 @@ class Association(threading.Thread):
         service_class.HangingProtocolQueryRetrieveServiceClass
         service_class.ColorPaletteQueryRetrieveServiceClass
         service_class.ImplantTemplateQueryRetrieveServiceClass
+        service_class.ProtocolApprovalQueryRetrieveServiceClass
 
         References
         ----------
@@ -1519,6 +1537,7 @@ class Association(threading.Thread):
         * DICOM Standard Part 4, `Annex Y <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_Y>`_
         * DICOM Standard Part 4, `Annex BB <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_BB>`_
         * DICOM Standard Part 4, `Annex HH <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_HH>`_
+        * DICOM Standard Part 4, `Annex II <http://dicom.nema.org/medical/dicom/current/output/html/part04.html#chapter_II>`_
         * DICOM Standard Part 7, Sections
           `9.1.4 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.1.4>`_,
           `9.3.4 <http://dicom.nema.org/medical/dicom/current/output/html/part07.html#sect_9.3.4>`_
@@ -1540,6 +1559,7 @@ class Association(threading.Thread):
             "IG" : GenericImplantTemplateInformationModelMove,
             "IA" : ImplantAssemblyTemplateInformationModelMove,
             "IT" : ImplantTemplateGroupInformationModelMove,
+            "PA" : ProtocolApprovalInformationModelMove,
         }
 
         try:
diff --git a/pynetdicom/service_class.py b/pynetdicom/service_class.py
index 22924d475..8bd9be31e 100644
--- a/pynetdicom/service_class.py
+++ b/pynetdicom/service_class.py
@@ -434,7 +434,8 @@ class QueryRetrieveServiceClass(ServiceClass):
                                        '1.2.840.10008.5.1.4.39.2',
                                        '1.2.840.10008.5.1.4.43.2',
                                        '1.2.840.10008.5.1.4.44.2',
-                                       '1.2.840.10008.5.1.4.45.2']:
+                                       '1.2.840.10008.5.1.4.45.2',
+                                       '1.2.840.10008.5.1.4.1.1.200.4']:
             self.statuses = QR_FIND_SERVICE_CLASS_STATUS
             self._find_scp(req, context)
         elif context.abstract_syntax in ['1.2.840.10008.5.1.4.1.2.1.3',
@@ -447,7 +448,8 @@ class QueryRetrieveServiceClass(ServiceClass):
                                          '1.2.840.10008.5.1.4.39.4',
                                          '1.2.840.10008.5.1.4.43.4',
                                          '1.2.840.10008.5.1.4.44.4',
-                                         '1.2.840.10008.5.1.4.45.4']:
+                                         '1.2.840.10008.5.1.4.45.4',
+                                         '1.2.840.10008.5.1.4.1.1.200.6']:
             self.statuses = QR_GET_SERVICE_CLASS_STATUS
             self._get_scp(req, context)
         elif context.abstract_syntax in ['1.2.840.10008.5.1.4.1.2.1.2',
@@ -459,7 +461,8 @@ class QueryRetrieveServiceClass(ServiceClass):
                                          '1.2.840.10008.5.1.4.39.3',
                                          '1.2.840.10008.5.1.4.43.3',
                                          '1.2.840.10008.5.1.4.44.3',
-                                         '1.2.840.10008.5.1.4.45.3']:
+                                         '1.2.840.10008.5.1.4.45.3',
+                                         '1.2.840.10008.5.1.4.1.1.200.5']:
             self.statuses = QR_MOVE_SERVICE_CLASS_STATUS
             self._move_scp(req, context)
         else:
@@ -1925,3 +1928,7 @@ class ColorPaletteQueryRetrieveServiceClass(QueryRetrieveServiceClass):
 class ImplantTemplateQueryRetrieveServiceClass(QueryRetrieveServiceClass):
     """Implementation of the Implant Template QR Service."""
     pass
+
+class ProtocolApprovalQueryRetrieveServiceClass(QueryRetrieveServiceClass):
+    """Implementation of the Protocol Approval QR Service."""
+    pass
diff --git a/pynetdicom/sop_class.py b/pynetdicom/sop_class.py
index 2a6f9f023..3e12d90f1 100644
--- a/pynetdicom/sop_class.py
+++ b/pynetdicom/sop_class.py
@@ -19,6 +19,7 @@ from pynetdicom.service_class import (
     DefinedProcedureProtocolQueryRetrieveServiceClass,
     ColorPaletteQueryRetrieveServiceClass,
     ImplantTemplateQueryRetrieveServiceClass,
+    ProtocolApprovalQueryRetrieveServiceClass,
 )
 from pynetdicom.service_class_n import (
     DisplaySystemManagementServiceClass,
@@ -80,6 +81,8 @@ def uid_to_service_class(uid):
         return ServiceClass  # Not yet implemented
     elif uid in _RT_MACHINE_VERIFICATION_CLASSES.values():
         return ServiceClass  # Not yet implemented
+    elif uid in _PROTOCOL_APPROVAL_SOP_CLASSES.values():
+        return ProtocolApprovalQueryRetrieveServiceClass
 
     # No SCP implemented
     return ServiceClass
@@ -365,6 +368,12 @@ _RT_MACHINE_VERIFICATION_CLASSES = {
     'RTIonMachineVerification' : '1.2.840.10008.5.1.4.34.9',
 }
 
+_PROTOCOL_APPROVAL_SOP_CLASSES = {
+    'ProtocolApprovalInformationModelFind' : '1.2.840.10008.5.1.4.1.1.200.4',
+    'ProtocolApprovalInformationModelMove' : '1.2.840.10008.5.1.4.1.1.200.5',
+    'ProtocolApprovalInformationModelGet' : '1.2.840.10008.5.1.4.1.1.200.6'
+}
+
 # pylint: enable=line-too-long
 _generate_sop_classes(_VERIFICATION_CLASSES)
 _generate_sop_classes(_STORAGE_CLASSES)
@@ -383,6 +392,7 @@ _generate_sop_classes(_PROCEDURE_STEP_CLASSES)
 _generate_sop_classes(_MEDIA_STORAGE_CLASSES)
 _generate_sop_classes(_UNIFIED_PROCEDURE_STEP_CLASSES)
 _generate_sop_classes(_RT_MACHINE_VERIFICATION_CLASSES)
+_generate_sop_classes(_PROTOCOL_APPROVAL_SOP_CLASSES)
 
 
 def uid_to_sop_class(uid):


Test patch:
diff --git a/pynetdicom/tests/test_assoc.py b/pynetdicom/tests/test_assoc.py
index e9c046334..1b7d7f284 100644
--- a/pynetdicom/tests/test_assoc.py
+++ b/pynetdicom/tests/test_assoc.py
@@ -78,6 +78,9 @@ from pynetdicom.sop_class import (
     ImplantTemplateGroupInformationModelFind,
     ImplantTemplateGroupInformationModelGet,
     ImplantTemplateGroupInformationModelMove,
+    ProtocolApprovalInformationModelFind,
+    ProtocolApprovalInformationModelGet,
+    ProtocolApprovalInformationModelMove,
 )
 from .dummy_c_scp import (
     DummyVerificationSCP, DummyStorageSCP, DummyFindSCP, DummyGetSCP,
@@ -1710,6 +1713,7 @@ class TestAssociationSendCFind(object):
         ae.add_supported_context(GenericImplantTemplateInformationModelFind)
         ae.add_supported_context(ImplantAssemblyTemplateInformationModelFind)
         ae.add_supported_context(ImplantTemplateGroupInformationModelFind)
+        ae.add_supported_context(ProtocolApprovalInformationModelFind)
         scp = ae.start_server(
             ('', 11112), block=False, evt_handlers=[(evt.EVT_C_FIND, handle)]
         )
@@ -1729,11 +1733,12 @@ class TestAssociationSendCFind(object):
         ae.add_requested_context(GenericImplantTemplateInformationModelFind)
         ae.add_requested_context(ImplantAssemblyTemplateInformationModelFind)
         ae.add_requested_context(ImplantTemplateGroupInformationModelFind)
+        ae.add_requested_context(ProtocolApprovalInformationModelFind)
         assoc = ae.associate('localhost', 11112)
         assert assoc.is_established
 
         for qm in ['P', 'S', 'O', 'W', 'G', 'B', 'C', 'PC', 'SA', 'H',
-                   'D', 'CP', 'IG', 'IA', 'IT']:
+                   'D', 'CP', 'IG', 'IA', 'IT', 'PA']:
             for (status, ds) in assoc.send_c_find(self.ds, query_model=qm):
                 assert status.Status == 0x0000
 
@@ -2323,6 +2328,7 @@ class TestAssociationSendCGet(object):
         ae.add_supported_context(GenericImplantTemplateInformationModelGet)
         ae.add_supported_context(ImplantAssemblyTemplateInformationModelGet)
         ae.add_supported_context(ImplantTemplateGroupInformationModelGet)
+        ae.add_supported_context(ProtocolApprovalInformationModelGet)
         ae.add_supported_context(CTImageStorage, scu_role=True, scp_role=True)
         scp = ae.start_server(
             ('', 11112), block=False, evt_handlers=[(evt.EVT_C_GET, handle_get)]
@@ -2339,6 +2345,7 @@ class TestAssociationSendCGet(object):
         ae.add_requested_context(GenericImplantTemplateInformationModelGet)
         ae.add_requested_context(ImplantAssemblyTemplateInformationModelGet)
         ae.add_requested_context(ImplantTemplateGroupInformationModelGet)
+        ae.add_requested_context(ProtocolApprovalInformationModelGet)
         ae.add_requested_context(CTImageStorage)
 
         role = build_role(CTImageStorage, scu_role=True, scp_role=True)
@@ -2348,7 +2355,8 @@ class TestAssociationSendCGet(object):
         )
         assert assoc.is_established
 
-        for qm in ['P', 'S', 'O', 'C', 'CB', 'H', 'D', 'CP', 'IG', 'IA', 'IT']:
+        for qm in ['P', 'S', 'O', 'C', 'CB', 'H', 'D', 'CP', 'IG', 'IA', 'IT',
+                   'PA']:
             for (status, ds) in assoc.send_c_get(self.ds, query_model=qm):
                 assert status.Status == 0x0000
 
@@ -3035,6 +3043,7 @@ class TestAssociationSendCMove(object):
         ae.add_supported_context(GenericImplantTemplateInformationModelMove)
         ae.add_supported_context(ImplantAssemblyTemplateInformationModelMove)
         ae.add_supported_context(ImplantTemplateGroupInformationModelMove)
+        ae.add_supported_context(ProtocolApprovalInformationModelMove)
         move_scp = ae.start_server(
             ('', 11113), block=False, evt_handlers=[(evt.EVT_C_MOVE, handle_move)]
         )
@@ -3050,11 +3059,12 @@ class TestAssociationSendCMove(object):
         ae.add_requested_context(GenericImplantTemplateInformationModelMove)
         ae.add_requested_context(ImplantAssemblyTemplateInformationModelMove)
         ae.add_requested_context(ImplantTemplateGroupInformationModelMove)
+        ae.add_requested_context(ProtocolApprovalInformationModelMove)
 
         assoc = ae.associate('localhost', 11113)
         assert assoc.is_established
 
-        for qm in ['P', 'S', 'O', 'C', 'H', 'D', 'CP', 'IG', 'IA', 'IT']:
+        for qm in ['P', 'S', 'O', 'C', 'H', 'D', 'CP', 'IG', 'IA', 'IT', 'PA']:
             result = assoc.send_c_move(self.ds, b'TESTMOVE', query_model=qm)
             (status, ds) = next(result)
             assert status.Status == 0xFF00
diff --git a/pynetdicom/tests/test_service_qr.py b/pynetdicom/tests/test_service_qr.py
index cb6c39a0c..655f690bd 100644
--- a/pynetdicom/tests/test_service_qr.py
+++ b/pynetdicom/tests/test_service_qr.py
@@ -5,6 +5,7 @@
 * DefinedProcedureProtocolQueryRetrieveServiceClass
 * ColorPaletteQueryRetrieveServiceClass
 * ImplantTemplateQueryRetrieveServiceClass
+* ProtocolApprovalQueryRetrieveServiceClass
 
 """
 
diff --git a/pynetdicom/tests/test_sop.py b/pynetdicom/tests/test_sop.py
index 791e11ad8..822ccdd92 100644
--- a/pynetdicom/tests/test_sop.py
+++ b/pynetdicom/tests/test_sop.py
@@ -22,6 +22,7 @@ from pynetdicom.sop_class import (
     _MEDIA_STORAGE_CLASSES,
     _UNIFIED_PROCEDURE_STEP_CLASSES,
     _RT_MACHINE_VERIFICATION_CLASSES,
+    _PROTOCOL_APPROVAL_SOP_CLASSES,
     VerificationSOPClass,
     CTImageStorage,
     StudyRootQueryRetrieveInformationModelFind,
@@ -33,6 +34,9 @@ from pynetdicom.sop_class import (
     DefinedProcedureProtocolInformationModelFind,
     ColorPaletteInformationModelMove,
     ImplantTemplateGroupInformationModelFind,
+    ProtocolApprovalInformationModelFind,
+    ProtocolApprovalInformationModelMove,
+    ProtocolApprovalInformationModelGet,
 )
 from pynetdicom.service_class import (
     ServiceClass,
@@ -47,6 +51,7 @@ from pynetdicom.service_class import (
     DefinedProcedureProtocolQueryRetrieveServiceClass,
     ColorPaletteQueryRetrieveServiceClass,
     ImplantTemplateQueryRetrieveServiceClass,
+    ProtocolApprovalQueryRetrieveServiceClass,
 )
 from pynetdicom.service_class_n import (
     DisplaySystemManagementServiceClass,
@@ -82,6 +87,8 @@ def test_all_sop_classes():
         assert uid in UID_dictionary
     for uid in _RT_MACHINE_VERIFICATION_CLASSES.values():
         assert uid in UID_dictionary
+    for uid in _PROTOCOL_APPROVAL_SOP_CLASSES.values():
+        assert uid in UID_dictionary
 
 def test_all_service_classes():
     """Test that all the Service Class UIDs are correct."""
@@ -179,6 +186,11 @@ class TestUIDToServiceClass(object):
         for uid in _DISPLAY_SYSTEM_CLASSES.values():
             assert uid_to_service_class(uid) == DisplaySystemManagementServiceClass
 
+    def test_protocol_approval_uids(self):
+        """Test that Protocol Approval SOP Class UIDs work correctly."""
+        for uid in _PROTOCOL_APPROVAL_SOP_CLASSES.values():
+            assert uid_to_service_class(uid) == ProtocolApprovalQueryRetrieveServiceClass
+
     def test_unknown_uid(self):
         """Test that an unknown UID returns default service class."""
         assert uid_to_service_class('1.2.3') == ServiceClass
@@ -253,3 +265,8 @@ class TestSOPClass(object):
         """Test an Implant Template Service SOP Class."""
         assert ImplantTemplateGroupInformationModelFind == '1.2.840.10008.5.1.4.45.2'
         assert ImplantTemplateGroupInformationModelFind.service_class == ImplantTemplateQueryRetrieveServiceClass
+
+    def test_protocol_approval_sop(self):
+        """Test an Protocol Approval Service SOP Class."""
+        assert ProtocolApprovalInformationModelFind == '1.2.840.10008.5.1.4.1.1.200.4'
+        assert ProtocolApprovalInformationModelFind.service_class == ProtocolApprovalQueryRetrieveServiceClass