### Workflow Actions
-There is currently one type of workflow action, "Assignment", which can assign:
+There are currently two types of workflow actions, "Assignment", which can assign:
- Title, see [title placeholders](usage.md#title-placeholders) below
- Tags, correspondent, document type and storage path
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
+and "Removal" actions, which can remove either all of or specific sets of the following:
+
+- Tags, correspondents, document types or storage paths
+- Document owner
+- View and / or edit permissions
+- Custom fields
+
#### Title placeholders
Workflow titles can include placeholders but the available options differ depending on the type of
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">167</context>
+ <context context-type="linenumber">111</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">112</context>
+ <context context-type="linenumber">171</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">131</context>
+ <context context-type="linenumber">190</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">257</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">276</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">120</context>
+ <context context-type="linenumber">179</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">139</context>
+ <context context-type="linenumber">198</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">265</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">284</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">145</context>
+ <context context-type="linenumber">204</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">290</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">208</context>
+ <context context-type="linenumber">206</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">762</context>
+ <context context-type="linenumber">766</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">764</context>
+ <context context-type="linenumber">768</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">166</context>
+ <context context-type="linenumber">110</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">190</context>
+ <context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="6457471243969293847" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">100</context>
+ <context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="4754802869258527587" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">101</context>
+ <context context-type="linenumber">160</context>
</context-group>
</trans-unit>
<trans-unit id="5232720756589450549" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">164</context>
+ <context context-type="linenumber">108</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context>
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
+ <trans-unit id="3288318211116868972" datatype="html">
+ <source>Trigger type</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">118</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="8727727835543352574" datatype="html">
+ <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="<em>"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/> filters specified below.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">119</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="7467799586957602479" datatype="html">
+ <source>Filter filename</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">122</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="3694878959415278689" datatype="html">
+ <source>Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">122</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="1473412958770421458" datatype="html">
+ <source>Filter sources</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">124</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="6540860478788535250" datatype="html">
+ <source>Filter path</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">125</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="5491897741674893121" datatype="html">
+ <source>Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a></source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">125</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="7468453896129193641" datatype="html">
+ <source>Filter mail rule</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">126</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="8663702115863339485" datatype="html">
+ <source>Apply to documents consumed via this mail rule.</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">126</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="6840369584127435743" datatype="html">
+ <source>Content matching algorithm</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">129</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="510635115034690805" datatype="html">
+ <source>Content matching pattern</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">131</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="1333789258712064056" datatype="html">
+ <source>Has tags</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">140</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="5281365940563983618" datatype="html">
+ <source>Has correspondent</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">141</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="4806713133917046341" datatype="html">
+ <source>Has document type</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">142</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="6417103744331194518" datatype="html">
<source>Action type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">94</context>
+ <context context-type="linenumber">152</context>
</context-group>
</trans-unit>
<trans-unit id="6019822389883736115" datatype="html">
<source>Assign title</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">98</context>
+ <context context-type="linenumber">157</context>
</context-group>
</trans-unit>
<trans-unit id="1098196422099517191" datatype="html">
<source>Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">98</context>
+ <context context-type="linenumber">157</context>
</context-group>
</trans-unit>
<trans-unit id="6528897010417701530" datatype="html">
<source>Assign tags</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">99</context>
+ <context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="7198346314713788799" datatype="html">
<source>Assign storage path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">102</context>
+ <context context-type="linenumber">161</context>
</context-group>
</trans-unit>
<trans-unit id="475685412372379925" datatype="html">
<source>Assign custom fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">103</context>
+ <context context-type="linenumber">162</context>
</context-group>
</trans-unit>
<trans-unit id="5057200219587080996" datatype="html">
<source>Assign owner</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">106</context>
+ <context context-type="linenumber">165</context>
</context-group>
</trans-unit>
<trans-unit id="1749184201773078639" datatype="html">
<source>Assign view permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">108</context>
+ <context context-type="linenumber">167</context>
</context-group>
</trans-unit>
<trans-unit id="1744964187586405039" datatype="html">
<source>Assign edit permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">127</context>
+ <context context-type="linenumber">186</context>
</context-group>
</trans-unit>
- <trans-unit id="3288318211116868972" datatype="html">
- <source>Trigger type</source>
+ <trans-unit id="6236311670364192011" datatype="html">
+ <source>Remove tags</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">174</context>
+ <context context-type="linenumber">213</context>
</context-group>
</trans-unit>
- <trans-unit id="8727727835543352574" datatype="html">
- <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="<em>"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/> filters specified below.</source>
+ <trans-unit id="7890599006071681081" datatype="html">
+ <source>Remove all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">175</context>
+ <context context-type="linenumber">214</context>
</context-group>
- </trans-unit>
- <trans-unit id="7467799586957602479" datatype="html">
- <source>Filter filename</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">178</context>
+ <context context-type="linenumber">220</context>
</context-group>
- </trans-unit>
- <trans-unit id="3694878959415278689" datatype="html">
- <source>Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">178</context>
+ <context context-type="linenumber">226</context>
</context-group>
- </trans-unit>
- <trans-unit id="1473412958770421458" datatype="html">
- <source>Filter sources</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">180</context>
+ <context context-type="linenumber">232</context>
</context-group>
- </trans-unit>
- <trans-unit id="6540860478788535250" datatype="html">
- <source>Filter path</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">181</context>
+ <context context-type="linenumber">238</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">245</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
+ <context context-type="linenumber">251</context>
</context-group>
</trans-unit>
- <trans-unit id="5491897741674893121" datatype="html">
- <source>Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a></source>
+ <trans-unit id="8636414563726517994" datatype="html">
+ <source>Remove correspondents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">181</context>
+ <context context-type="linenumber">219</context>
</context-group>
</trans-unit>
- <trans-unit id="7468453896129193641" datatype="html">
- <source>Filter mail rule</source>
+ <trans-unit id="5305293055593064952" datatype="html">
+ <source>Remove document types</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">182</context>
+ <context context-type="linenumber">225</context>
</context-group>
</trans-unit>
- <trans-unit id="8663702115863339485" datatype="html">
- <source>Apply to documents consumed via this mail rule.</source>
+ <trans-unit id="2400388879708187" datatype="html">
+ <source>Remove storage paths</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">182</context>
+ <context context-type="linenumber">231</context>
</context-group>
</trans-unit>
- <trans-unit id="6840369584127435743" datatype="html">
- <source>Content matching algorithm</source>
+ <trans-unit id="4324304327041955720" datatype="html">
+ <source>Remove custom fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">185</context>
+ <context context-type="linenumber">237</context>
</context-group>
</trans-unit>
- <trans-unit id="510635115034690805" datatype="html">
- <source>Content matching pattern</source>
+ <trans-unit id="8367536502602515064" datatype="html">
+ <source>Remove owners</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">187</context>
+ <context context-type="linenumber">244</context>
</context-group>
</trans-unit>
- <trans-unit id="1333789258712064056" datatype="html">
- <source>Has tags</source>
+ <trans-unit id="3393772184866313281" datatype="html">
+ <source>Remove permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">196</context>
+ <context context-type="linenumber">250</context>
</context-group>
</trans-unit>
- <trans-unit id="5281365940563983618" datatype="html">
- <source>Has correspondent</source>
+ <trans-unit id="3145629643370481114" datatype="html">
+ <source>View permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">197</context>
+ <context context-type="linenumber">253</context>
</context-group>
</trans-unit>
- <trans-unit id="4806713133917046341" datatype="html">
- <source>Has document type</source>
+ <trans-unit id="1946660694635960249" datatype="html">
+ <source>Edit permissions</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
- <context context-type="linenumber">198</context>
+ <context context-type="linenumber">272</context>
</context-group>
</trans-unit>
<trans-unit id="4626030417479279989" datatype="html">
<context context-type="linenumber">69</context>
</context-group>
</trans-unit>
+ <trans-unit id="6234812824772766804" datatype="html">
+ <source>Removal</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
+ <context context-type="linenumber">73</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="3138206142174978019" datatype="html">
<source>Create new workflow</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
- <context context-type="linenumber">137</context>
+ <context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="5996779210524133604" datatype="html">
<source>Edit workflow</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
- <context context-type="linenumber">141</context>
+ <context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="1616102757855967475" datatype="html">
<source>Document saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">637</context>
+ <context context-type="linenumber">638</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">646</context>
+ <context context-type="linenumber">649</context>
</context-group>
</trans-unit>
<trans-unit id="448882439049417053" datatype="html">
<source>Error saving document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">650</context>
+ <context context-type="linenumber">653</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">691</context>
+ <context context-type="linenumber">694</context>
</context-group>
</trans-unit>
<trans-unit id="9021887951960049161" datatype="html">
<source>Confirm delete</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">717</context>
+ <context context-type="linenumber">721</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">204</context>
+ <context context-type="linenumber">202</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
<source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">718</context>
+ <context context-type="linenumber">722</context>
</context-group>
</trans-unit>
<trans-unit id="6691075929777935948" datatype="html">
<source>The files for this document will be deleted permanently. This operation cannot be undone.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">719</context>
+ <context context-type="linenumber">723</context>
</context-group>
</trans-unit>
<trans-unit id="719892092227206532" datatype="html">
<source>Delete document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">721</context>
+ <context context-type="linenumber">725</context>
</context-group>
</trans-unit>
<trans-unit id="7295637485862454066" datatype="html">
<source>Error deleting document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">740</context>
+ <context context-type="linenumber">744</context>
</context-group>
</trans-unit>
<trans-unit id="7362691899087997122" datatype="html">
<source>Redo OCR confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">760</context>
+ <context context-type="linenumber">764</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<source>This operation will permanently redo OCR for this document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">761</context>
+ <context context-type="linenumber">765</context>
</context-group>
</trans-unit>
<trans-unit id="5729001209753056399" datatype="html">
<source>Redo OCR operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">772</context>
+ <context context-type="linenumber">776</context>
</context-group>
</trans-unit>
<trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">783</context>
+ <context context-type="linenumber">787</context>
</context-group>
</trans-unit>
<trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">852</context>
+ <context context-type="linenumber">856</context>
</context-group>
</trans-unit>
<trans-unit id="6857598786757174736" datatype="html">
<source>Automatic</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">116</context>
+ <context context-type="linenumber">114</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
<source>None</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">118</context>
+ <context context-type="linenumber">116</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/matching-model.ts</context>
<source>Successfully created <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">161</context>
+ <context context-type="linenumber">159</context>
</context-group>
</trans-unit>
<trans-unit id="3928835053823658072" datatype="html">
<source>Error occurred while creating <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">166</context>
+ <context context-type="linenumber">164</context>
</context-group>
</trans-unit>
<trans-unit id="2541368547549828690" datatype="html">
<source>Successfully updated <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">181</context>
+ <context context-type="linenumber">179</context>
</context-group>
</trans-unit>
<trans-unit id="6442673774206210733" datatype="html">
<source>Error occurred while saving <x id="PH" equiv-text="this.typeName"/>.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">186</context>
+ <context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit id="8371896857609524947" datatype="html">
<source>Associated documents will not be deleted.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">206</context>
+ <context context-type="linenumber">204</context>
</context-group>
</trans-unit>
<trans-unit id="6639207128255974941" datatype="html">
<source>Error while deleting element</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
- <context context-type="linenumber">222</context>
+ <context context-type="linenumber">220</context>
</context-group>
</trans-unit>
<trans-unit id="4863024195229581844" datatype="html">
</div>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
- <pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
- <input type="hidden" formControlName="id" />
- <div class="row">
- <div class="col">
- <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
- <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
- <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
- <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
- <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
- <pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
- </div>
- <div class="col">
- <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
- <div>
- <label class="form-label" i18n>Assign view permissions</label>
- <div class="mb-2">
- <div class="row mb-1">
- <div class="col-lg-3">
- <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
- </div>
- <div class="col-lg-9">
- <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
- </div>
- </div>
- <div class="row">
- <div class="col-lg-3">
- <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
- </div>
- <div class="col-lg-9">
- <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
- </div>
- </div>
- </div>
- <label class="form-label" i18n>Assign edit permissions</label>
- <div>
- <div class="row mb-1">
- <div class="col-lg-3">
- <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
- </div>
- <div class="col-lg-9">
- <pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
- </div>
- </div>
- <div class="row">
- <div class="col-lg-3">
- <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
- </div>
- <div class="col-lg-9">
- <pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
- </div>
- </div>
- <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
- </div>
- </div>
- </div>
- </div>
-
+ <ng-template [ngTemplateOutlet]="actionForm" [ngTemplateOutletContext]="{ formGroup: actionFields.controls[i], action: action }"></ng-template>
</div>
</div>
</div>
</div>
</div>
</ng-template>
+
+<ng-template #actionForm let-formGroup="formGroup" let action="action">
+ <div [formGroup]="formGroup">
+ <input type="hidden" formControlName="id" />
+ <pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
+ @switch(formGroup.get('type').value) {
+ @case ( WorkflowActionType.Assignment) {
+ <div class="row">
+ <div class="col">
+ <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
+ <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags>
+ <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
+ <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
+ <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select>
+ <pngx-input-select i18n-title title="Assign custom fields" multiple="true" [items]="customFields" [allowNull]="true" formControlName="assign_custom_fields"></pngx-input-select>
+ </div>
+ <div class="col">
+ <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select>
+ <div>
+ <label class="form-label" i18n>Assign view permissions</label>
+ <div class="mb-2">
+ <div class="row mb-1">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group>
+ </div>
+ </div>
+ </div>
+ <label class="form-label" i18n>Assign edit permissions</label>
+ <div>
+ <div class="row mb-1">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group>
+ </div>
+ </div>
+ <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
+ </div>
+ </div>
+ </div>
+ </div>
+ }
+ @case (WorkflowActionType.Removal) {
+ <div class="row">
+ <div class="col">
+ <h6 class="form-label" i18n>Remove tags</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_tags"></pngx-input-switch>
+ <div class="mt-n3">
+ <pngx-input-tags [allowCreate]="false" title="" formControlName="remove_tags"></pngx-input-tags>
+ </div>
+
+ <h6 class="form-label" i18n>Remove correspondents</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_correspondents"></pngx-input-switch>
+ <div class="mt-n3">
+ <pngx-input-select i18n-title title="" multiple="true" [items]="correspondents" formControlName="remove_correspondents"></pngx-input-select>
+ </div>
+
+ <h6 class="form-label" i18n>Remove document types</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_document_types"></pngx-input-switch>
+ <div class="mt-n3">
+ <pngx-input-select i18n-title title="" multiple="true" [items]="documentTypes" formControlName="remove_document_types"></pngx-input-select>
+ </div>
+
+ <h6 class="form-label" i18n>Remove storage paths</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_storage_paths"></pngx-input-switch>
+ <div class="mt-n3">
+ <pngx-input-select i18n-title title="" multiple="true" [items]="storagePaths" formControlName="remove_storage_paths"></pngx-input-select>
+ </div>
+
+ <h6 class="form-label" i18n>Remove custom fields</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_custom_fields"></pngx-input-switch>
+ <div class="mt-n3">
+ <pngx-input-select i18n-title title="" multiple="true" [items]="customFields" formControlName="remove_custom_fields"></pngx-input-select>
+ </div>
+ </div>
+ <div class="col">
+ <h6 class="form-label" i18n>Remove owners</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_owners"></pngx-input-switch>
+ <div class="mt-n3">
+ <pngx-input-select i18n-title title="" multiple="true" [items]="users" bindLabel="username" formControlName="remove_owners"></pngx-input-select>
+ </div>
+
+ <h6 class="form-label" i18n>Remove permissions</h6>
+ <pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_permissions"></pngx-input-switch>
+ <div>
+ <label class="form-label" i18n>View permissions</label>
+ <div class="mb-2">
+ <div class="row mb-1">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-user type="view" formControlName="remove_view_users"></pngx-permissions-user>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-group type="view" formControlName="remove_view_groups"></pngx-permissions-group>
+ </div>
+ </div>
+ </div>
+ <label class="form-label" i18n>Edit permissions</label>
+ <div>
+ <div class="row mb-1">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-user type="change" formControlName="remove_change_users"></pngx-permissions-user>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
+ </div>
+ <div class="col-lg-9">
+ <pngx-permissions-group type="change" formControlName="remove_change_groups"></pngx-permissions-group>
+ </div>
+ </div>
+ <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
+ </div>
+ </div>
+ </div>
+ </div>
+ }
+ }
+ </div>
+</ng-template>
MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
)
})
+
+ it('should disable or enable action fields based on removal action type', () => {
+ const workflow: Workflow = {
+ name: 'Workflow 1',
+ id: 1,
+ order: 1,
+ enabled: true,
+ triggers: [],
+ actions: [
+ {
+ id: 1,
+ type: WorkflowActionType.Removal,
+ remove_all_tags: true,
+ remove_all_document_types: true,
+ remove_all_correspondents: true,
+ remove_all_storage_paths: true,
+ remove_all_custom_fields: true,
+ remove_all_owners: true,
+ remove_all_permissions: true,
+ },
+ ],
+ }
+ component.object = workflow
+ component.ngOnInit()
+
+ component['checkRemovalActionFields'](workflow)
+
+ // Assert that the action fields are disabled or enabled correctly
+ expect(
+ component.actionFields.at(0).get('remove_tags').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_document_types').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_correspondents').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_storage_paths').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_custom_fields').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_owners').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_view_users').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_view_groups').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_change_users').disabled
+ ).toBeTruthy()
+ expect(
+ component.actionFields.at(0).get('remove_change_groups').disabled
+ ).toBeTruthy()
+
+ workflow.actions[0].remove_all_tags = false
+ workflow.actions[0].remove_all_document_types = false
+ workflow.actions[0].remove_all_correspondents = false
+ workflow.actions[0].remove_all_storage_paths = false
+ workflow.actions[0].remove_all_custom_fields = false
+ workflow.actions[0].remove_all_owners = false
+ workflow.actions[0].remove_all_permissions = false
+
+ component['checkRemovalActionFields'](workflow)
+
+ // Assert that the action fields are disabled or enabled correctly
+ expect(component.actionFields.at(0).get('remove_tags').disabled).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_document_types').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_correspondents').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_storage_paths').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_custom_fields').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_owners').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_view_users').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_view_groups').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_change_users').disabled
+ ).toBeFalsy()
+ expect(
+ component.actionFields.at(0).get('remove_change_groups').disabled
+ ).toBeFalsy()
+ })
})
id: WorkflowActionType.Assignment,
name: $localize`Assignment`,
},
+ {
+ id: WorkflowActionType.Removal,
+ name: $localize`Removal`,
+ },
]
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
implements OnInit
{
public WorkflowTriggerType = WorkflowTriggerType
+ public WorkflowActionType = WorkflowActionType
templates: Workflow[]
correspondents: Correspondent[]
ngOnInit(): void {
super.ngOnInit()
this.updateAllTriggerActionFields()
+ this.objectForm.valueChanges.subscribe(
+ this.checkRemovalActionFields.bind(this)
+ )
+ this.checkRemovalActionFields(this.objectForm.value)
+ }
+
+ private checkRemovalActionFields(formWorkflow: Workflow) {
+ formWorkflow.actions
+ .filter((action) => action.type === WorkflowActionType.Removal)
+ .forEach((action, i) => {
+ if (action.remove_all_tags) {
+ this.actionFields
+ .at(i)
+ .get('remove_tags')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_tags')
+ .enable({ emitEvent: false })
+ }
+
+ if (action.remove_all_document_types) {
+ this.actionFields
+ .at(i)
+ .get('remove_document_types')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_document_types')
+ .enable({ emitEvent: false })
+ }
+
+ if (action.remove_all_correspondents) {
+ this.actionFields
+ .at(i)
+ .get('remove_correspondents')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_correspondents')
+ .enable({ emitEvent: false })
+ }
+
+ if (action.remove_all_storage_paths) {
+ this.actionFields
+ .at(i)
+ .get('remove_storage_paths')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_storage_paths')
+ .enable({ emitEvent: false })
+ }
+
+ if (action.remove_all_custom_fields) {
+ this.actionFields
+ .at(i)
+ .get('remove_custom_fields')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_custom_fields')
+ .enable({ emitEvent: false })
+ }
+
+ if (action.remove_all_owners) {
+ this.actionFields
+ .at(i)
+ .get('remove_owners')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_owners')
+ .enable({ emitEvent: false })
+ }
+
+ if (action.remove_all_permissions) {
+ this.actionFields
+ .at(i)
+ .get('remove_view_users')
+ .disable({ emitEvent: false })
+ this.actionFields
+ .at(i)
+ .get('remove_view_groups')
+ .disable({ emitEvent: false })
+ this.actionFields
+ .at(i)
+ .get('remove_change_users')
+ .disable({ emitEvent: false })
+ this.actionFields
+ .at(i)
+ .get('remove_change_groups')
+ .disable({ emitEvent: false })
+ } else {
+ this.actionFields
+ .at(i)
+ .get('remove_view_users')
+ .enable({ emitEvent: false })
+ this.actionFields
+ .at(i)
+ .get('remove_view_groups')
+ .enable({ emitEvent: false })
+ this.actionFields
+ .at(i)
+ .get('remove_change_users')
+ .enable({ emitEvent: false })
+ this.actionFields
+ .at(i)
+ .get('remove_change_groups')
+ .enable({ emitEvent: false })
+ }
+ })
}
get triggerFields(): FormArray {
assign_change_users: new FormControl(action.assign_change_users),
assign_change_groups: new FormControl(action.assign_change_groups),
assign_custom_fields: new FormControl(action.assign_custom_fields),
+ remove_tags: new FormControl(action.remove_tags),
+ remove_all_tags: new FormControl(action.remove_all_tags),
+ remove_document_types: new FormControl(action.remove_document_types),
+ remove_all_document_types: new FormControl(
+ action.remove_all_document_types
+ ),
+ remove_correspondents: new FormControl(action.remove_correspondents),
+ remove_all_correspondents: new FormControl(
+ action.remove_all_correspondents
+ ),
+ remove_storage_paths: new FormControl(action.remove_storage_paths),
+ remove_all_storage_paths: new FormControl(
+ action.remove_all_storage_paths
+ ),
+ remove_owners: new FormControl(action.remove_owners),
+ remove_all_owners: new FormControl(action.remove_all_owners),
+ remove_view_users: new FormControl(action.remove_view_users),
+ remove_view_groups: new FormControl(action.remove_view_groups),
+ remove_change_users: new FormControl(action.remove_change_users),
+ remove_change_groups: new FormControl(action.remove_change_groups),
+ remove_all_permissions: new FormControl(action.remove_all_permissions),
+ remove_custom_fields: new FormControl(action.remove_custom_fields),
+ remove_all_custom_fields: new FormControl(
+ action.remove_all_custom_fields
+ ),
}),
{ emitEvent }
)
assign_change_users: [],
assign_change_groups: [],
assign_custom_fields: [],
+ remove_tags: [],
+ remove_all_tags: false,
+ remove_document_types: [],
+ remove_all_document_types: false,
+ remove_correspondents: [],
+ remove_all_correspondents: false,
+ remove_storage_paths: [],
+ remove_all_storage_paths: false,
+ remove_owners: [],
+ remove_all_owners: false,
+ remove_view_users: [],
+ remove_view_groups: [],
+ remove_change_users: [],
+ remove_change_groups: [],
+ remove_all_permissions: false,
+ remove_custom_fields: [],
+ remove_all_custom_fields: false,
}
this.object.actions.push(action)
this.createActionField(action)
-<div class="paperless-input-select">
+<div class="paperless-input-select" [class.disabled]="disabled">
<div>
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
+.paperless-input-select.disabled {
+ cursor: not-allowed;
+
+ ::ng-deep ng-select {
+ pointer-events: none;
+
+ .ng-select-container {
+ background-color: var(--pngx-bg-alt) !important;
+ }
+ }
+}
-<div class="paperless-input-select">
+<div class="paperless-input-select" [class.disabled]="disabled">
<div>
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
+.paperless-input-select.disabled {
+ cursor: not-allowed;
+
+ ::ng-deep ng-select {
+ pointer-events: none;
+
+ .ng-select-container {
+ background-color: var(--pngx-bg-alt) !important;
+ }
+ }
+}
// styles for ng-select child are in styles.scss
.paperless-input-select.disabled {
- .input-group {
+ .input-group,
+ div > div {
cursor: not-allowed;
}
export enum WorkflowActionType {
Assignment = 1,
+ Removal = 2,
}
export interface WorkflowAction extends ObjectWithId {
type: WorkflowActionType
assign_change_groups?: number[] // [Group.id]
assign_custom_fields?: number[] // [CustomField.id]
+
+ remove_tags?: number[] // Tag.id
+
+ remove_all_tags?: boolean
+
+ remove_document_types?: number[] // [DocumentType.id]
+
+ remove_all_document_types?: boolean
+
+ remove_correspondents?: number[] // [Correspondent.id]
+
+ remove_all_correspondents?: boolean
+
+ remove_storage_paths?: number[] // [StoragePath.id]
+
+ remove_all_storage_paths?: boolean
+
+ remove_owners?: number[] // [User.id]
+
+ remove_all_owners?: boolean
+
+ remove_view_users?: number[] // [User.id]
+
+ remove_view_groups?: number[] // [Group.id]
+
+ remove_change_users?: number[] // [User.id]
+
+ remove_change_groups?: number[] // [Group.id]
+
+ remove_all_permissions?: boolean
+
+ remove_custom_fields?: number[] // [CustomField.id]
+
+ remove_all_custom_fields?: boolean
}
from pathlib import Path
from subprocess import CompletedProcess
from subprocess import run
+from typing import TYPE_CHECKING
from typing import Optional
import magic
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
+from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.parsers import DocumentParser
from documents.parsers import ParseError
"""
Get overrides from matching workflows
"""
+ msg = ""
overrides = DocumentMetadataOverrides()
- for workflow in Workflow.objects.filter(enabled=True).order_by("order"):
- template_overrides = DocumentMetadataOverrides()
+ for workflow in (
+ Workflow.objects.filter(enabled=True)
+ .prefetch_related("actions")
+ .prefetch_related("actions__assign_view_users")
+ .prefetch_related("actions__assign_view_groups")
+ .prefetch_related("actions__assign_change_users")
+ .prefetch_related("actions__assign_change_groups")
+ .prefetch_related("actions__assign_custom_fields")
+ .prefetch_related("actions__remove_tags")
+ .prefetch_related("actions__remove_correspondents")
+ .prefetch_related("actions__remove_document_types")
+ .prefetch_related("actions__remove_storage_paths")
+ .prefetch_related("actions__remove_custom_fields")
+ .prefetch_related("actions__remove_owners")
+ .prefetch_related("triggers")
+ .order_by("order")
+ ):
+ action_overrides = DocumentMetadataOverrides()
if document_matches_workflow(
self.input_doc,
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
):
for action in workflow.actions.all():
- if action.assign_title is not None:
- template_overrides.title = action.assign_title
- if action.assign_tags is not None:
- template_overrides.tag_ids = [
- tag.pk for tag in action.assign_tags.all()
- ]
- if action.assign_correspondent is not None:
- template_overrides.correspondent_id = (
- action.assign_correspondent.pk
- )
- if action.assign_document_type is not None:
- template_overrides.document_type_id = (
- action.assign_document_type.pk
- )
- if action.assign_storage_path is not None:
- template_overrides.storage_path_id = (
- action.assign_storage_path.pk
- )
- if action.assign_owner is not None:
- template_overrides.owner_id = action.assign_owner.pk
- if action.assign_view_users is not None:
- template_overrides.view_users = [
- user.pk for user in action.assign_view_users.all()
- ]
- if action.assign_view_groups is not None:
- template_overrides.view_groups = [
- group.pk for group in action.assign_view_groups.all()
- ]
- if action.assign_change_users is not None:
- template_overrides.change_users = [
- user.pk for user in action.assign_change_users.all()
- ]
- if action.assign_change_groups is not None:
- template_overrides.change_groups = [
- group.pk for group in action.assign_change_groups.all()
- ]
- if action.assign_custom_fields is not None:
- template_overrides.custom_field_ids = [
- field.pk for field in action.assign_custom_fields.all()
- ]
-
- overrides.update(template_overrides)
+ if TYPE_CHECKING:
+ assert isinstance(action, WorkflowAction)
+ msg += f"Applying {action} from {workflow}\n"
+ if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT:
+ if action.assign_title is not None:
+ action_overrides.title = action.assign_title
+ if action.assign_tags is not None:
+ action_overrides.tag_ids = list(
+ action.assign_tags.values_list("pk", flat=True),
+ )
+
+ if action.assign_correspondent is not None:
+ action_overrides.correspondent_id = (
+ action.assign_correspondent.pk
+ )
+ if action.assign_document_type is not None:
+ action_overrides.document_type_id = (
+ action.assign_document_type.pk
+ )
+ if action.assign_storage_path is not None:
+ action_overrides.storage_path_id = (
+ action.assign_storage_path.pk
+ )
+ if action.assign_owner is not None:
+ action_overrides.owner_id = action.assign_owner.pk
+ if action.assign_view_users is not None:
+ action_overrides.view_users = list(
+ action.assign_view_users.values_list("pk", flat=True),
+ )
+ if action.assign_view_groups is not None:
+ action_overrides.view_groups = list(
+ action.assign_view_groups.values_list("pk", flat=True),
+ )
+ if action.assign_change_users is not None:
+ action_overrides.change_users = list(
+ action.assign_change_users.values_list("pk", flat=True),
+ )
+ if action.assign_change_groups is not None:
+ action_overrides.change_groups = list(
+ action.assign_change_groups.values_list(
+ "pk",
+ flat=True,
+ ),
+ )
+ if action.assign_custom_fields is not None:
+ action_overrides.custom_field_ids = list(
+ action.assign_custom_fields.values_list(
+ "pk",
+ flat=True,
+ ),
+ )
+ overrides.update(action_overrides)
+ elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
+ # Removal actions overwrite the current overrides
+ if action.remove_all_tags:
+ overrides.tag_ids = []
+ elif overrides.tag_ids:
+ for tag in action.remove_custom_fields.filter(
+ pk__in=overrides.tag_ids,
+ ):
+ overrides.tag_ids.remove(tag.pk)
+
+ if action.remove_all_correspondents or (
+ overrides.correspondent_id is not None
+ and action.remove_correspondents.filter(
+ pk=overrides.correspondent_id,
+ ).exists()
+ ):
+ overrides.correspondent_id = None
+
+ if action.remove_all_document_types or (
+ overrides.document_type_id is not None
+ and action.remove_document_types.filter(
+ pk=overrides.document_type_id,
+ ).exists()
+ ):
+ overrides.document_type_id = None
+
+ if action.remove_all_storage_paths or (
+ overrides.storage_path_id is not None
+ and action.remove_storage_paths.filter(
+ pk=overrides.storage_path_id,
+ ).exists()
+ ):
+ overrides.storage_path_id = None
+
+ if action.remove_all_custom_fields:
+ overrides.custom_field_ids = []
+ elif overrides.custom_field_ids:
+ for field in action.remove_custom_fields.filter(
+ pk__in=overrides.custom_field_ids,
+ ):
+ overrides.custom_field_ids.remove(field.pk)
+
+ if action.remove_all_owners or (
+ overrides.owner_id is not None
+ and action.remove_owners.filter(
+ pk=overrides.owner_id,
+ ).exists()
+ ):
+ overrides.owner_id = None
+
+ if action.remove_all_permissions:
+ overrides.view_users = []
+ overrides.view_groups = []
+ overrides.change_users = []
+ overrides.change_groups = []
+ else:
+ if overrides.view_users:
+ for user in action.remove_view_users.filter(
+ pk__in=overrides.view_users,
+ ):
+ overrides.view_users.remove(user.pk)
+ if overrides.change_users:
+ for user in action.remove_change_users.filter(
+ pk__in=overrides.change_users,
+ ):
+ overrides.change_users.remove(user.pk)
+ if overrides.view_groups:
+ for user in action.remove_view_groups.filter(
+ pk__in=overrides.view_groups,
+ ):
+ overrides.view_groups.remove(user.pk)
+ if overrides.change_groups:
+ for user in action.remove_change_groups.filter(
+ pk__in=overrides.change_groups,
+ ):
+ overrides.change_groups.remove(user.pk)
+
self.metadata.update(overrides)
+ return msg
class ConsumerError(Exception):
--- /dev/null
+# Generated by Django 4.2.10 on 2024-02-21 21:19
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("auth", "0012_alter_user_first_name_max_length"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("documents", "1045_alter_customfieldinstance_value_monetary"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_correspondents",
+ field=models.BooleanField(
+ default=False,
+ verbose_name="remove all correspondents",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_custom_fields",
+ field=models.BooleanField(
+ default=False,
+ verbose_name="remove all custom fields",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_document_types",
+ field=models.BooleanField(
+ default=False,
+ verbose_name="remove all document types",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_owners",
+ field=models.BooleanField(default=False, verbose_name="remove all owners"),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_permissions",
+ field=models.BooleanField(
+ default=False,
+ verbose_name="remove all permissions",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_storage_paths",
+ field=models.BooleanField(
+ default=False,
+ verbose_name="remove all storage paths",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_all_tags",
+ field=models.BooleanField(default=False, verbose_name="remove all tags"),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_change_groups",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="auth.group",
+ verbose_name="remove change permissions for these groups",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_change_users",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="remove change permissions for these users",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_correspondents",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="documents.correspondent",
+ verbose_name="remove these correspondent(s)",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_custom_fields",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="documents.customfield",
+ verbose_name="remove these custom fields",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_document_types",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="documents.documenttype",
+ verbose_name="remove these document type(s)",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_owners",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="remove these owner(s)",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_storage_paths",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="documents.storagepath",
+ verbose_name="remove these storage path(s)",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_tags",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="documents.tag",
+ verbose_name="remove these tag(s)",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_view_groups",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="auth.group",
+ verbose_name="remove view permissions for these groups",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="remove_view_users",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to=settings.AUTH_USER_MODEL,
+ verbose_name="remove view permissions for these users",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="assign_correspondent",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="documents.correspondent",
+ verbose_name="assign this correspondent",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="assign_document_type",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="documents.documenttype",
+ verbose_name="assign this document type",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="assign_storage_path",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="+",
+ to="documents.storagepath",
+ verbose_name="assign this storage path",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="assign_tags",
+ field=models.ManyToManyField(
+ blank=True,
+ related_name="+",
+ to="documents.tag",
+ verbose_name="assign this tag",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="type",
+ field=models.PositiveIntegerField(
+ choices=[(1, "Assignment"), (2, "Removal")],
+ default=1,
+ verbose_name="Workflow Action Type",
+ ),
+ ),
+ ]
class WorkflowAction(models.Model):
class WorkflowActionType(models.IntegerChoices):
- ASSIGNMENT = 1, _("Assignment")
+ ASSIGNMENT = (
+ 1,
+ _("Assignment"),
+ )
+ REMOVAL = (
+ 2,
+ _("Removal"),
+ )
type = models.PositiveIntegerField(
_("Workflow Action Type"),
assign_tags = models.ManyToManyField(
Tag,
blank=True,
+ related_name="+",
verbose_name=_("assign this tag"),
)
null=True,
blank=True,
on_delete=models.SET_NULL,
+ related_name="+",
verbose_name=_("assign this document type"),
)
null=True,
blank=True,
on_delete=models.SET_NULL,
+ related_name="+",
verbose_name=_("assign this correspondent"),
)
null=True,
blank=True,
on_delete=models.SET_NULL,
+ related_name="+",
verbose_name=_("assign this storage path"),
)
verbose_name=_("assign these custom fields"),
)
+ remove_tags = models.ManyToManyField(
+ Tag,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove these tag(s)"),
+ )
+
+ remove_all_tags = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all tags"),
+ )
+
+ remove_document_types = models.ManyToManyField(
+ DocumentType,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove these document type(s)"),
+ )
+
+ remove_all_document_types = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all document types"),
+ )
+
+ remove_correspondents = models.ManyToManyField(
+ Correspondent,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove these correspondent(s)"),
+ )
+
+ remove_all_correspondents = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all correspondents"),
+ )
+
+ remove_storage_paths = models.ManyToManyField(
+ StoragePath,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove these storage path(s)"),
+ )
+
+ remove_all_storage_paths = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all storage paths"),
+ )
+
+ remove_owners = models.ManyToManyField(
+ User,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove these owner(s)"),
+ )
+
+ remove_all_owners = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all owners"),
+ )
+
+ remove_view_users = models.ManyToManyField(
+ User,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove view permissions for these users"),
+ )
+
+ remove_view_groups = models.ManyToManyField(
+ Group,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove view permissions for these groups"),
+ )
+
+ remove_change_users = models.ManyToManyField(
+ User,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove change permissions for these users"),
+ )
+
+ remove_change_groups = models.ManyToManyField(
+ Group,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove change permissions for these groups"),
+ )
+
+ remove_all_permissions = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all permissions"),
+ )
+
+ remove_custom_fields = models.ManyToManyField(
+ CustomField,
+ blank=True,
+ related_name="+",
+ verbose_name=_("remove these custom fields"),
+ )
+
+ remove_all_custom_fields = models.BooleanField(
+ default=False,
+ verbose_name=_("remove all custom fields"),
+ )
+
class Meta:
verbose_name = _("workflow action")
verbose_name_plural = _("workflow actions")
"assign_change_users",
"assign_change_groups",
"assign_custom_fields",
+ "remove_all_tags",
+ "remove_tags",
+ "remove_all_correspondents",
+ "remove_correspondents",
+ "remove_all_document_types",
+ "remove_document_types",
+ "remove_all_storage_paths",
+ "remove_storage_paths",
+ "remove_custom_fields",
+ "remove_all_custom_fields",
+ "remove_all_owners",
+ "remove_owners",
+ "remove_all_permissions",
+ "remove_view_users",
+ "remove_view_groups",
+ "remove_change_users",
+ "remove_change_groups",
]
def validate(self, attrs):
assign_change_users = action.pop("assign_change_users", None)
assign_change_groups = action.pop("assign_change_groups", None)
assign_custom_fields = action.pop("assign_custom_fields", None)
+ remove_tags = action.pop("remove_tags", None)
+ remove_correspondents = action.pop("remove_correspondents", None)
+ remove_document_types = action.pop("remove_document_types", None)
+ remove_storage_paths = action.pop("remove_storage_paths", None)
+ remove_custom_fields = action.pop("remove_custom_fields", None)
+ remove_owners = action.pop("remove_owners", None)
+ remove_view_users = action.pop("remove_view_users", None)
+ remove_view_groups = action.pop("remove_view_groups", None)
+ remove_change_users = action.pop("remove_change_users", None)
+ remove_change_groups = action.pop("remove_change_groups", None)
+
action_instance, _ = WorkflowAction.objects.update_or_create(
id=action.get("id"),
defaults=action,
)
+
if assign_tags is not None:
action_instance.assign_tags.set(assign_tags)
if assign_view_users is not None:
action_instance.assign_change_groups.set(assign_change_groups)
if assign_custom_fields is not None:
action_instance.assign_custom_fields.set(assign_custom_fields)
+ if remove_tags is not None:
+ action_instance.remove_tags.set(remove_tags)
+ if remove_correspondents is not None:
+ action_instance.remove_correspondents.set(remove_correspondents)
+ if remove_document_types is not None:
+ action_instance.remove_document_types.set(remove_document_types)
+ if remove_storage_paths is not None:
+ action_instance.remove_storage_paths.set(remove_storage_paths)
+ if remove_custom_fields is not None:
+ action_instance.remove_custom_fields.set(remove_custom_fields)
+ if remove_owners is not None:
+ action_instance.remove_owners.set(remove_owners)
+ if remove_view_users is not None:
+ action_instance.remove_view_users.set(remove_view_users)
+ if remove_view_groups is not None:
+ action_instance.remove_view_groups.set(remove_view_groups)
+ if remove_change_users is not None:
+ action_instance.remove_change_users.set(remove_change_users)
+ if remove_change_groups is not None:
+ action_instance.remove_change_groups.set(remove_change_groups)
+
set_actions.append(action_instance)
instance.triggers.set(set_triggers)
from django.dispatch import receiver
from django.utils import timezone
from filelock import FileLock
+from guardian.shortcuts import remove_perm
from documents import matching
from documents.caching import clear_metadata_cache
from documents.models import PaperlessTask
from documents.models import Tag
from documents.models import Workflow
+from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import set_permissions_for_object
document: Document,
logging_group=None,
):
- for workflow in Workflow.objects.filter(
- enabled=True,
- triggers__type=trigger_type,
- ).order_by("order"):
+ for workflow in (
+ Workflow.objects.filter(
+ enabled=True,
+ triggers__type=trigger_type,
+ )
+ .prefetch_related("actions")
+ .prefetch_related("actions__assign_view_users")
+ .prefetch_related("actions__assign_view_groups")
+ .prefetch_related("actions__assign_change_users")
+ .prefetch_related("actions__assign_change_groups")
+ .prefetch_related("actions__assign_custom_fields")
+ .prefetch_related("actions__remove_tags")
+ .prefetch_related("actions__remove_correspondents")
+ .prefetch_related("actions__remove_document_types")
+ .prefetch_related("actions__remove_storage_paths")
+ .prefetch_related("actions__remove_custom_fields")
+ .prefetch_related("actions__remove_owners")
+ .prefetch_related("triggers")
+ .order_by("order")
+ ):
if matching.document_matches_workflow(
document,
workflow,
trigger_type,
):
+ action: WorkflowAction
for action in workflow.actions.all():
logger.info(
f"Applying {action} from {workflow}",
extra={"group": logging_group},
)
- if action.assign_tags.all().count() > 0:
- document.tags.add(*action.assign_tags.all())
-
- if action.assign_correspondent is not None:
- document.correspondent = action.assign_correspondent
-
- if action.assign_document_type is not None:
- document.document_type = action.assign_document_type
-
- if action.assign_storage_path is not None:
- document.storage_path = action.assign_storage_path
-
- if action.assign_owner is not None:
- document.owner = action.assign_owner
-
- if action.assign_title is not None:
- try:
- document.title = parse_doc_title_w_placeholders(
- action.assign_title,
- (
- document.correspondent.name
- if document.correspondent is not None
- else ""
- ),
- (
- document.document_type.name
- if document.document_type is not None
- else ""
- ),
- (
- document.owner.username
- if document.owner is not None
- else ""
- ),
- timezone.localtime(document.added),
- (
- document.original_filename
- if document.original_filename is not None
- else ""
- ),
- timezone.localtime(document.created),
- )
- except Exception:
- logger.exception(
- f"Error occurred parsing title assignment '{action.assign_title}', falling back to original",
- extra={"group": logging_group},
- )
- if (
- (
- action.assign_view_users is not None
- and action.assign_view_users.count() > 0
- )
- or (
- action.assign_view_groups is not None
- and action.assign_view_groups.count() > 0
- )
- or (
- action.assign_change_users is not None
- and action.assign_change_users.count() > 0
- )
- or (
- action.assign_change_groups is not None
- and action.assign_change_groups.count() > 0
- )
- ):
- permissions = {
- "view": {
- "users": action.assign_view_users.all().values_list("id")
- or [],
- "groups": action.assign_view_groups.all().values_list("id")
- or [],
- },
- "change": {
- "users": action.assign_change_users.all().values_list("id")
- or [],
- "groups": action.assign_change_groups.all().values_list(
- "id",
+ if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT:
+ if action.assign_tags.all().count() > 0:
+ document.tags.add(*action.assign_tags.all())
+
+ if action.assign_correspondent is not None:
+ document.correspondent = action.assign_correspondent
+
+ if action.assign_document_type is not None:
+ document.document_type = action.assign_document_type
+
+ if action.assign_storage_path is not None:
+ document.storage_path = action.assign_storage_path
+
+ if action.assign_owner is not None:
+ document.owner = action.assign_owner
+
+ if action.assign_title is not None:
+ try:
+ document.title = parse_doc_title_w_placeholders(
+ action.assign_title,
+ (
+ document.correspondent.name
+ if document.correspondent is not None
+ else ""
+ ),
+ (
+ document.document_type.name
+ if document.document_type is not None
+ else ""
+ ),
+ (
+ document.owner.username
+ if document.owner is not None
+ else ""
+ ),
+ timezone.localtime(document.added),
+ (
+ document.original_filename
+ if document.original_filename is not None
+ else ""
+ ),
+ timezone.localtime(document.created),
)
- or [],
- },
- }
- set_permissions_for_object(
- permissions=permissions,
- object=document,
- merge=True,
- )
-
- if action.assign_custom_fields is not None:
- for field in action.assign_custom_fields.all():
- if (
- CustomFieldInstance.objects.filter(
- field=field,
- document=document,
- ).count()
- == 0
- ):
- # can be triggered on existing docs, so only add the field if it doesn't already exist
- CustomFieldInstance.objects.create(
- field=field,
- document=document,
+ except Exception:
+ logger.exception(
+ f"Error occurred parsing title assignment '{action.assign_title}', falling back to original",
+ extra={"group": logging_group},
)
+ if (
+ (
+ action.assign_view_users is not None
+ and action.assign_view_users.count() > 0
+ )
+ or (
+ action.assign_view_groups is not None
+ and action.assign_view_groups.count() > 0
+ )
+ or (
+ action.assign_change_users is not None
+ and action.assign_change_users.count() > 0
+ )
+ or (
+ action.assign_change_groups is not None
+ and action.assign_change_groups.count() > 0
+ )
+ ):
+ permissions = {
+ "view": {
+ "users": action.assign_view_users.all().values_list(
+ "id",
+ )
+ or [],
+ "groups": action.assign_view_groups.all().values_list(
+ "id",
+ )
+ or [],
+ },
+ "change": {
+ "users": action.assign_change_users.all().values_list(
+ "id",
+ )
+ or [],
+ "groups": action.assign_change_groups.all().values_list(
+ "id",
+ )
+ or [],
+ },
+ }
+ set_permissions_for_object(
+ permissions=permissions,
+ object=document,
+ merge=True,
+ )
+
+ if action.assign_custom_fields is not None:
+ for field in action.assign_custom_fields.all():
+ if (
+ CustomFieldInstance.objects.filter(
+ field=field,
+ document=document,
+ ).count()
+ == 0
+ ):
+ # can be triggered on existing docs, so only add the field if it doesn't already exist
+ CustomFieldInstance.objects.create(
+ field=field,
+ document=document,
+ )
+
+ elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
+ if action.remove_all_tags:
+ document.tags.clear()
+ else:
+ for tag in action.remove_tags.filter(
+ pk__in=list(document.tags.values_list("pk", flat=True)),
+ ).all():
+ document.tags.remove(tag.pk)
+
+ if action.remove_all_correspondents or (
+ document.correspondent
+ and (
+ action.remove_correspondents.filter(
+ pk=document.correspondent.pk,
+ ).exists()
+ )
+ ):
+ document.correspondent = None
+
+ if action.remove_all_document_types or (
+ document.document_type
+ and (
+ action.remove_document_types.filter(
+ pk=document.document_type.pk,
+ ).exists()
+ )
+ ):
+ document.document_type = None
+
+ if action.remove_all_storage_paths or (
+ document.storage_path
+ and (
+ action.remove_storage_paths.filter(
+ pk=document.storage_path.pk,
+ ).exists()
+ )
+ ):
+ document.storage_path = None
+
+ if action.remove_all_owners or (
+ document.owner
+ and (action.remove_owners.filter(pk=document.owner.pk).exists())
+ ):
+ document.owner = None
+
+ if action.remove_all_permissions:
+ permissions = {
+ "view": {
+ "users": [],
+ "groups": [],
+ },
+ "change": {
+ "users": [],
+ "groups": [],
+ },
+ }
+ set_permissions_for_object(
+ permissions=permissions,
+ object=document,
+ merge=False,
+ )
+ elif (
+ (action.remove_view_users.all().count() > 0)
+ or (action.remove_view_groups.all().count() > 0)
+ or (action.remove_change_users.all().count() > 0)
+ or (action.remove_change_groups.all().count() > 0)
+ ):
+ for user in action.remove_view_users.all():
+ remove_perm("view_document", user, document)
+ for user in action.remove_change_users.all():
+ remove_perm("change_document", user, document)
+ for group in action.remove_view_groups.all():
+ remove_perm("view_document", group, document)
+ for group in action.remove_change_groups.all():
+ remove_perm("change_document", group, document)
+
+ if action.remove_all_custom_fields:
+ CustomFieldInstance.objects.filter(document=document).delete()
+ elif action.remove_custom_fields.all().count() > 0:
+ CustomFieldInstance.objects.filter(
+ field__in=action.remove_custom_fields.all(),
+ document=document,
+ ).delete()
+
document.save()
"assign_change_groups": [self.group1.id],
"assign_custom_fields": [self.cf2.id],
},
+ {
+ "type": WorkflowAction.WorkflowActionType.REMOVAL,
+ "remove_tags": [self.t3.id],
+ "remove_document_types": [self.dt.id],
+ "remove_correspondents": [self.c.id],
+ "remove_storage_paths": [self.sp.id],
+ "remove_custom_fields": [self.cf1.id],
+ "remove_owners": [self.user2.id],
+ "remove_view_users": [self.user3.id],
+ "remove_change_users": [self.user3.id],
+ "remove_view_groups": [self.group1.id],
+ "remove_change_groups": [self.group1.id],
+ },
],
},
),
title="test",
)
self.assertRaises(Exception, document_matches_workflow, doc, w, 4)
+
+ def test_removal_action_document_updated_workflow(self):
+ """
+ GIVEN:
+ - Workflow with removal action
+ WHEN:
+ - File that matches is updated
+ THEN:
+ - Action removals are applied
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ filter_path="*",
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.REMOVAL,
+ )
+ action.remove_correspondents.add(self.c)
+ action.remove_tags.add(self.t1)
+ action.remove_document_types.add(self.dt)
+ action.remove_storage_paths.add(self.sp)
+ action.remove_owners.add(self.user2)
+ action.remove_custom_fields.add(self.cf1)
+ action.remove_view_users.add(self.user3)
+ action.remove_view_groups.add(self.group1)
+ action.remove_change_users.add(self.user3)
+ action.remove_change_groups.add(self.group1)
+ action.save()
+
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ document_type=self.dt,
+ storage_path=self.sp,
+ owner=self.user2,
+ original_filename="sample.pdf",
+ )
+ doc.tags.set([self.t1, self.t2])
+ CustomFieldInstance.objects.create(document=doc, field=self.cf1)
+ doc.save()
+ assign_perm("documents.view_document", self.user3, doc)
+ assign_perm("documents.change_document", self.user3, doc)
+ assign_perm("documents.view_document", self.group1, doc)
+ assign_perm("documents.change_document", self.group1, doc)
+
+ superuser = User.objects.create_superuser("superuser")
+ self.client.force_authenticate(user=superuser)
+
+ self.client.patch(
+ f"/api/documents/{doc.id}/",
+ {"title": "new title"},
+ format="json",
+ )
+ doc.refresh_from_db()
+
+ self.assertIsNone(doc.document_type)
+ self.assertIsNone(doc.correspondent)
+ self.assertIsNone(doc.storage_path)
+ self.assertEqual(doc.tags.all().count(), 1)
+ self.assertIn(self.t2, doc.tags.all())
+ self.assertIsNone(doc.owner)
+ self.assertEqual(doc.custom_fields.all().count(), 0)
+ self.assertFalse(self.user3.has_perm("documents.view_document", doc))
+ self.assertFalse(self.user3.has_perm("documents.change_document", doc))
+ group_perms: QuerySet = get_groups_with_perms(doc)
+ self.assertNotIn(self.group1, group_perms)
+
+ def test_removal_action_document_updated_removeall(self):
+ """
+ GIVEN:
+ - Workflow with removal action with remove all fields set
+ WHEN:
+ - File that matches is updated
+ THEN:
+ - Action removals are applied
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ filter_path="*",
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.REMOVAL,
+ remove_all_correspondents=True,
+ remove_all_tags=True,
+ remove_all_document_types=True,
+ remove_all_storage_paths=True,
+ remove_all_custom_fields=True,
+ remove_all_owners=True,
+ remove_all_permissions=True,
+ )
+ action.save()
+
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ document_type=self.dt,
+ storage_path=self.sp,
+ owner=self.user2,
+ original_filename="sample.pdf",
+ )
+ doc.tags.set([self.t1, self.t2])
+ CustomFieldInstance.objects.create(document=doc, field=self.cf1)
+ doc.save()
+ assign_perm("documents.view_document", self.user3, doc)
+ assign_perm("documents.change_document", self.user3, doc)
+ assign_perm("documents.view_document", self.group1, doc)
+ assign_perm("documents.change_document", self.group1, doc)
+
+ superuser = User.objects.create_superuser("superuser")
+ self.client.force_authenticate(user=superuser)
+
+ self.client.patch(
+ f"/api/documents/{doc.id}/",
+ {"title": "new title"},
+ format="json",
+ )
+ doc.refresh_from_db()
+
+ self.assertIsNone(doc.document_type)
+ self.assertIsNone(doc.correspondent)
+ self.assertIsNone(doc.storage_path)
+ self.assertEqual(doc.tags.all().count(), 0)
+ self.assertEqual(doc.tags.all().count(), 0)
+ self.assertIsNone(doc.owner)
+ self.assertEqual(doc.custom_fields.all().count(), 0)
+ self.assertFalse(self.user3.has_perm("documents.view_document", doc))
+ self.assertFalse(self.user3.has_perm("documents.change_document", doc))
+ group_perms: QuerySet = get_groups_with_perms(doc)
+ self.assertNotIn(self.group1, group_perms)
+
+ @mock.patch("documents.consumer.Consumer.try_consume_file")
+ def test_removal_action_document_consumed(self, m):
+ """
+ GIVEN:
+ - Workflow with assignment and removal actions
+ WHEN:
+ - File that matches is consumed
+ THEN:
+ - Action removals are applied
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ filter_filename="*simple*",
+ )
+ action = WorkflowAction.objects.create(
+ assign_title="Doc from {correspondent}",
+ assign_correspondent=self.c,
+ assign_document_type=self.dt,
+ assign_storage_path=self.sp,
+ assign_owner=self.user2,
+ )
+ action.assign_tags.add(self.t1)
+ action.assign_tags.add(self.t2)
+ action.assign_tags.add(self.t3)
+ action.assign_view_users.add(self.user2)
+ action.assign_view_users.add(self.user3)
+ action.assign_view_groups.add(self.group1)
+ action.assign_view_groups.add(self.group2)
+ action.assign_change_users.add(self.user2)
+ action.assign_change_users.add(self.user3)
+ action.assign_change_groups.add(self.group1)
+ action.assign_change_groups.add(self.group2)
+ action.assign_custom_fields.add(self.cf1)
+ action.assign_custom_fields.add(self.cf2)
+ action.save()
+
+ action2 = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.REMOVAL,
+ )
+ action2.remove_correspondents.add(self.c)
+ action2.remove_tags.add(self.t1)
+ action2.remove_document_types.add(self.dt)
+ action2.remove_storage_paths.add(self.sp)
+ action2.remove_owners.add(self.user2)
+ action2.remove_custom_fields.add(self.cf1)
+ action2.remove_view_users.add(self.user3)
+ action2.remove_change_users.add(self.user3)
+ action2.remove_view_groups.add(self.group1)
+ action2.remove_change_groups.add(self.group1)
+ action2.save()
+
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.actions.add(action2)
+ w.save()
+
+ test_file = self.SAMPLE_DIR / "simple.pdf"
+
+ with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
+ with self.assertLogs("paperless.matching", level="INFO") as cm:
+ tasks.consume_file(
+ ConsumableDocument(
+ source=DocumentSource.ConsumeFolder,
+ original_file=test_file,
+ ),
+ None,
+ )
+ m.assert_called_once()
+ _, overrides = m.call_args
+ self.assertIsNone(overrides["override_correspondent_id"])
+ self.assertIsNone(overrides["override_document_type_id"])
+ self.assertEqual(
+ overrides["override_tag_ids"],
+ [self.t2.pk, self.t3.pk],
+ )
+ self.assertIsNone(overrides["override_storage_path_id"])
+ self.assertIsNone(overrides["override_owner_id"])
+ self.assertEqual(overrides["override_view_users"], [self.user2.pk])
+ self.assertEqual(overrides["override_view_groups"], [self.group2.pk])
+ self.assertEqual(overrides["override_change_users"], [self.user2.pk])
+ self.assertEqual(overrides["override_change_groups"], [self.group2.pk])
+ self.assertEqual(
+ overrides["override_title"],
+ "Doc from {correspondent}",
+ )
+ self.assertEqual(
+ overrides["override_custom_field_ids"],
+ [self.cf2.pk],
+ )
+
+ info = cm.output[0]
+ expected_str = f"Document matched {trigger} from {w}"
+ self.assertIn(expected_str, info)
+
+ @mock.patch("documents.consumer.Consumer.try_consume_file")
+ def test_removal_action_document_consumed_removeall(self, m):
+ """
+ GIVEN:
+ - Workflow with assignment and removal actions with remove all fields set
+ WHEN:
+ - File that matches is consumed
+ THEN:
+ - Action removals are applied
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ filter_filename="*simple*",
+ )
+ action = WorkflowAction.objects.create(
+ assign_title="Doc from {correspondent}",
+ assign_correspondent=self.c,
+ assign_document_type=self.dt,
+ assign_storage_path=self.sp,
+ assign_owner=self.user2,
+ )
+ action.assign_tags.add(self.t1)
+ action.assign_tags.add(self.t2)
+ action.assign_tags.add(self.t3)
+ action.assign_view_users.add(self.user3.pk)
+ action.assign_view_groups.add(self.group1.pk)
+ action.assign_change_users.add(self.user3.pk)
+ action.assign_change_groups.add(self.group1.pk)
+ action.assign_custom_fields.add(self.cf1.pk)
+ action.assign_custom_fields.add(self.cf2.pk)
+ action.save()
+
+ action2 = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.REMOVAL,
+ remove_all_correspondents=True,
+ remove_all_tags=True,
+ remove_all_document_types=True,
+ remove_all_storage_paths=True,
+ remove_all_custom_fields=True,
+ remove_all_owners=True,
+ remove_all_permissions=True,
+ )
+
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.actions.add(action2)
+ w.save()
+
+ test_file = self.SAMPLE_DIR / "simple.pdf"
+
+ with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
+ with self.assertLogs("paperless.matching", level="INFO") as cm:
+ tasks.consume_file(
+ ConsumableDocument(
+ source=DocumentSource.ConsumeFolder,
+ original_file=test_file,
+ ),
+ None,
+ )
+ m.assert_called_once()
+ _, overrides = m.call_args
+ self.assertIsNone(overrides["override_correspondent_id"])
+ self.assertIsNone(overrides["override_document_type_id"])
+ self.assertEqual(
+ overrides["override_tag_ids"],
+ [],
+ )
+ self.assertIsNone(overrides["override_storage_path_id"])
+ self.assertIsNone(overrides["override_owner_id"])
+ self.assertEqual(overrides["override_view_users"], [])
+ self.assertEqual(overrides["override_view_groups"], [])
+ self.assertEqual(overrides["override_change_users"], [])
+ self.assertEqual(overrides["override_change_groups"], [])
+ self.assertEqual(
+ overrides["override_custom_field_ids"],
+ [],
+ )
+
+ info = cm.output[0]
+ expected_str = f"Document matched {trigger} from {w}"
+ self.assertIn(expected_str, info)
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-26 13:34-0800\n"
+"POT-Creation-Date: 2024-02-27 10:51-0800\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
msgid "Automatic"
msgstr ""
-#: documents/models.py:62 documents/models.py:397 documents/models.py:1102
+#: documents/models.py:62 documents/models.py:397 documents/models.py:1218
#: paperless_mail/models.py:18 paperless_mail/models.py:93
msgid "name"
msgstr ""
msgid "workflow triggers"
msgstr ""
-#: documents/models.py:1000
+#: documents/models.py:1002
msgid "Assignment"
msgstr ""
-#: documents/models.py:1003
+#: documents/models.py:1006
+msgid "Removal"
+msgstr ""
+
+#: documents/models.py:1010
msgid "Workflow Action Type"
msgstr ""
-#: documents/models.py:1009
+#: documents/models.py:1016
msgid "assign title"
msgstr ""
-#: documents/models.py:1014
+#: documents/models.py:1021
msgid ""
"Assign a document title, can include some placeholders, see documentation."
msgstr ""
-#: documents/models.py:1022 paperless_mail/models.py:216
+#: documents/models.py:1030 paperless_mail/models.py:216
msgid "assign this tag"
msgstr ""
-#: documents/models.py:1030 paperless_mail/models.py:224
+#: documents/models.py:1039 paperless_mail/models.py:224
msgid "assign this document type"
msgstr ""
-#: documents/models.py:1038 paperless_mail/models.py:238
+#: documents/models.py:1048 paperless_mail/models.py:238
msgid "assign this correspondent"
msgstr ""
-#: documents/models.py:1046
+#: documents/models.py:1057
msgid "assign this storage path"
msgstr ""
-#: documents/models.py:1055
+#: documents/models.py:1066
msgid "assign this owner"
msgstr ""
-#: documents/models.py:1062
+#: documents/models.py:1073
msgid "grant view permissions to these users"
msgstr ""
-#: documents/models.py:1069
+#: documents/models.py:1080
msgid "grant view permissions to these groups"
msgstr ""
-#: documents/models.py:1076
+#: documents/models.py:1087
msgid "grant change permissions to these users"
msgstr ""
-#: documents/models.py:1083
+#: documents/models.py:1094
msgid "grant change permissions to these groups"
msgstr ""
-#: documents/models.py:1090
+#: documents/models.py:1101
msgid "assign these custom fields"
msgstr ""
-#: documents/models.py:1094
+#: documents/models.py:1108
+msgid "remove these tag(s)"
+msgstr ""
+
+#: documents/models.py:1113
+msgid "remove all tags"
+msgstr ""
+
+#: documents/models.py:1120
+msgid "remove these document type(s)"
+msgstr ""
+
+#: documents/models.py:1125
+msgid "remove all document types"
+msgstr ""
+
+#: documents/models.py:1132
+msgid "remove these correspondent(s)"
+msgstr ""
+
+#: documents/models.py:1137
+msgid "remove all correspondents"
+msgstr ""
+
+#: documents/models.py:1144
+msgid "remove these storage path(s)"
+msgstr ""
+
+#: documents/models.py:1149
+msgid "remove all storage paths"
+msgstr ""
+
+#: documents/models.py:1156
+msgid "remove these owner(s)"
+msgstr ""
+
+#: documents/models.py:1161
+msgid "remove all owners"
+msgstr ""
+
+#: documents/models.py:1168
+msgid "remove view permissions for these users"
+msgstr ""
+
+#: documents/models.py:1175
+msgid "remove view permissions for these groups"
+msgstr ""
+
+#: documents/models.py:1182
+msgid "remove change permissions for these users"
+msgstr ""
+
+#: documents/models.py:1189
+msgid "remove change permissions for these groups"
+msgstr ""
+
+#: documents/models.py:1194
+msgid "remove all permissions"
+msgstr ""
+
+#: documents/models.py:1201
+msgid "remove these custom fields"
+msgstr ""
+
+#: documents/models.py:1206
+msgid "remove all custom fields"
+msgstr ""
+
+#: documents/models.py:1210
msgid "workflow action"
msgstr ""
-#: documents/models.py:1095
+#: documents/models.py:1211
msgid "workflow actions"
msgstr ""
-#: documents/models.py:1104 paperless_mail/models.py:95
+#: documents/models.py:1220 paperless_mail/models.py:95
msgid "order"
msgstr ""
-#: documents/models.py:1110
+#: documents/models.py:1226
msgid "triggers"
msgstr ""
-#: documents/models.py:1117
+#: documents/models.py:1233
msgid "actions"
msgstr ""
-#: documents/models.py:1120
+#: documents/models.py:1236
msgid "enabled"
msgstr ""
-#: documents/serialisers.py:113
+#: documents/serialisers.py:114
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
-#: documents/serialisers.py:407
+#: documents/serialisers.py:408
msgid "Invalid color."
msgstr ""
-#: documents/serialisers.py:1073
+#: documents/serialisers.py:1070
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
-#: documents/serialisers.py:1176
+#: documents/serialisers.py:1173
msgid "Invalid variable detected."
msgstr ""