Compare commits

..
Author SHA1 Message Date
Crowdin Bot 793374158f New Crowdin translations by GitHub Action 2026-08-12 12:23:55 +00:00
Trenton HandGitHub 6a02b87dde Feature: Updates remote OCR parser to respect the OCR mode setting (#13408)
* Have the remote parser respect the provided produce_archive_file setting, as already determined via the consumer checks

* Updates the documentation to be correct about the respecting now

* merge conflict fixing
2026-08-11 19:23:46 +00:00
Trenton HandGitHub 59a2651804 Fix: pass document chat queries as a QuerySet instead of a materialized list (#13638)
In tracemalloc based profiling, not materializing the whole Document list
reduced memory to approximately 20% of the baseline, with a peak memory
that scaled with the library size.  Now, the lazt queryset is used and only
the needed pk value is actually contributing to memory
2026-08-11 15:25:08 +00:00
GitHub Actions a99f63e059 Auto translate strings 2026-08-11 14:11:20 +00:00
shamoonandGitHub 939cb52f6e Fix: add pagination to saved views management page (#13646) 2026-08-11 07:08:30 -07:00
shamoonandGitHub 855669ddf9 Fix: fixes for workflow assign custom field values (#13630) 2026-08-10 07:38:07 -07:00
153 changed files with 40691 additions and 41205 deletions
+1 -2
View File
@@ -301,8 +301,7 @@ The following methods are supported:
- `delete` - `delete`
- No `parameters` required - No `parameters` required
- `reprocess` - `reprocess`
- Optional `parameters`: `{ "remote_ocr": true }` to send the documents to the - No `parameters` required
remote OCR engine, see [Remote OCR](usage.md#remote-ocr). Defaults to false.
- `set_permissions` - `set_permissions`
- Requires `parameters`: - Requires `parameters`:
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or - `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
+5 -16
View File
@@ -948,10 +948,11 @@ for display in the web interface.
!!! note !!! note
The **remote OCR parser** (Azure AI) always produces a searchable The **remote OCR parser** (Azure AI) also honors this setting: when
PDF and stores it as the archive copy, regardless of this setting. no archive is requested (`never`, or `auto` with a born-digital PDF),
`ARCHIVE_FILE_GENERATION=never` has no effect when the remote the remote engine is skipped entirely and locally-extracted text is
parser handles a document. used instead, avoiding an unnecessary API call and a duplicate text
layer.
#### [`PAPERLESS_OCR_CLEAN=<mode>`](#PAPERLESS_OCR_CLEAN) {#PAPERLESS_OCR_CLEAN} #### [`PAPERLESS_OCR_CLEAN=<mode>`](#PAPERLESS_OCR_CLEAN) {#PAPERLESS_OCR_CLEAN}
@@ -2047,18 +2048,6 @@ password. All of these options come from their similarly-named [Django settings]
Defaults to None. Defaults to None.
#### [`PAPERLESS_REMOTE_OCR_MODE=<str>`](#PAPERLESS_REMOTE_OCR_MODE) {#PAPERLESS_REMOTE_OCR_MODE}
: Which documents are sent to the remote OCR engine.
- `always`: every document of a supported file type is sent to the remote
engine, bypassing the local OCR engine.
- `workflow_only`: documents are processed locally unless a workflow
explicitly enables remote OCR for them, letting you use the remote engine
selectively.
Defaults to "always".
## AI {#ai} ## AI {#ai}
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED} #### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
-14
View File
@@ -456,20 +456,6 @@ def score(
return 10 return 10
``` ```
**Remote services**
If your parser sends document content to a remote service, declare it:
```python
class MyCustomParser:
uses_remote_service = True
```
Paperless-ngx excludes such parsers when the document being consumed has not
been marked for remote processing, so users can keep remote OCR off by default
and enable it selectively with a workflow. Parsers that do not declare the
attribute are treated as fully local and are always considered.
**Archive and rendition flags** **Archive and rendition flags**
```python ```python
+5 -4
View File
@@ -187,10 +187,11 @@ PAPERLESS_ARCHIVE_FILE_GENERATION=auto
### Remote OCR parser ### Remote OCR parser
If you use the **remote OCR parser** (Azure AI), note that it always produces a If you use the **remote OCR parser** (Azure AI), `ARCHIVE_FILE_GENERATION` is
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never` honored the same way as for the local engine: when no archive is requested
has no effect for documents handled by the remote parser - the archive is produced (`never`, or `auto` with a born-digital PDF), the remote engine is skipped
unconditionally by the remote engine. entirely and locally-extracted text is used instead, avoiding an unnecessary
API call and a duplicate text layer.
## Search Index (Whoosh -> Tantivy) ## Search Index (Whoosh -> Tantivy)
+4 -9
View File
@@ -576,7 +576,9 @@ The following workflow action types are available:
- Tags, correspondent, document type and storage path - Tags, correspondent, document type and storage path
- Document owner - Document owner
- View and / or edit permissions to users or groups - View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set - Custom fields, optionally with a value. If no value is set, the field is only added to the
document and any value it may already have is left untouched. If a value is set, it will
overwrite an existing value of that field on the document.
##### Removal {#workflow-action-removal} ##### Removal {#workflow-action-removal}
@@ -1084,18 +1086,11 @@ Paperless-ngx supports performing OCR on documents using remote services. At the
[Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence). [Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence).
This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with
Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details. These the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details.
settings can be supplied as environment variables or via **Application Configuration**.
Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size Additionally, when using a commercial service with this feature, consider both potential costs as well as any associated file size
or page limitations (e.g. with a free tier). or page limitations (e.g. with a free tier).
By default, every document of a supported file type is sent to the remote engine. To use it more selectively, set the
[remote OCR mode](configuration.md#PAPERLESS_REMOTE_OCR_MODE) to `workflow_only`. Documents are then processed locally
unless a workflow explicitly enables remote OCR for them, so you can limit the remote engine to particular documents.
Setting the mode to `workflow_only` also allows the **Reprocess** actions to selectively use remote OCR for individual documents.
## Architecture ## Architecture
Paperless-ngx consists of the following components: Paperless-ngx consists of the following components:
+8 -8
View File
@@ -599,7 +599,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">84,85</context> <context context-type="linenumber">85,86</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3768927257183755959" datatype="html"> <trans-unit id="3768927257183755959" datatype="html">
@@ -670,7 +670,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">85,86</context> <context context-type="linenumber">86,87</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5079885666748292382" datatype="html"> <trans-unit id="5079885666748292382" datatype="html">
@@ -9907,7 +9907,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">296</context> <context context-type="linenumber">314</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2620006875434695386" datatype="html"> <trans-unit id="2620006875434695386" datatype="html">
@@ -10234,7 +10234,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">290</context> <context context-type="linenumber">308</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3501895737484542570" datatype="html"> <trans-unit id="3501895737484542570" datatype="html">
@@ -10346,28 +10346,28 @@
<source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source> <source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">160</context> <context context-type="linenumber">178</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1660419335376265526" datatype="html"> <trans-unit id="1660419335376265526" datatype="html">
<source>Views saved successfully.</source> <source>Views saved successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">237</context> <context context-type="linenumber">255</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1699877326523238632" datatype="html"> <trans-unit id="1699877326523238632" datatype="html">
<source>Error while saving views.</source> <source>Error while saving views.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">242</context> <context context-type="linenumber">260</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4919025779187821586" datatype="html"> <trans-unit id="4919025779187821586" datatype="html">
<source>Note: Sharing saved views does not share the underlying documents.</source> <source>Note: Sharing saved views does not share the underlying documents.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">278</context> <context context-type="linenumber">296</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1229748338333965418" datatype="html"> <trans-unit id="1229748338333965418" datatype="html">
@@ -14,48 +14,43 @@
<a ngbNavLink>{{category}}</a> <a ngbNavLink>{{category}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="p-3"> <div class="p-3">
@for (section of getCategorySections(category); track section) { <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
@if (section) { @for (option of getCategoryOptions(category); track option.key) {
<h5 class="mt-4 mb-3">{{section}}</h5> <div class="col">
} <div class="card bg-light">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2"> <div class="card-body">
@for (option of getCategoryOptions(category, section); track option.key) { <div class="card-title d-flex align-items-center">
<div class="col"> <h6 class="mb-0">
<div class="card bg-light"> {{option.title}}
<div class="card-body"> </h6>
<div class="card-title d-flex align-items-center"> <a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer">
<h6 class="mb-0"> <i-bs name="info-circle"></i-bs>
{{option.title}} </a>
</h6> @if (isSet(option.key)) {
<a class="btn btn-sm btn-link" title="Read the documentation about this setting" i18n-title [href]="getDocsUrl(option.config_key)" target="_blank" referrerpolicy="no-referrer"> <button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
<i-bs name="info-circle"></i-bs> <i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
</a> </button>
@if (isSet(option.key)) {
<button type="button" class="btn btn-sm btn-link text-danger ms-auto pe-0" title="Reset" i18n-title (click)="resetOption(option.key)">
<i-bs class="me-1" name="x"></i-bs><ng-container i18n>Reset</ng-container>
</button>
}
</div>
<div class="mb-n3">
@switch (option.type) {
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
}
</div>
@if (option.note) {
<div class="form-text fst-italic">{{option.note}}</div>
} }
</div> </div>
<div class="mb-n3">
@switch (option.type) {
@case (ConfigOptionType.Select) { <pngx-input-select [formControlName]="option.key" [error]="errors[option.key]" [items]="option.choices" [allowNull]="true"></pngx-input-select> }
@case (ConfigOptionType.Number) { <pngx-input-number [formControlName]="option.key" [error]="errors[option.key]" [showAdd]="false"></pngx-input-number> }
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
}
</div>
@if (option.note) {
<div class="form-text fst-italic">{{option.note}}</div>
}
</div> </div>
</div> </div>
} </div>
</div> }
} </div>
</div> </div>
</ng-template> </ng-template>
</li> </li>
@@ -8,11 +8,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { import { OutputTypeConfig } from 'src/app/data/paperless-config'
ConfigCategory,
ConfigSection,
OutputTypeConfig,
} from 'src/app/data/paperless-config'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
@@ -162,24 +158,4 @@ describe('ConfigComponent', () => {
component.resetOption('barcodes_enabled') component.resetOption('barcodes_enabled')
expect(component.configForm.get('barcodes_enabled').value).toBeNull() expect(component.configForm.get('barcodes_enabled').value).toBeNull()
}) })
it('should group options into sections within a category, or not', () => {
const sections = component.getCategorySections(ConfigCategory.OCR)
expect(sections).toEqual([null, ConfigSection.RemoteOCR])
expect(
component
.getCategoryOptions(ConfigCategory.OCR)
.map((option) => option.key)
).toContain('output_type')
expect(
component
.getCategoryOptions(ConfigCategory.OCR, ConfigSection.RemoteOCR)
.map((option) => option.key)
).toEqual([
'remote_ocr_engine',
'remote_ocr_api_key',
'remote_ocr_endpoint',
'remote_ocr_mode',
])
})
}) })
@@ -74,20 +74,8 @@ export class ConfigComponent
return Object.values(ConfigCategory) return Object.values(ConfigCategory)
} }
getCategorySections(category: string): string[] { getCategoryOptions(category: string): ConfigOption[] {
return [ return PaperlessConfigOptions.filter((o) => o.category === category)
...new Set(
PaperlessConfigOptions.filter((o) => o.category === category).map(
(o) => o.section ?? null // null means no section
)
),
]
}
getCategoryOptions(category: string, section: string = null): ConfigOption[] {
return PaperlessConfigOptions.filter(
(o) => o.category === category && (o.section ?? null) === section
)
} }
initialConfig: PaperlessConfig initialConfig: PaperlessConfig
@@ -1,28 +0,0 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
@if (messageBold) {
<p class="text-break"><b>{{messageBold}}</b></p>
}
@if (message) {
<p class="mb-0 text-break" [innerHTML]="message"></p>
}
@if (showRemoteOcr) {
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="reprocessRemoteOcr" [(ngModel)]="remoteOcr" />
<label class="form-check-label" for="reprocessRemoteOcr" i18n>Use remote OCR</label>
<div class="form-text" i18n>Sends the document to the configured remote OCR service, which may incur costs.</div>
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
</button>
</div>
@@ -1,72 +0,0 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { RemoteOCRModeConfig } from 'src/app/data/paperless-config'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from 'src/app/services/settings.service'
import { ReprocessConfirmDialogComponent } from './reprocess-confirm-dialog.component'
describe('ReprocessConfirmDialogComponent', () => {
let component: ReprocessConfirmDialogComponent
let fixture: ComponentFixture<ReprocessConfirmDialogComponent>
let settingsService: SettingsService
const createComponent = (configured: boolean, mode: string) => {
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, configured)
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_MODE, mode)
fixture = TestBed.createComponent(ReprocessConfirmDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
}
beforeEach(async () => {
TestBed.configureTestingModule({
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
imports: [ReprocessConfirmDialogComponent],
}).compileComponents()
settingsService = TestBed.inject(SettingsService)
})
it('should not request remote OCR by default', () => {
createComponent(true, RemoteOCRModeConfig.WORKFLOW_ONLY)
expect(component.remoteOcr).toBeFalsy()
})
it('should not offer remote OCR when no engine is configured', () => {
createComponent(false, RemoteOCRModeConfig.WORKFLOW_ONLY)
expect(component.showRemoteOcr).toBeFalsy()
expect(
fixture.nativeElement.querySelector('#reprocessRemoteOcr')
).toBeNull()
})
it('should not offer remote OCR when it already handles every document', () => {
createComponent(true, RemoteOCRModeConfig.ALWAYS)
expect(component.showRemoteOcr).toBeFalsy()
expect(
fixture.nativeElement.querySelector('#reprocessRemoteOcr')
).toBeNull()
})
it('should offer remote OCR when configured and selective', () => {
createComponent(true, RemoteOCRModeConfig.WORKFLOW_ONLY)
expect(component.showRemoteOcr).toBeTruthy()
const checkbox = fixture.nativeElement.querySelector('#reprocessRemoteOcr')
expect(checkbox).not.toBeNull()
checkbox.click()
fixture.detectChanges()
expect(component.remoteOcr).toBeTruthy()
})
})
@@ -1,20 +0,0 @@
import { Component, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { SettingsService } from 'src/app/services/settings.service'
import { ConfirmDialogComponent } from '../confirm-dialog.component'
@Component({
selector: 'pngx-reprocess-confirm-dialog',
templateUrl: './reprocess-confirm-dialog.component.html',
imports: [FormsModule],
})
export class ReprocessConfirmDialogComponent extends ConfirmDialogComponent {
private settings = inject(SettingsService)
remoteOcr: boolean = false
public get showRemoteOcr(): boolean {
// Hidden when it is not configured, or when it already handles every document anyway.
return this.settings.remoteOCRIsSelectable
}
}
@@ -52,10 +52,10 @@ describe('CustomFieldsValuesComponent', () => {
}) })
it('should set selectedFields and map values correctly', () => { it('should set selectedFields and map values correctly', () => {
component.value = { 1: 'value1' } component.value = { 1: 'value1', 3: 0, 4: false }
component.selectedFields = [1, 2] component.selectedFields = [1, 2, 3, 4]
expect(component.selectedFields).toEqual([1, 2]) expect(component.selectedFields).toEqual([1, 2, 3, 4])
expect(component.value).toEqual({ 1: 'value1', 2: null }) expect(component.value).toEqual({ 1: 'value1', 2: null, 3: 0, 4: false })
}) })
it('should return the correct custom field by id', () => { it('should return the correct custom field by id', () => {
@@ -77,7 +77,7 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
this._selectedFields = newFields this._selectedFields = newFields
// map the selected fields to an object with field_id as key and value as value // map the selected fields to an object with field_id as key and value as value
this.value = newFields.reduce((acc, fieldId) => { this.value = newFields.reduce((acc, fieldId) => {
acc[fieldId] = this.value?.[fieldId] || null acc[fieldId] = this.value?.[fieldId] ?? null
return acc return acc
}, {}) }, {})
this.onChange(this.value) this.onChange(this.value)
@@ -963,24 +963,12 @@ describe('DocumentDetailComponent', () => {
component.reprocess() component.reprocess()
const modalCloseSpy = jest.spyOn(openModal, 'close') const modalCloseSpy = jest.spyOn(openModal, 'close')
openModal.componentInstance.confirmClicked.next() openModal.componentInstance.confirmClicked.next()
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, false) expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] })
expect(modalSpy).toHaveBeenCalled() expect(modalSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled()
expect(modalCloseSpy).toHaveBeenCalled() expect(modalCloseSpy).toHaveBeenCalled()
}) })
it('should pass remote OCR choice when reprocessing', () => {
initNormally()
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
reprocessSpy.mockReturnValue(of(true))
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
component.reprocess()
openModal.componentInstance.remoteOcr = true
openModal.componentInstance.confirmClicked.next()
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, true)
})
it('should show error if redo ocr call fails', () => { it('should show error if redo ocr call fails', () => {
initNormally() initNormally()
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments') const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
@@ -97,7 +97,6 @@ import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif' import * as UTIF from 'utif'
import { DocumentDetailFieldID } from '../admin/settings/settings.component' import { DocumentDetailFieldID } from '../admin/settings/settings.component'
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
import { ReprocessConfirmDialogComponent } from '../common/confirm-dialog/reprocess-confirm-dialog/reprocess-confirm-dialog.component'
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component' import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component' import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
@@ -1403,7 +1402,7 @@ export class DocumentDetailComponent
} }
reprocess() { reprocess() {
let modal = this.modalService.open(ReprocessConfirmDialogComponent, { let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.title = $localize`Reprocess confirm` modal.componentInstance.title = $localize`Reprocess confirm`
@@ -1414,10 +1413,7 @@ export class DocumentDetailComponent
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.documentsService this.documentsService
.reprocessDocuments( .reprocessDocuments({ documents: [this.document().id] })
{ documents: [this.document().id] },
modal.componentInstance.remoteOcr
)
.subscribe({ .subscribe({
next: () => { next: () => {
this.toastService.showInfo( this.toastService.showInfo(
@@ -1122,7 +1122,6 @@ describe('BulkEditorComponent', () => {
req.flush(true) req.flush(true)
expect(req.request.body).toEqual({ expect(req.request.body).toEqual({
documents: [3, 4], documents: [3, 4],
remote_ocr: false,
}) })
httpTestingController.match( httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
@@ -51,7 +51,6 @@ import { ToastService } from 'src/app/services/toast.service'
import { flattenTags } from 'src/app/utils/flatten-tags' import { flattenTags } from 'src/app/utils/flatten-tags'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params' import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
import { ReprocessConfirmDialogComponent } from '../../common/confirm-dialog/reprocess-confirm-dialog/reprocess-confirm-dialog.component'
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
@@ -910,7 +909,7 @@ export class BulkEditorComponent
} }
reprocessSelected() { reprocessSelected() {
let modal = this.modalService.open(ReprocessConfirmDialogComponent, { let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
}) })
modal.componentInstance.title = $localize`Reprocess confirm` modal.componentInstance.title = $localize`Reprocess confirm`
@@ -924,10 +923,7 @@ export class BulkEditorComponent
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.executeDocumentAction( this.executeDocumentAction(
modal, modal,
this.documentService.reprocessDocuments( this.documentService.reprocessDocuments(this.getSelectionQuery())
this.getSelectionQuery(),
modal.componentInstance.remoteOcr
)
) )
}) })
} }
@@ -7,7 +7,7 @@
</pngx-page-header> </pngx-page-header>
<form [formGroup]="savedViewsForm" (ngSubmit)="save()"> <form [formGroup]="savedViewsForm" (ngSubmit)="save()">
<ul class="list-group mb-3" formGroupName="savedViews"> <ul class="list-group mb-3" formGroupName="savedViews">
@for (view of savedViews(); track view) { @for (view of pagedSavedViews(); track view) {
<li class="list-group-item py-3"> <li class="list-group-item py-3">
<div [formGroupName]="view.id"> <div [formGroupName]="view.id">
<div class="row"> <div class="row">
@@ -81,6 +81,11 @@
} }
</ul> </ul>
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button> <div class="d-flex align-items-center mb-3">
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button> <button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
@if (savedViews()?.length > pageSize) {
<ngb-pagination class="ms-auto" [pageSize]="pageSize" [collectionSize]="savedViews().length" [page]="page()" [maxSize]="5" (pageChange)="page.set($event)" size="sm" aria-label="Pagination"></ngb-pagination>
}
</div>
</form> </form>
@@ -4,6 +4,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { signal } from '@angular/core' import { signal } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject, of, throwError } from 'rxjs' import { Subject, of, throwError } from 'rxjs'
@@ -222,6 +223,44 @@ describe('SavedViewsComponent', () => {
).toEqual(view.show_on_dashboard) ).toEqual(view.show_on_dashboard)
}) })
it('should page saved views, clamp the page if views are removed', () => {
const manyViews = Array.from({ length: 30 }, (_, i) => ({
id: i + 1,
name: `view${i + 1}`,
})) as SavedView[]
const listSpy = jest.spyOn(savedViewService, 'list').mockReturnValue(
of({
all: manyViews.map((v) => v.id),
count: manyViews.length,
results: manyViews.concat([]),
})
)
component.ngOnInit()
fixture.detectChanges()
expect(listSpy).toHaveBeenCalledWith(1, 100000, null, false, {
full_perms: true,
})
expect(component.pagedSavedViews()).toHaveLength(25)
expect(fixture.debugElement.query(By.css('ngb-pagination'))).not.toBeNull()
// all views have controls, not just the current page
expect(
Object.keys(component.savedViewsForm.get('savedViews').value)
).toHaveLength(30)
component.page.set(2)
expect(component.pagedSavedViews()).toHaveLength(5)
listSpy.mockReturnValue(
of({
all: manyViews.slice(0, 25).map((v) => v.id),
count: 25,
results: manyViews.slice(0, 25),
})
)
component.ngOnInit()
expect(component.page()).toEqual(1)
})
it('should support editing permissions', () => { it('should support editing permissions', () => {
const confirmClicked = new Subject<any>() const confirmClicked = new Subject<any>()
const modalRef = { const modalRef = {
@@ -1,12 +1,19 @@
import { AsyncPipe } from '@angular/common' import { AsyncPipe } from '@angular/common'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core' import {
Component,
OnDestroy,
OnInit,
computed,
inject,
signal,
} from '@angular/core'
import { import {
FormControl, FormControl,
FormGroup, FormGroup,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms' } from '@angular/forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
import { dirtyCheck } from '@ngneat/dirty-check-forms' import { dirtyCheck } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs' import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
@@ -42,6 +49,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
AsyncPipe, AsyncPipe,
NgbPaginationModule,
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
], ],
}) })
@@ -58,6 +66,14 @@ export class SavedViewsComponent
DisplayMode = DisplayMode DisplayMode = DisplayMode
readonly savedViews = signal<SavedView[]>(undefined) readonly savedViews = signal<SavedView[]>(undefined)
readonly page = signal(1)
public readonly pageSize = 25
// All views are loaded at init, so paging is only for display
readonly pagedSavedViews = computed(() => {
const start = (this.page() - 1) * this.pageSize
return this.savedViews()?.slice(start, start + this.pageSize)
})
private savedViewsGroup = new FormGroup({}) private savedViewsGroup = new FormGroup({})
public savedViewsForm: FormGroup = new FormGroup({ public savedViewsForm: FormGroup = new FormGroup({
savedViews: this.savedViewsGroup, savedViews: this.savedViewsGroup,
@@ -84,9 +100,11 @@ export class SavedViewsComponent
private reloadViews(): void { private reloadViews(): void {
this.loading.set(true) this.loading.set(true)
this.savedViewService this.savedViewService
.list(null, null, null, false, { full_perms: true }) .list(1, 100000, null, false, { full_perms: true })
.subscribe((r) => { .subscribe((r) => {
this.savedViews.set(r.results) this.savedViews.set(r.results)
const pageCount = Math.ceil(r.results.length / this.pageSize)
this.page.update((page) => Math.min(page, Math.max(1, pageCount)))
this.initialize() this.initialize()
}) })
} }
-55
View File
@@ -54,10 +54,6 @@ export const ConfigCategory = {
AI: $localize`AI Settings`, AI: $localize`AI Settings`,
} }
export const ConfigSection = {
RemoteOCR: $localize`Remote OCR`,
}
export const LLMEmbeddingBackendConfig = { export const LLMEmbeddingBackendConfig = {
OPENAI_LIKE: 'openai-like', OPENAI_LIKE: 'openai-like',
HUGGINGFACE: 'huggingface', HUGGINGFACE: 'huggingface',
@@ -69,15 +65,6 @@ export const LLMBackendConfig = {
OLLAMA: 'ollama', OLLAMA: 'ollama',
} }
export const RemoteOCREngineConfig = {
AZURE_AI: 'azureai',
}
export const RemoteOCRModeConfig = {
ALWAYS: 'always',
WORKFLOW_ONLY: 'workflow_only',
}
export interface ConfigOption { export interface ConfigOption {
key: string key: string
title: string title: string
@@ -85,7 +72,6 @@ export interface ConfigOption {
choices?: Array<{ id: string; name: string }> choices?: Array<{ id: string; name: string }>
config_key?: string config_key?: string
category: string category: string
section?: string
note?: string note?: string
} }
@@ -195,43 +181,6 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_OCR_USER_ARGS', config_key: 'PAPERLESS_OCR_USER_ARGS',
category: ConfigCategory.OCR, category: ConfigCategory.OCR,
}, },
{
key: 'remote_ocr_engine',
title: $localize`Remote OCR Engine`,
type: ConfigOptionType.Select,
choices: mapToItems(RemoteOCREngineConfig),
config_key: 'PAPERLESS_REMOTE_OCR_ENGINE',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
note: $localize`Enabling remote OCR sends documents to a third-party service for processing. Consider the privacy implications as well as potential costs before enabling.`,
},
{
key: 'remote_ocr_api_key',
title: $localize`Remote OCR API Key`,
type: ConfigOptionType.Password,
config_key: 'PAPERLESS_REMOTE_OCR_API_KEY',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
},
{
key: 'remote_ocr_endpoint',
title: $localize`Remote OCR Endpoint`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_REMOTE_OCR_ENDPOINT',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
note: $localize`Required when using the Azure AI engine.`,
},
{
key: 'remote_ocr_mode',
title: $localize`Remote OCR Mode`,
type: ConfigOptionType.Select,
choices: mapToItems(RemoteOCRModeConfig),
config_key: 'PAPERLESS_REMOTE_OCR_MODE',
category: ConfigCategory.OCR,
section: ConfigSection.RemoteOCR,
note: $localize`Which documents are sent to the remote engine. Use 'workflow_only' to keep remote OCR off unless a workflow enables it for a document.`,
},
{ {
key: 'app_logo', key: 'app_logo',
title: $localize`Application Logo`, title: $localize`Application Logo`,
@@ -449,10 +398,6 @@ export interface PaperlessConfig extends ObjectWithId {
barcode_enable_tag: boolean barcode_enable_tag: boolean
barcode_tag_mapping: object barcode_tag_mapping: object
barcode_tag_split: boolean barcode_tag_split: boolean
remote_ocr_engine: string
remote_ocr_api_key: string
remote_ocr_endpoint: string
remote_ocr_mode: string
ai_enabled: boolean ai_enabled: boolean
llm_embedding_backend: string llm_embedding_backend: string
llm_embedding_model: string llm_embedding_model: string
-13
View File
@@ -1,6 +1,5 @@
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode' import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types' import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
import { RemoteOCRModeConfig } from './paperless-config'
import { User } from './user' import { User } from './user'
export interface UiSettings { export interface UiSettings {
@@ -95,8 +94,6 @@ export const SETTINGS_KEYS = {
OUTLOOK_OAUTH_URL: 'outlook_oauth_url', OUTLOOK_OAUTH_URL: 'outlook_oauth_url',
EMAIL_ENABLED: 'email_enabled', EMAIL_ENABLED: 'email_enabled',
AI_ENABLED: 'ai_enabled', AI_ENABLED: 'ai_enabled',
REMOTE_OCR_CONFIGURED: 'remote_ocr:configured',
REMOTE_OCR_MODE: 'remote_ocr:mode',
} }
export const SETTINGS: UiSetting[] = [ export const SETTINGS: UiSetting[] = [
@@ -350,14 +347,4 @@ export const SETTINGS: UiSetting[] = [
type: 'string', type: 'string',
default: PdfEditorEditMode.Create, default: PdfEditorEditMode.Create,
}, },
{
key: SETTINGS_KEYS.REMOTE_OCR_CONFIGURED,
type: 'boolean',
default: false,
},
{
key: SETTINGS_KEYS.REMOTE_OCR_MODE,
type: 'string',
default: RemoteOCRModeConfig.ALWAYS,
},
] ]
@@ -284,21 +284,6 @@ describe(`DocumentService`, () => {
expect(req.request.method).toEqual('POST') expect(req.request.method).toEqual('POST')
expect(req.request.body).toEqual({ expect(req.request.body).toEqual({
documents: ids, documents: ids,
remote_ocr: false,
})
})
it('should request remote OCR when reprocessing with it enabled', () => {
const ids = [1, 2, 3]
subscription = service
.reprocessDocuments({ documents: ids }, true)
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/reprocess/`
)
expect(req.request.body).toEqual({
documents: ids,
remote_ocr: true,
}) })
}) })
@@ -349,13 +349,9 @@ export class DocumentService extends AbstractPaperlessService<Document> {
}) })
} }
reprocessDocuments( reprocessDocuments(selection: DocumentSelectionQuery) {
selection: DocumentSelectionQuery,
remoteOcr: boolean = false
) {
return this.http.post(this.getResourceUrl(null, 'reprocess'), { return this.http.post(this.getResourceUrl(null, 'reprocess'), {
...selection, ...selection,
remote_ocr: remoteOcr,
}) })
} }
@@ -13,7 +13,6 @@ import { environment } from 'src/environments/environment'
import { CustomFieldDataType } from '../data/custom-field' import { CustomFieldDataType } from '../data/custom-field'
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document' import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
import { SavedView } from '../data/saved-view' import { SavedView } from '../data/saved-view'
import { RemoteOCRModeConfig } from '../data/paperless-config'
import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings' import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings'
import { PermissionsService } from './permissions.service' import { PermissionsService } from './permissions.service'
import { CustomFieldsService } from './rest/custom-fields.service' import { CustomFieldsService } from './rest/custom-fields.service'
@@ -435,26 +434,4 @@ describe('SettingsService', () => {
).name ).name
).toEqual(customFields[0].name) ).toEqual(customFields[0].name)
}) })
it('should offer remote OCR only when configured and selective', () => {
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, false)
settingsService.set(
SETTINGS_KEYS.REMOTE_OCR_MODE,
RemoteOCRModeConfig.WORKFLOW_ONLY
)
expect(settingsService.remoteOCRIsSelectable).toBeFalsy()
// configured, but already handling every document
settingsService.set(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED, true)
settingsService.set(
SETTINGS_KEYS.REMOTE_OCR_MODE,
RemoteOCRModeConfig.ALWAYS
)
expect(settingsService.remoteOCRIsSelectable).toBeFalsy()
settingsService.set(
SETTINGS_KEYS.REMOTE_OCR_MODE,
RemoteOCRModeConfig.WORKFLOW_ONLY
)
expect(settingsService.remoteOCRIsSelectable).toBeTruthy()
})
}) })
@@ -19,7 +19,6 @@ import {
} from 'src/app/utils/color' } from 'src/app/utils/color'
import { DEFAULT_APP_TITLE, environment } from 'src/environments/environment' import { DEFAULT_APP_TITLE, environment } from 'src/environments/environment'
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document' import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
import { RemoteOCRModeConfig } from '../data/paperless-config'
import { SavedView } from '../data/saved-view' import { SavedView } from '../data/saved-view'
import { import {
PAPERLESS_GREEN_HEX, PAPERLESS_GREEN_HEX,
@@ -688,17 +687,6 @@ export class SettingsService {
return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED) return this.settingIsSet(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)
} }
/**
* Offering remote OCR as a choice only makes sense when an engine
* is configured but is not already handling every document.
*/
get remoteOCRIsSelectable(): boolean {
return (
this.get(SETTINGS_KEYS.REMOTE_OCR_CONFIGURED) &&
this.get(SETTINGS_KEYS.REMOTE_OCR_MODE) !== RemoteOCRModeConfig.ALWAYS
)
}
offerTour(): boolean { offerTour(): boolean {
return this.dashboardIsEmpty() && !this.get(SETTINGS_KEYS.TOUR_COMPLETE) return this.dashboardIsEmpty() && !this.get(SETTINGS_KEYS.TOUR_COMPLETE)
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -8
View File
@@ -394,16 +394,10 @@ def delete(doc_ids: list[int]) -> Literal["OK"]:
return "OK" return "OK"
def reprocess(doc_ids: list[int], *, remote_ocr: bool = False) -> Literal["OK"]: def reprocess(doc_ids: list[int]) -> Literal["OK"]:
"""
Re-run parsing for the given documents.
Consumption workflows do not run here, so ``remote_ocr`` is how the user
asks for the remote engine when it is not configured to handle everything.
"""
for document_id in doc_ids: for document_id in doc_ids:
update_document_content_maybe_archive_file.apply_async( update_document_content_maybe_archive_file.apply_async(
kwargs={"document_id": document_id, "remote_ocr": remote_ocr}, kwargs={"document_id": document_id},
headers={"trigger_source": PaperlessTask.TriggerSource.MANUAL}, headers={"trigger_source": PaperlessTask.TriggerSource.MANUAL},
) )
-8
View File
@@ -53,7 +53,6 @@ from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats from documents.utils import copy_file_with_basic_stats
from documents.utils import run_subprocess from documents.utils import run_subprocess
from paperless.config import OcrConfig from paperless.config import OcrConfig
from paperless.config import RemoteOCRConfig
from paperless.models import ArchiveFileGenerationChoices from paperless.models import ArchiveFileGenerationChoices
from paperless.parsers import ParserContext from paperless.parsers import ParserContext
from paperless.parsers import ParserProtocol from paperless.parsers import ParserProtocol
@@ -452,19 +451,12 @@ class ConsumerPlugin(
except Exception as e: except Exception as e:
self.log.error(f"Error attempting to clean PDF: {e}") self.log.error(f"Error attempting to clean PDF: {e}")
# Workflows have already run at this point, so the metadata knows
# whether this document was singled out for remote OCR
allow_remote = (
self.metadata.remote_ocr or RemoteOCRConfig().remote_ocr_by_default
)
# Based on the mime type, get the parser for that type # Based on the mime type, get the parser for that type
parser_class: type[ParserProtocol] | None = ( parser_class: type[ParserProtocol] | None = (
get_parser_registry().get_parser_for_file( get_parser_registry().get_parser_for_file(
mime_type, mime_type,
self.filename, self.filename,
self.working_copy, self.working_copy,
allow_remote=allow_remote,
) )
) )
if not parser_class: if not parser_class:
-3
View File
@@ -34,7 +34,6 @@ class DocumentMetadataOverrides:
skip_asn_if_exists: bool = False skip_asn_if_exists: bool = False
version_label: str | None = None version_label: str | None = None
actor_id: int | None = None actor_id: int | None = None
remote_ocr: bool = False
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides": def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
""" """
@@ -58,8 +57,6 @@ class DocumentMetadataOverrides:
self.actor_id = other.actor_id self.actor_id = other.actor_id
if other.skip_asn_if_exists: if other.skip_asn_if_exists:
self.skip_asn_if_exists = True self.skip_asn_if_exists = True
if other.remote_ocr:
self.remote_ocr = True
if other.version_label is not None: if other.version_label is not None:
self.version_label = other.version_label self.version_label = other.version_label
+8 -10
View File
@@ -1744,7 +1744,7 @@ class DeleteDocumentsSerializer(DocumentSelectionSerializer):
class ReprocessDocumentsSerializer(DocumentSelectionSerializer): class ReprocessDocumentsSerializer(DocumentSelectionSerializer):
remote_ocr = serializers.BooleanField(required=False, default=False) pass
class BulkEditSerializer( class BulkEditSerializer(
@@ -2086,13 +2086,6 @@ class BulkEditSerializer(
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.", f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
) )
def _validate_parameters_reprocess(self, parameters) -> None:
if "remote_ocr" in parameters:
if not isinstance(parameters["remote_ocr"], bool):
raise serializers.ValidationError("remote_ocr must be a boolean")
else:
parameters["remote_ocr"] = False
def validate_parameters_remove_password(self, parameters): def validate_parameters_remove_password(self, parameters):
if "password" not in parameters: if "password" not in parameters:
raise serializers.ValidationError("password not specified") raise serializers.ValidationError("password not specified")
@@ -2157,8 +2150,6 @@ class BulkEditSerializer(
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0]) self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
elif method == bulk_edit.remove_password: elif method == bulk_edit.remove_password:
self.validate_parameters_remove_password(parameters) self.validate_parameters_remove_password(parameters)
elif method == bulk_edit.reprocess:
self._validate_parameters_reprocess(parameters)
return attrs return attrs
@@ -3222,6 +3213,13 @@ class WorkflowActionSerializer(serializers.ModelSerializer[WorkflowAction]):
{"assign_title": f'Invalid f-string detected: "{e.args[0]}"'}, {"assign_title": f'Invalid f-string detected: "{e.args[0]}"'},
) )
if attrs.get("assign_custom_fields_values"):
# Empty strings treated as None to avoid unexpected behavior
attrs["assign_custom_fields_values"] = {
field_id: (None if value == "" else value)
for field_id, value in attrs["assign_custom_fields_values"].items()
}
if ( if (
"type" in attrs "type" in attrs
and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL
+1 -10
View File
@@ -66,7 +66,6 @@ from documents.utils import compute_checksum
from documents.utils import identity from documents.utils import identity
from documents.workflows.utils import get_workflows_for_trigger from documents.workflows.utils import get_workflows_for_trigger
from paperless.config import AIConfig from paperless.config import AIConfig
from paperless.config import RemoteOCRConfig
from paperless.logging import consume_task_id from paperless.logging import consume_task_id
from paperless.parsers import ParserContext from paperless.parsers import ParserContext
from paperless.parsers.registry import get_parser_registry from paperless.parsers.registry import get_parser_registry
@@ -338,17 +337,10 @@ def bulk_update_documents(document_ids) -> None:
@shared_task @shared_task
def update_document_content_maybe_archive_file( def update_document_content_maybe_archive_file(document_id) -> None:
document_id,
*,
remote_ocr: bool = False,
) -> None:
""" """
Re-creates OCR content and thumbnail for a document, and archive file if Re-creates OCR content and thumbnail for a document, and archive file if
it exists. it exists.
Remote OCR is used only when the engine is configured to handle everything
or if explicitly asked for via ``remote_ocr``.
""" """
document = Document.objects.get(id=document_id) document = Document.objects.get(id=document_id)
@@ -358,7 +350,6 @@ def update_document_content_maybe_archive_file(
mime_type, mime_type,
document.original_filename or "", document.original_filename or "",
document.source_path, document.source_path,
allow_remote=remote_ocr or RemoteOCRConfig().remote_ocr_by_default,
) )
if not parser_class: if not parser_class:
@@ -72,10 +72,6 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
"barcode_enable_tag": None, "barcode_enable_tag": None,
"barcode_tag_mapping": None, "barcode_tag_mapping": None,
"barcode_tag_split": None, "barcode_tag_split": None,
"remote_ocr_engine": None,
"remote_ocr_api_key": None,
"remote_ocr_endpoint": None,
"remote_ocr_mode": None,
"ai_enabled": False, "ai_enabled": False,
"llm_embedding_backend": None, "llm_embedding_backend": None,
"llm_embedding_model": None, "llm_embedding_model": None,
@@ -874,49 +870,6 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
config.refresh_from_db() config.refresh_from_db()
self.assertEqual(config.llm_api_key, None) self.assertEqual(config.llm_api_key, None)
def test_update_remote_ocr_api_key(self) -> None:
"""
GIVEN:
- Existing config with remote_ocr_api_key specified
WHEN:
- API to update remote_ocr_api_key is called with all *s
- API to update remote_ocr_api_key is called with empty string
THEN:
- remote_ocr_api_key is unchanged
- remote_ocr_api_key is set to None
"""
config = ApplicationConfiguration.objects.first()
assert config is not None
config.remote_ocr_api_key = "1234567890"
config.save()
# Test with all *
response = self.client.patch(
f"{self.ENDPOINT}1/",
json.dumps(
{
"remote_ocr_api_key": "*" * 32,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
config.refresh_from_db()
self.assertEqual(config.remote_ocr_api_key, "1234567890")
# Test with empty string
response = self.client.patch(
f"{self.ENDPOINT}1/",
json.dumps(
{
"remote_ocr_api_key": "",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
config.refresh_from_db()
self.assertEqual(config.remote_ocr_api_key, None)
def test_enable_ai_index_triggers_update(self) -> None: def test_enable_ai_index_triggers_update(self) -> None:
""" """
GIVEN: GIVEN:
+1 -46
View File
@@ -532,29 +532,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
m.assert_called_once() m.assert_called_once()
args, kwargs = m.call_args args, kwargs = m.call_args
self.assertEqual(args[0], [self.doc1.id]) self.assertEqual(args[0], [self.doc1.id])
self.assertEqual(kwargs, {"remote_ocr": False}) self.assertEqual(len(kwargs), 0)
@mock.patch("documents.views.bulk_edit.reprocess")
def test_reprocess_documents_endpoint_remote_ocr(self, m) -> None:
"""
GIVEN:
- API data to reprocess a document with remote OCR requested
WHEN:
- API is called
THEN:
- reprocess is called with remote_ocr=True
"""
self.setup_mock(m, "reprocess")
response = self.client.post(
"/api/documents/reprocess/",
json.dumps({"documents": [self.doc1.id], "remote_ocr": True}),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
m.assert_called_once()
args, kwargs = m.call_args
self.assertEqual(args[0], [self.doc1.id])
self.assertEqual(kwargs, {"remote_ocr": True})
@mock.patch("documents.serialisers.bulk_edit.set_storage_path") @mock.patch("documents.serialisers.bulk_edit.set_storage_path")
def test_api_set_storage_path(self, m) -> None: def test_api_set_storage_path(self, m) -> None:
@@ -1575,29 +1553,6 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
), ),
) )
def test_legacy_bulk_edit_reprocess_invalid_remote_ocr(self) -> None:
"""
GIVEN:
- The deprecated bulk_edit endpoint with a non-boolean remote_ocr
WHEN:
- API is called
THEN:
- The request is rejected rather than passed through to the task
"""
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "reprocess",
"parameters": {"remote_ocr": "yes please"},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@mock.patch("documents.views.bulk_edit.edit_pdf") @mock.patch("documents.views.bulk_edit.edit_pdf")
def test_edit_pdf(self, m) -> None: def test_edit_pdf(self, m) -> None:
self.setup_mock(m, "edit_pdf") self.setup_mock(m, "edit_pdf")
@@ -60,10 +60,6 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
}, },
"email_enabled": False, "email_enabled": False,
"ai_enabled": False, "ai_enabled": False,
"remote_ocr": {
"configured": False,
"mode": "always",
},
}, },
) )
@@ -158,50 +154,6 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
str(response.data["settings"]), str(response.data["settings"]),
) )
@override_settings(
REMOTE_OCR_ENGINE="azureai",
REMOTE_OCR_API_KEY="somekey",
REMOTE_OCR_ENDPOINT="https://example.cognitiveservices.azure.com",
REMOTE_OCR_MODE="workflow_only",
)
def test_settings_reports_remote_ocr_when_configured(self) -> None:
"""
GIVEN:
- A fully configured remote OCR engine in workflow_only mode
WHEN:
- The ui_settings endpoint is called
THEN:
- The UI is told remote OCR is available and selective, so it can
offer it where it would actually change something
"""
response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data["settings"]["remote_ocr"],
{"configured": True, "mode": "workflow_only"},
)
@override_settings(
REMOTE_OCR_ENGINE="azureai",
REMOTE_OCR_API_KEY=None,
REMOTE_OCR_ENDPOINT=None,
)
def test_settings_reports_remote_ocr_incompletely_configured(self) -> None:
"""
GIVEN:
- An engine named but missing its endpoint and API key
WHEN:
- The ui_settings endpoint is called
THEN:
- It is reported as not configured, matching what the parser
registry will actually do
"""
response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data["settings"]["remote_ocr"]["configured"])
@override_settings( @override_settings(
OAUTH_CALLBACK_BASE_URL="http://localhost:8000", OAUTH_CALLBACK_BASE_URL="http://localhost:8000",
GMAIL_OAUTH_CLIENT_ID="abc123", GMAIL_OAUTH_CLIENT_ID="abc123",
@@ -422,6 +422,11 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
json.dumps( json.dumps(
{ {
"assign_title": "", "assign_title": "",
"assign_custom_fields": [self.cf1.id, self.cf2.id],
"assign_custom_fields_values": {
str(self.cf1.id): "",
str(self.cf2.id): 0,
},
}, },
), ),
content_type="application/json", content_type="application/json",
@@ -429,6 +434,10 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
action = WorkflowAction.objects.get(id=response.data["id"]) action = WorkflowAction.objects.get(id=response.data["id"])
self.assertIsNone(action.assign_title) self.assertIsNone(action.assign_title)
self.assertEqual(
action.assign_custom_fields_values,
{str(self.cf1.id): None, str(self.cf2.id): 0},
)
response = self.client.post( response = self.client.post(
self.ENDPOINT_TRIGGERS, self.ENDPOINT_TRIGGERS,
-53
View File
@@ -1782,56 +1782,3 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertIn("wrong password", str(exc.exception)) self.assertIn("wrong password", str(exc.exception))
self.assertIn("Error removing password from document", cm.output[0]) self.assertIn("Error removing password from document", cm.output[0])
class TestBulkEditReprocess(DirectoriesMixin, TestCase):
def setUp(self) -> None:
super().setUp()
self.doc = Document.objects.create(
title="test",
checksum="A",
mime_type="application/pdf",
)
@mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file")
def test_reprocess_defaults_to_local(self, mock_task: mock.Mock) -> None:
"""
GIVEN:
- A reprocess request that says nothing about remote OCR
WHEN:
- reprocess is called
THEN:
- The task is queued without asking for the remote engine
"""
result = bulk_edit.reprocess([self.doc.id])
self.assertEqual(result, "OK")
mock_task.apply_async.assert_called_once()
_, kwargs = mock_task.apply_async.call_args
self.assertEqual(
kwargs["kwargs"],
{"document_id": self.doc.id, "remote_ocr": False},
)
@mock.patch("documents.bulk_edit.update_document_content_maybe_archive_file")
def test_reprocess_passes_remote_ocr(self, mock_task: mock.Mock) -> None:
"""
GIVEN:
- A reprocess request that explicitly asks for remote OCR
WHEN:
- reprocess is called
THEN:
- The request is forwarded to the task for every document
"""
other = Document.objects.create(
title="test2",
checksum="B",
mime_type="application/pdf",
)
bulk_edit.reprocess([self.doc.id, other.id], remote_ocr=True)
self.assertEqual(mock_task.apply_async.call_count, 2)
for call in mock_task.apply_async.call_args_list:
self.assertTrue(call.kwargs["kwargs"]["remote_ocr"])
-80
View File
@@ -1559,72 +1559,6 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
consumer.run_post_consume_script(doc) consumer.run_post_consume_script(doc)
class TestConsumerRemoteOCR(
DirectoriesMixin,
FileSystemAssertsMixin,
GetConsumerMixin,
TestCase,
):
"""
The consumer resolves the remote OCR mode and the per-document request from
workflows into the allow_remote flag it hands to the parser registry.
"""
def setUp(self) -> None:
super().setUp()
patcher = mock.patch("documents.consumer.get_parser_registry")
self.mock_registry = patcher.start()
self.mock_registry.return_value.get_parser_for_file.return_value = DummyParser
self.addCleanup(patcher.stop)
def _consume(self, *, overrides: DocumentMetadataOverrides | None = None) -> bool:
src = (
Path(__file__).parent
/ "samples"
/ "documents"
/ "originals"
/ "0000001.pdf"
)
dst = self.dirs.scratch_dir / "sample.pdf"
shutil.copy(src, dst)
with self.get_consumer(dst, overrides=overrides) as consumer:
consumer.run()
_, kwargs = self.mock_registry.return_value.get_parser_for_file.call_args
return kwargs["allow_remote"]
@override_settings(REMOTE_OCR_MODE="always")
def test_always_mode_allows_remote(self) -> None:
"""
GIVEN: Remote OCR mode is 'always'.
WHEN: A document is consumed without any workflow asking for it.
THEN: The registry is allowed to pick the remote parser.
"""
self.assertTrue(self._consume())
@override_settings(REMOTE_OCR_MODE="workflow_only")
def test_workflow_only_mode_denies_remote_by_default(self) -> None:
"""
GIVEN: Remote OCR mode is 'workflow_only'.
WHEN: A document is consumed and nothing asked for remote OCR.
THEN: The remote parser is excluded.
"""
self.assertFalse(self._consume())
@override_settings(REMOTE_OCR_MODE="workflow_only")
def test_workflow_only_mode_allows_remote_when_requested(self) -> None:
"""
GIVEN: Remote OCR mode is 'workflow_only'.
WHEN: A workflow set remote_ocr on the metadata overrides.
THEN: The registry is allowed to pick the remote parser.
"""
self.assertTrue(
self._consume(overrides=DocumentMetadataOverrides(remote_ocr=True)),
)
class TestMetadataOverrides(TestCase): class TestMetadataOverrides(TestCase):
def test_update_skip_asn_if_exists(self) -> None: def test_update_skip_asn_if_exists(self) -> None:
base = DocumentMetadataOverrides() base = DocumentMetadataOverrides()
@@ -1632,20 +1566,6 @@ class TestMetadataOverrides(TestCase):
base.update(incoming) base.update(incoming)
self.assertTrue(base.skip_asn_if_exists) self.assertTrue(base.skip_asn_if_exists)
def test_update_remote_ocr(self) -> None:
base = DocumentMetadataOverrides()
base.update(DocumentMetadataOverrides(remote_ocr=True))
self.assertTrue(base.remote_ocr)
def test_update_remote_ocr_is_not_unset(self) -> None:
"""
A later workflow that says nothing must not undo an earlier one that
asked for remote OCR.
"""
base = DocumentMetadataOverrides(remote_ocr=True)
base.update(DocumentMetadataOverrides())
self.assertTrue(base.remote_ocr)
def test_update_actor_and_version_label(self) -> None: def test_update_actor_and_version_label(self) -> None:
base = DocumentMetadataOverrides( base = DocumentMetadataOverrides(
actor_id=1, actor_id=1,
-39
View File
@@ -287,45 +287,6 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test") self.assertNotEqual(Document.objects.get(pk=doc.pk).content, "test")
class TestUpdateContentRemoteOCR(DirectoriesMixin, TestCase):
"""
Consumption workflows do not run on reprocess, so the remote parser is
used only in 'always' mode or when the caller explicitly asks for it.
"""
def setUp(self) -> None:
super().setUp()
patcher = mock.patch("documents.tasks.get_parser_registry")
self.mock_registry = patcher.start()
self.mock_registry.return_value.get_parser_for_file.return_value = None
self.addCleanup(patcher.stop)
self.doc = Document.objects.create(
title="test",
content="my document",
checksum="wow",
mime_type="application/pdf",
)
def _allow_remote(self, **kwargs) -> bool:
tasks.update_document_content_maybe_archive_file(self.doc.pk, **kwargs)
_, call_kwargs = self.mock_registry.return_value.get_parser_for_file.call_args
return call_kwargs["allow_remote"]
@override_settings(REMOTE_OCR_MODE="always")
def test_always_mode_allows_remote(self) -> None:
self.assertTrue(self._allow_remote())
@override_settings(REMOTE_OCR_MODE="workflow_only")
def test_workflow_only_mode_denies_remote_by_default(self) -> None:
self.assertFalse(self._allow_remote())
@override_settings(REMOTE_OCR_MODE="workflow_only")
def test_workflow_only_mode_allows_remote_when_requested(self) -> None:
self.assertTrue(self._allow_remote(remote_ocr=True))
class TestAIIndex(DirectoriesMixin, TestCase): class TestAIIndex(DirectoriesMixin, TestCase):
@override_settings( @override_settings(
AI_ENABLED=True, AI_ENABLED=True,
+49
View File
@@ -2000,6 +2000,55 @@ class TestWorkflows(
r"Doc added in \w{3,}", r"Doc added in \w{3,}",
) # Match any 3-letter month name ) # Match any 3-letter month name
def test_document_updated_workflow_existing_custom_field_empty_value(self) -> None:
"""
GIVEN:
- Existing workflow with UPDATED trigger and action that assigns a custom field
with an empty value
WHEN:
- Document is updated that already contains the field with a value
THEN:
- The existing value is left untouched, see GH #13627
"""
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
filter_has_document_type=self.dt,
)
action = WorkflowAction.objects.create()
action.assign_custom_fields.add(self.cf1)
action.assign_custom_fields_values = {self.cf1.pk: ""}
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,
original_filename="sample.pdf",
)
CustomFieldInstance.objects.create(
document=doc,
field=self.cf1,
value_text="existing value",
)
superuser = User.objects.create_superuser("superuser")
self.client.force_authenticate(user=superuser)
self.client.patch(
f"/api/documents/{doc.id}/",
{"document_type": self.dt.id},
format="json",
)
doc.refresh_from_db()
self.assertEqual(doc.custom_fields.get(field=self.cf1).value, "existing value")
def test_document_updated_workflow_existing_custom_field(self) -> None: def test_document_updated_workflow_existing_custom_field(self) -> None:
""" """
GIVEN: GIVEN:
+1 -8
View File
@@ -236,10 +236,8 @@ from paperless import version
from paperless.celery import app as celery_app from paperless.celery import app as celery_app
from paperless.config import AIConfig from paperless.config import AIConfig
from paperless.config import GeneralConfig from paperless.config import GeneralConfig
from paperless.config import RemoteOCRConfig
from paperless.models import ApplicationConfiguration from paperless.models import ApplicationConfiguration
from paperless.parsers.registry import get_parser_registry from paperless.parsers.registry import get_parser_registry
from paperless.parsers.remote import RemoteEngineConfig
from paperless.serialisers import GroupSerializer from paperless.serialisers import GroupSerializer
from paperless.serialisers import UserSerializer from paperless.serialisers import UserSerializer
from paperless.views import StandardPagination from paperless.views import StandardPagination
@@ -2269,7 +2267,7 @@ class ChatStreamingView(GenericAPIView[Any]):
if not has_perms_owner_aware(request.user, "view_document", document): if not has_perms_owner_aware(request.user, "view_document", document):
return HttpResponseForbidden("Insufficient permissions") return HttpResponseForbidden("Insufficient permissions")
documents = [document] documents = Document.objects.filter(pk=document.pk)
else: else:
documents = Document.objects.filter( documents = Document.objects.filter(
id__in=permitted_document_ids(request.user), id__in=permitted_document_ids(request.user),
@@ -4012,11 +4010,6 @@ class UiSettingsView(GenericAPIView[Any]):
ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED
ui_settings["remote_ocr"] = {
"configured": RemoteEngineConfig.from_app_config().engine_is_valid(),
"mode": RemoteOCRConfig().remote_ocr_mode,
}
if settings.GMAIL_OAUTH_ENABLED or settings.OUTLOOK_OAUTH_ENABLED: if settings.GMAIL_OAUTH_ENABLED or settings.OUTLOOK_OAUTH_ENABLED:
manager = PaperlessMailOAuth2Manager() manager = PaperlessMailOAuth2Manager()
if settings.GMAIL_OAUTH_ENABLED: if settings.GMAIL_OAUTH_ENABLED:
+2 -1
View File
@@ -105,7 +105,8 @@ def apply_assignment_to_document(
field=field, field=field,
document=document, document=document,
).first() ).first()
if instance and args[value_field_name] is not None: # empty string is indistinguishable from no value in the UI
if instance and args[value_field_name] not in (None, ""):
setattr(instance, value_field_name, args[value_field_name]) setattr(instance, value_field_name, args[value_field_name])
instance.save() instance.save()
elif not instance: elif not instance:
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Afrikaans\n" "Language-Team: Afrikaans\n"
"Language: af_ZA\n" "Language: af_ZA\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumente" msgstr "Dokumente"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Waarde moet geldige JSON wees." msgstr "Waarde moet geldige JSON wees."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ongeldige gepasmaakte veldnavraaguitdrukking" msgstr "Ongeldige gepasmaakte veldnavraaguitdrukking"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ongeldige uitdrukking lys. Moet nie leeg wees nie." msgstr "Ongeldige uitdrukking lys. Moet nie leeg wees nie."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ongeldige logiese uitdrukking {op!r}" msgstr "Ongeldige logiese uitdrukking {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "" msgstr ""
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "" msgstr ""
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "" msgstr ""
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ongeldige kleur." msgstr "Ongeldige kleur."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Lêertipe %(type)s word nie ondersteun nie" msgstr "Lêertipe %(type)s word nie ondersteun nie"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ongeldige veranderlike bespeur." msgstr "Ongeldige veranderlike bespeur."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Amharic\n" "Language-Team: Amharic\n"
"Language: am_ET\n" "Language: am_ET\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "መዝገባት" msgstr "መዝገባት"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "የሚሰራው እሴት \"JSON\" መሆን አለበት" msgstr "የሚሰራው እሴት \"JSON\" መሆን አለበት"
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "ልክ ያልሆነ የተወሰነ የቦታ መጠይቅ አገላለጽ" msgstr "ልክ ያልሆነ የተወሰነ የቦታ መጠይቅ አገላለጽ"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "ልክ ያልሆነ የመግለጫ ዝርዝር። ባዶ መሆን የለበትም።" msgstr "ልክ ያልሆነ የመግለጫ ዝርዝር። ባዶ መሆን የለበትም።"
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "ልክ ያልሆነ የሎጂክ ኦፕሬተር {op!r}" msgstr "ልክ ያልሆነ የሎጂክ ኦፕሬተር {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "ከፍተኛው የጥያቄ ሁኔታዎች/መጠን ብዛት አልፏል።" msgstr "ከፍተኛው የጥያቄ ሁኔታዎች/መጠን ብዛት አልፏል።"
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ይሄ ታዐማኒነት ያለው ልማድ አይደለም።" msgstr "{name!r} ይሄ ታዐማኒነት ያለው ልማድ አይደለም።"
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "ጥያቄን አይደግፍም expr {expr!r}." msgstr "ጥያቄን አይደግፍም expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።" msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።"
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "ይህ ልማድ አልተገኘም" msgstr "ይህ ልማድ አልተገኘም"
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "" msgstr ""
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "" msgstr ""
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "" msgstr ""
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Arabic\n" "Language-Team: Arabic\n"
"Language: ar_SA\n" "Language: ar_SA\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "المستندات" msgstr "المستندات"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "يجب أن تكون القيمة JSON." msgstr "يجب أن تكون القيمة JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "تعبير استعلام غير صالح للحقول المخصصة" msgstr "تعبير استعلام غير صالح للحقول المخصصة"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "قائمة عبارة خاطئة." msgstr "قائمة عبارة خاطئة."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "تجاوز الحد الأقصى لعدد شروط الاستعلام." msgstr "تجاوز الحد الأقصى لعدد شروط الاستعلام."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} حقل مخصص غير صالح." msgstr "{name!r} حقل مخصص غير صالح."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} لا يدعم تعبير الاستعلام {expr!r}." msgstr "{data_type} لا يدعم تعبير الاستعلام {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "لم يتم العثور على حقل مخصص" msgstr "لم يتم العثور على حقل مخصص"
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "لون خاطئ." msgstr "لون خاطئ."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "نوع الملف %(type)s غير مدعوم" msgstr "نوع الملف %(type)s غير مدعوم"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "اكتشاف متغير خاطئ." msgstr "اكتشاف متغير خاطئ."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Belarusian\n" "Language-Team: Belarusian\n"
"Language: be_BY\n" "Language: be_BY\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Дакументы" msgstr "Дакументы"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "" msgstr ""
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "" msgstr ""
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "" msgstr ""
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "" msgstr ""
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "" msgstr ""
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "" msgstr ""
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "" msgstr ""
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "" msgstr ""
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "" msgstr ""
@@ -1338,48 +1338,48 @@ msgstr ""
msgid "workflow runs" msgid "workflow runs"
msgstr "" msgstr ""
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Няправільны колер." msgstr "Няправільны колер."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Тып файла %(type)s не падтрымліваецца" msgstr "Тып файла %(type)s не падтрымліваецца"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Выяўлена няправільная зменная." msgstr "Выяўлена няправільная зменная."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1634,36 +1634,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Bulgarian\n" "Language-Team: Bulgarian\n"
"Language: bg_BG\n" "Language: bg_BG\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Документи" msgstr "Документи"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Стойността трябва да е валидна JSON." msgstr "Стойността трябва да е валидна JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Невалидна заявка на персонализираното полето" msgstr "Невалидна заявка на персонализираното полето"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Списък с невалиден израз. Не може да е празно." msgstr "Списък с невалиден израз. Не може да е празно."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Невалиден логически оператор {op!r}" msgstr "Невалиден логически оператор {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Надвишен е максимален брой за заявки." msgstr "Надвишен е максимален брой за заявки."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} не е валидно персонализирано поле." msgstr "{name!r} не е валидно персонализирано поле."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} не поддържа заявка expr {expr!r}." msgstr "{data_type} не поддържа заявка expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Надвишена е максималната дълбочина на вмъкване." msgstr "Надвишена е максималната дълбочина на вмъкване."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Персонализирано поле не е намерено" msgstr "Персонализирано поле не е намерено"
@@ -1338,48 +1338,48 @@ msgstr "стартиране на работния процес"
msgid "workflow runs" msgid "workflow runs"
msgstr "стартиране на работните процеси" msgstr "стартиране на работните процеси"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Невалиден цвят." msgstr "Невалиден цвят."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Файловия тип %(type)s не се поддържа" msgstr "Файловия тип %(type)s не се поддържа"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Засечена е невалидна променлива." msgstr "Засечена е невалидна променлива."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-12 12:23\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Catalan\n" "Language-Team: Catalan\n"
"Language: ca_ES\n" "Language: ca_ES\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Documents " msgstr "Documents "
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Valor ha de ser un JSON valid." msgstr "Valor ha de ser un JSON valid."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Expressió de camp de consulta invàlid" msgstr "Expressió de camp de consulta invàlid"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Expressió de llista invàlida. No ha d'estar buida." msgstr "Expressió de llista invàlida. No ha d'estar buida."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Invàlid operand lògic {op!r}" msgstr "Invàlid operand lògic {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Condicions de consulta excedits." msgstr "Condicions de consulta excedits."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} no és un camp personalitzat vàlid." msgstr "{name!r} no és un camp personalitzat vàlid."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} no suporta expressió de consulta {expr!r}." msgstr "{data_type} no suporta expressió de consulta {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Màxima profunditat anidada excedida." msgstr "Màxima profunditat anidada excedida."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Camp personalitzat no trobat" msgstr "Camp personalitzat no trobat"
@@ -1338,48 +1338,48 @@ msgstr "data del flux"
msgid "workflow runs" msgid "workflow runs"
msgstr "flux corrents" msgstr "flux corrents"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Permisos insuficients." msgstr "Permisos insuficients."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Color Invàlid." msgstr "Color Invàlid."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Tipus arxiu %(type)s no suportat" msgstr "Tipus arxiu %(type)s no suportat"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "ID de camp personalizat ha de ser enter: %(id)s" msgstr "ID de camp personalizat ha de ser enter: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Camp personalitzat amb ID %(id)s no existeix" msgstr "Camp personalitzat amb ID %(id)s no existeix"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Camps personalitzats han de ser una llista d'enters o un objecte que mapegi els identificadors amb els valors." msgstr "Camps personalitzats han de ser una llista d'enters o un objecte que mapegi els identificadors amb els valors."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Alguns camps personalitzats no existeixen o s'han especificat dues vegades." msgstr "Alguns camps personalitzats no existeixen o s'han especificat dues vegades."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Variable detectada invàlida." msgstr "Variable detectada invàlida."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Duplicat d'identificadors de documents no permès." msgstr "Duplicat d'identificadors de documents no permès."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Documents no trobats: %(ids)s" msgstr "Documents no trobats: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "L'esquema d'URI '{parts.scheme}' no està permès. Esquemes permesos: {'
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "No s'ha pogut analitzar l'URI {value}" msgstr "No s'ha pogut analitzar l'URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Invalid more_like_id" msgstr "Invalid more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Configuració AI invàlida." msgstr "Configuració AI invàlida."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Especifica només un dels següents valors: text, title_search, query o more_like_id." msgstr "Especifica només un dels següents valors: text, title_search, query o more_like_id."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Permisos insuficients per compartir document %(id)s." msgstr "Permisos insuficients per compartir document %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paquet ja s'està processant." msgstr "Paquet ja s'està processant."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "El paquet de link encarà s'està preparant. Prova de nou més tard." msgstr "El paquet de link encarà s'està preparant. Prova de nou més tard."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "El paquet d'enllaç no està disponible." msgstr "El paquet d'enllaç no està disponible."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Czech\n" "Language-Team: Czech\n"
"Language: cs_CZ\n" "Language: cs_CZ\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumenty" msgstr "Dokumenty"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Hodnota musí být platný JSON." msgstr "Hodnota musí být platný JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Neplatný výraz dotazu na vlastní pole" msgstr "Neplatný výraz dotazu na vlastní pole"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Neplatný seznam výrazů. Nesmí být prázdný." msgstr "Neplatný seznam výrazů. Nesmí být prázdný."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Neplatný logický operátor {op!r}" msgstr "Neplatný logický operátor {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Překročen maximální počet podmínek dotazu." msgstr "Překročen maximální počet podmínek dotazu."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} není platné vlastní pole." msgstr "{name!r} není platné vlastní pole."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} nepodporuje výraz dotazu {expr!r}." msgstr "{data_type} nepodporuje výraz dotazu {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Překročena maximální hloubka větvení." msgstr "Překročena maximální hloubka větvení."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Vlastní pole nebylo nalezeno" msgstr "Vlastní pole nebylo nalezeno"
@@ -1338,48 +1338,48 @@ msgstr "spuštění pracovního postupu"
msgid "workflow runs" msgid "workflow runs"
msgstr "spuštění pracovních postupů" msgstr "spuštění pracovních postupů"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Nedostatečná oprávnění." msgstr "Nedostatečná oprávnění."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Neplatná barva." msgstr "Neplatná barva."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Typ souboru %(type)s není podporován" msgstr "Typ souboru %(type)s není podporován"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Vlastní ID pole musí být celé číslo: %(id)s" msgstr "Vlastní ID pole musí být celé číslo: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Vlastní pole s ID %(id)s neexistuje" msgstr "Vlastní pole s ID %(id)s neexistuje"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Vlastní pole musí být seznam celých čísel nebo ID pro mapování objektů na hodnoty." msgstr "Vlastní pole musí být seznam celých čísel nebo ID pro mapování objektů na hodnoty."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Některá vlastní pole neexistují nebo byla zadána dvakrát." msgstr "Některá vlastní pole neexistují nebo byla zadána dvakrát."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Zjištěna neplatná proměnná." msgstr "Zjištěna neplatná proměnná."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1636,36 +1636,36 @@ msgstr "URI schéma '{parts.scheme}' není povoleno. Povolená schémata: {',\n"
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "Nelze zpracovat URI {value}" msgstr "Nelze zpracovat URI {value}"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Nedostatečná oprávnění ke sdílení dokumentu %(id)s." msgstr "Nedostatečná oprávnění ke sdílení dokumentu %(id)s."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Danish\n" "Language-Team: Danish\n"
"Language: da_DK\n" "Language: da_DK\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumenter" msgstr "Dokumenter"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Værdien skal være gyldig JSON." msgstr "Værdien skal være gyldig JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ugyldigt tilpasset feltforespørgselsudtryk" msgstr "Ugyldigt tilpasset feltforespørgselsudtryk"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ugyldig udtryksliste. Må ikke være tom." msgstr "Ugyldig udtryksliste. Må ikke være tom."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ugyldig logisk operatør {op!r}" msgstr "Ugyldig logisk operatør {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maksimalt antal forespørgselsbetingelser overskredet." msgstr "Maksimalt antal forespørgselsbetingelser overskredet."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} er ikke et gyldigt tilpasset felt." msgstr "{name!r} er ikke et gyldigt tilpasset felt."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} understøtter ikke forespørgsel expr {expr!r}." msgstr "{data_type} understøtter ikke forespørgsel expr {expr!r}."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maksimal indlejringsdybde overskredet." msgstr "Maksimal indlejringsdybde overskredet."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Tilpasset felt ikke fundet" msgstr "Tilpasset felt ikke fundet"
@@ -1338,48 +1338,48 @@ msgstr "workflow-kørsel"
msgid "workflow runs" msgid "workflow runs"
msgstr "workflow-kørsler" msgstr "workflow-kørsler"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ugyldig farve." msgstr "Ugyldig farve."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Filtype %(type)s understøttes ikke" msgstr "Filtype %(type)s understøttes ikke"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ugyldig variabel fundet." msgstr "Ugyldig variabel fundet."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:28\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German, Switzerland\n" "Language-Team: German, Switzerland\n"
"Language: de_CH\n" "Language: de_CH\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumente" msgstr "Dokumente"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Wert muss gültiges JSON sein." msgstr "Wert muss gültiges JSON sein."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ungültiger benutzerdefinierter Feldabfrageausdruck" msgstr "Ungültiger benutzerdefinierter Feldabfrageausdruck"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein." msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ungültiger logischer Operator {op!r}" msgstr "Ungültiger logischer Operator {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maximale Anzahl an Abfragebedingungen überschritten." msgstr "Maximale Anzahl an Abfragebedingungen überschritten."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ist kein gültiges Zusatzfeld." msgstr "{name!r} ist kein gültiges Zusatzfeld."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht." msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maximale Verschachtelungstiefe überschritten." msgstr "Maximale Verschachtelungstiefe überschritten."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Benutzerdefiniertes Feld nicht gefunden" msgstr "Benutzerdefiniertes Feld nicht gefunden"
@@ -1338,48 +1338,48 @@ msgstr "Arbeitsablauf-Ausführung"
msgid "workflow runs" msgid "workflow runs"
msgstr "Arbeitsablauf wird ausgeführt" msgstr "Arbeitsablauf wird ausgeführt"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen." msgstr "Unzureichende Berechtigungen."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ungültige Farbe." msgstr "Ungültige Farbe."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Dateityp %(type)s nicht unterstützt" msgstr "Dateityp %(type)s nicht unterstützt"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Feld-ID eines benutzerdefinierten Felds muss eine Ganzzahl sein: %(id)s" msgstr "Feld-ID eines benutzerdefinierten Felds muss eine Ganzzahl sein: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Benutzerdefiniertes Feld mit ID %(id)s existiert nicht" msgstr "Benutzerdefiniertes Feld mit ID %(id)s existiert nicht"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Benutzerdefinierte Felder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein." msgstr "Benutzerdefinierte Felder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Einige benutzerdefinierte Felder existieren nicht oder wurden zweimal angegeben." msgstr "Einige benutzerdefinierte Felder existieren nicht oder wurden zweimal angegeben."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ungültige Variable erkannt." msgstr "Ungültige Variable erkannt."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt." msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumente nicht gefunden: %(ids)s" msgstr "Dokumente nicht gefunden: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "URI-Schema „{parts.scheme}“ ist nicht erlaubt. Erlaubte Schemata: {'
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "URI {value} kann nicht gelesen werden" msgstr "URI {value} kann nicht gelesen werden"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Ungültige more_like_id" msgstr "Ungültige more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Ungültige KI-Konfiguration." msgstr "Ungültige KI-Konfiguration."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an." msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen." msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet." msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut." msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar." msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: German\n" "Language-Team: German\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Dokumente" msgstr "Dokumente"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Wert muss gültiges JSON sein." msgstr "Wert muss gültiges JSON sein."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Ungültiger Zusatzfeld-Abfrageausdruck" msgstr "Ungültiger Zusatzfeld-Abfrageausdruck"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein." msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Ungültiger logischer Operator {op!r}" msgstr "Ungültiger logischer Operator {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Maximale Anzahl an Abfragebedingungen überschritten." msgstr "Maximale Anzahl an Abfragebedingungen überschritten."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "{name!r} ist kein gültiges Zusatzfeld." msgstr "{name!r} ist kein gültiges Zusatzfeld."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht." msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Maximale Verschachtelungstiefe überschritten." msgstr "Maximale Verschachtelungstiefe überschritten."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Zusatzfeld nicht gefunden" msgstr "Zusatzfeld nicht gefunden"
@@ -1338,48 +1338,48 @@ msgstr "Arbeitsablauf-Ausführung"
msgid "workflow runs" msgid "workflow runs"
msgstr "Arbeitsablauf wird ausgeführt" msgstr "Arbeitsablauf wird ausgeführt"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Unzureichende Berechtigungen." msgstr "Unzureichende Berechtigungen."
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Ungültige Farbe." msgstr "Ungültige Farbe."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Dateityp %(type)s nicht unterstützt" msgstr "Dateityp %(type)s nicht unterstützt"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "Zusatzfeld-ID muss eine Ganzzahl sein: %(id)s" msgstr "Zusatzfeld-ID muss eine Ganzzahl sein: %(id)s"
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "Zusatzfeld mit ID %(id)s existiert nicht" msgstr "Zusatzfeld mit ID %(id)s existiert nicht"
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "Zusatzfelder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein." msgstr "Zusatzfelder müssen eine Liste von Ganzzahlen oder ein Objekt mit Zuordnung von IDs zu Werten sein."
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "Einige Zusatzfelder existieren nicht oder wurden zweimal angegeben." msgstr "Einige Zusatzfelder existieren nicht oder wurden zweimal angegeben."
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Ungültige Variable erkannt." msgstr "Ungültige Variable erkannt."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt." msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "Dokumente nicht gefunden: %(ids)s" msgstr "Dokumente nicht gefunden: %(ids)s"
@@ -1635,36 +1635,36 @@ msgstr "URI-Schema „{parts.scheme}“ ist nicht erlaubt. Erlaubte Schemata: {'
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "URI {value} kann nicht gelesen werden" msgstr "URI {value} kann nicht gelesen werden"
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "Ungültige more_like_id" msgstr "Ungültige more_like_id"
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "Ungültige KI-Konfiguration." msgstr "Ungültige KI-Konfiguration."
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "Zeitüberschreitung bei der KI-Backendanfrage." msgstr "Zeitüberschreitung bei der KI-Backendanfrage."
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an." msgstr "Geben Sie nur einen von text, title_search, query, oder more_like_id an."
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen." msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "Paket wird bereits verarbeitet." msgstr "Paket wird bereits verarbeitet."
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut." msgstr "Das Freigabelink-Paket wird noch vorbereitet. Bitte versuchen Sie es später erneut."
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "Das Freigabelink-Paket ist nicht verfügbar." msgstr "Das Freigabelink-Paket ist nicht verfügbar."
+30 -30
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: paperless-ngx\n" "Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-31 16:14+0000\n" "POT-Creation-Date: 2026-08-10 02:25+0000\n"
"PO-Revision-Date: 2026-07-31 16:15\n" "PO-Revision-Date: 2026-08-10 02:27\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Greek\n" "Language-Team: Greek\n"
"Language: el_GR\n" "Language: el_GR\n"
@@ -21,39 +21,39 @@ msgstr ""
msgid "Documents" msgid "Documents"
msgstr "Έγγραφα" msgstr "Έγγραφα"
#: documents/filters.py:472 #: documents/filters.py:471
msgid "Value must be valid JSON." msgid "Value must be valid JSON."
msgstr "Η τιμή πρέπει να είναι σε έγκυρη μορφή JSON." msgstr "Η τιμή πρέπει να είναι σε έγκυρη μορφή JSON."
#: documents/filters.py:491 #: documents/filters.py:490
msgid "Invalid custom field query expression" msgid "Invalid custom field query expression"
msgstr "Μη έγκυρη έκφραση προσαρμοσμένου ερωτήματος πεδίου" msgstr "Μη έγκυρη έκφραση προσαρμοσμένου ερωτήματος πεδίου"
#: documents/filters.py:501 #: documents/filters.py:500
msgid "Invalid expression list. Must be nonempty." msgid "Invalid expression list. Must be nonempty."
msgstr "Μη έγκυρη λίστα έκφρασης. Πρέπει να είναι μη κενή." msgstr "Μη έγκυρη λίστα έκφρασης. Πρέπει να είναι μη κενή."
#: documents/filters.py:522 #: documents/filters.py:521
msgid "Invalid logical operator {op!r}" msgid "Invalid logical operator {op!r}"
msgstr "Μη έγκυρος λογικός τελεστής {op!r}" msgstr "Μη έγκυρος λογικός τελεστής {op!r}"
#: documents/filters.py:536 #: documents/filters.py:535
msgid "Maximum number of query conditions exceeded." msgid "Maximum number of query conditions exceeded."
msgstr "Υπέρβαση μέγιστου αριθμού συνθηκών ερωτήματος." msgstr "Υπέρβαση μέγιστου αριθμού συνθηκών ερωτήματος."
#: documents/filters.py:600 #: documents/filters.py:599
msgid "{name!r} is not a valid custom field." msgid "{name!r} is not a valid custom field."
msgstr "Το προσαρμοσμένο πεδίο {name!r} δεν είναι ένα έγκυρο." msgstr "Το προσαρμοσμένο πεδίο {name!r} δεν είναι ένα έγκυρο."
#: documents/filters.py:637 #: documents/filters.py:636
msgid "{data_type} does not support query expr {expr!r}." msgid "{data_type} does not support query expr {expr!r}."
msgstr "Το {data_type} δεν υποστηρίζει το ερώτημα expr {expr!r}s." msgstr "Το {data_type} δεν υποστηρίζει το ερώτημα expr {expr!r}s."
#: documents/filters.py:752 documents/models.py:136 #: documents/filters.py:755 documents/models.py:136
msgid "Maximum nesting depth exceeded." msgid "Maximum nesting depth exceeded."
msgstr "Υπέρβαση μέγιστου βάθους εμφώλευσης." msgstr "Υπέρβαση μέγιστου βάθους εμφώλευσης."
#: documents/filters.py:1094 #: documents/filters.py:1079
msgid "Custom field not found" msgid "Custom field not found"
msgstr "Το προσαρμοσμένο πεδίο δε βρέθηκε" msgstr "Το προσαρμοσμένο πεδίο δε βρέθηκε"
@@ -1338,48 +1338,48 @@ msgstr "εκτέλεση ροής εργασίας"
msgid "workflow runs" msgid "workflow runs"
msgstr "εκτελέσεις ροής εργασίας" msgstr "εκτελέσεις ροής εργασίας"
#: documents/serialisers.py:522 documents/serialisers.py:874 #: documents/serialisers.py:521 documents/serialisers.py:873
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558 #: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
#: paperless_mail/serialisers.py:155 #: paperless_mail/serialisers.py:155
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: documents/serialisers.py:710 #: documents/serialisers.py:709
msgid "Invalid color." msgid "Invalid color."
msgstr "Άκυρο χρώμα." msgstr "Άκυρο χρώμα."
#: documents/serialisers.py:2248 #: documents/serialisers.py:2244
#, python-format #, python-format
msgid "File type %(type)s not supported" msgid "File type %(type)s not supported"
msgstr "Ο τύπος αρχείου %(type)s δεν υποστηρίζεται" msgstr "Ο τύπος αρχείου %(type)s δεν υποστηρίζεται"
#: documents/serialisers.py:2292 #: documents/serialisers.py:2288
#, python-format #, python-format
msgid "Custom field id must be an integer: %(id)s" msgid "Custom field id must be an integer: %(id)s"
msgstr "" msgstr ""
#: documents/serialisers.py:2299 #: documents/serialisers.py:2295
#, python-format #, python-format
msgid "Custom field with id %(id)s does not exist" msgid "Custom field with id %(id)s does not exist"
msgstr "" msgstr ""
#: documents/serialisers.py:2316 documents/serialisers.py:2326 #: documents/serialisers.py:2312 documents/serialisers.py:2322
msgid "Custom fields must be a list of integers or an object mapping ids to values." msgid "Custom fields must be a list of integers or an object mapping ids to values."
msgstr "" msgstr ""
#: documents/serialisers.py:2321 #: documents/serialisers.py:2317
msgid "Some custom fields don't exist or were specified twice." msgid "Some custom fields don't exist or were specified twice."
msgstr "" msgstr ""
#: documents/serialisers.py:2468 #: documents/serialisers.py:2464
msgid "Invalid variable detected." msgid "Invalid variable detected."
msgstr "Εντοπίστηκε μη έγκυρη μεταβλητή." msgstr "Εντοπίστηκε μη έγκυρη μεταβλητή."
#: documents/serialisers.py:2832 #: documents/serialisers.py:2823
msgid "Duplicate document identifiers are not allowed." msgid "Duplicate document identifiers are not allowed."
msgstr "" msgstr ""
#: documents/serialisers.py:2862 documents/views.py:4517 #: documents/serialisers.py:2853 documents/views.py:4509
#, python-format #, python-format
msgid "Documents not found: %(ids)s" msgid "Documents not found: %(ids)s"
msgstr "" msgstr ""
@@ -1635,36 +1635,36 @@ msgstr ""
msgid "Unable to parse URI {value}" msgid "Unable to parse URI {value}"
msgstr "" msgstr ""
#: documents/views.py:292 documents/views.py:2555 #: documents/views.py:292 documents/views.py:2552
msgid "Invalid more_like_id" msgid "Invalid more_like_id"
msgstr "" msgstr ""
#: documents/views.py:1567 #: documents/views.py:1566
msgid "Invalid AI configuration." msgid "Invalid AI configuration."
msgstr "" msgstr ""
#: documents/views.py:1576 #: documents/views.py:1575
msgid "AI backend request timed out." msgid "AI backend request timed out."
msgstr "" msgstr ""
#: documents/views.py:2380 documents/views.py:2701 #: documents/views.py:2377 documents/views.py:2698
msgid "Specify only one of text, title_search, query, or more_like_id." msgid "Specify only one of text, title_search, query, or more_like_id."
msgstr "" msgstr ""
#: documents/views.py:4529 #: documents/views.py:4522
#, python-format #, python-format
msgid "Insufficient permissions to share document %(id)s." msgid "Insufficient permissions to share document %(id)s."
msgstr "" msgstr ""
#: documents/views.py:4575 #: documents/views.py:4568
msgid "Bundle is already being processed." msgid "Bundle is already being processed."
msgstr "" msgstr ""
#: documents/views.py:4636 #: documents/views.py:4629
msgid "The share link bundle is still being prepared. Please try again later." msgid "The share link bundle is still being prepared. Please try again later."
msgstr "" msgstr ""
#: documents/views.py:4646 #: documents/views.py:4639
msgid "The share link bundle is unavailable." msgid "The share link bundle is unavailable."
msgstr "" msgstr ""

Some files were not shown because too many files have changed in this diff Show More