mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-12 13:53:20 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c96b38f4e | ||
|
|
21ac856e3f | ||
|
|
37d6b02ebc | ||
|
|
7456b52e84 | ||
|
|
6a392ea099 | ||
|
|
2032ad1341 | ||
|
|
cc8fee91c4 | ||
|
|
a5d46a883e | ||
|
|
b0e1793093 | ||
|
|
7e466d1f71 | ||
|
|
7b69a178c0 | ||
|
|
22cd13a8a9 | ||
|
|
72a4676be0 | ||
|
|
6673144d23 | ||
|
|
994a84cf92 | ||
|
|
654ce5d8f3 | ||
|
|
5d5e9b6db4 |
+2
-1
@@ -301,7 +301,8 @@ The following methods are supported:
|
||||
- `delete`
|
||||
- No `parameters` required
|
||||
- `reprocess`
|
||||
- No `parameters` required
|
||||
- Optional `parameters`: `{ "remote_ocr": true }` to send the documents to the
|
||||
remote OCR engine, see [Remote OCR](usage.md#remote-ocr). Defaults to false.
|
||||
- `set_permissions`
|
||||
- Requires `parameters`:
|
||||
- `"set_permissions": PERMISSIONS_OBJ` (see format [above](#permissions)) and / or
|
||||
|
||||
+16
-5
@@ -948,11 +948,10 @@ for display in the web interface.
|
||||
|
||||
!!! note
|
||||
|
||||
The **remote OCR parser** (Azure AI) also honors this setting: when
|
||||
no archive is requested (`never`, or `auto` with a born-digital PDF),
|
||||
the remote engine is skipped entirely and locally-extracted text is
|
||||
used instead, avoiding an unnecessary API call and a duplicate text
|
||||
layer.
|
||||
The **remote OCR parser** (Azure AI) always produces a searchable
|
||||
PDF and stores it as the archive copy, regardless of this setting.
|
||||
`ARCHIVE_FILE_GENERATION=never` has no effect when the remote
|
||||
parser handles a document.
|
||||
|
||||
#### [`PAPERLESS_OCR_CLEAN=<mode>`](#PAPERLESS_OCR_CLEAN) {#PAPERLESS_OCR_CLEAN}
|
||||
|
||||
@@ -2048,6 +2047,18 @@ password. All of these options come from their similarly-named [Django settings]
|
||||
|
||||
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}
|
||||
|
||||
#### [`PAPERLESS_AI_ENABLED=<bool>`](#PAPERLESS_AI_ENABLED) {#PAPERLESS_AI_ENABLED}
|
||||
|
||||
@@ -456,6 +456,20 @@ def score(
|
||||
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**
|
||||
|
||||
```python
|
||||
|
||||
@@ -187,11 +187,10 @@ PAPERLESS_ARCHIVE_FILE_GENERATION=auto
|
||||
|
||||
### Remote OCR parser
|
||||
|
||||
If you use the **remote OCR parser** (Azure AI), `ARCHIVE_FILE_GENERATION` is
|
||||
honored the same way as for the local engine: when no archive is requested
|
||||
(`never`, or `auto` with a born-digital PDF), the remote engine is skipped
|
||||
entirely and locally-extracted text is used instead, avoiding an unnecessary
|
||||
API call and a duplicate text layer.
|
||||
If you use the **remote OCR parser** (Azure AI), note that it always produces a
|
||||
searchable PDF and stores it as the archive copy. `ARCHIVE_FILE_GENERATION=never`
|
||||
has no effect for documents handled by the remote parser - the archive is produced
|
||||
unconditionally by the remote engine.
|
||||
|
||||
## Search Index (Whoosh -> Tantivy)
|
||||
|
||||
|
||||
+9
-4
@@ -576,9 +576,7 @@ The following workflow action types are available:
|
||||
- Tags, correspondent, document type and storage path
|
||||
- Document owner
|
||||
- View and / or edit permissions to users or groups
|
||||
- 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.
|
||||
- Custom fields. Note that no value for the field will be set
|
||||
|
||||
##### Removal {#workflow-action-removal}
|
||||
|
||||
@@ -1086,11 +1084,18 @@ 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).
|
||||
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
|
||||
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details.
|
||||
the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details. These
|
||||
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
|
||||
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
|
||||
|
||||
Paperless-ngx consists of the following components:
|
||||
|
||||
+8
-8
@@ -599,7 +599,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<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">84,85</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3768927257183755959" datatype="html">
|
||||
@@ -670,7 +670,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||
<context context-type="linenumber">86,87</context>
|
||||
<context context-type="linenumber">85,86</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5079885666748292382" datatype="html">
|
||||
@@ -9907,7 +9907,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
|
||||
<context context-type="linenumber">314</context>
|
||||
<context context-type="linenumber">296</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2620006875434695386" datatype="html">
|
||||
@@ -10234,7 +10234,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
|
||||
<context context-type="linenumber">308</context>
|
||||
<context context-type="linenumber">290</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3501895737484542570" datatype="html">
|
||||
@@ -10346,28 +10346,28 @@
|
||||
<source>Saved view "<x id="PH" equiv-text="savedView.name"/>" deleted.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
|
||||
<context context-type="linenumber">178</context>
|
||||
<context context-type="linenumber">160</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1660419335376265526" datatype="html">
|
||||
<source>Views saved successfully.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
|
||||
<context context-type="linenumber">255</context>
|
||||
<context context-type="linenumber">237</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1699877326523238632" datatype="html">
|
||||
<source>Error while saving views.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
|
||||
<context context-type="linenumber">260</context>
|
||||
<context context-type="linenumber">242</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4919025779187821586" datatype="html">
|
||||
<source>Note: Sharing saved views does not share the underlying documents.</source>
|
||||
<context-group purpose="location">
|
||||
<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">278</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1229748338333965418" datatype="html">
|
||||
|
||||
@@ -14,43 +14,48 @@
|
||||
<a ngbNavLink>{{category}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="p-3">
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
||||
@for (option of getCategoryOptions(category); track option.key) {
|
||||
<div class="col">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
{{option.title}}
|
||||
</h6>
|
||||
<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">
|
||||
<i-bs name="info-circle"></i-bs>
|
||||
</a>
|
||||
@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>
|
||||
@for (section of getCategorySections(category); track section) {
|
||||
@if (section) {
|
||||
<h5 class="mt-4 mb-3">{{section}}</h5>
|
||||
}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
|
||||
@for (option of getCategoryOptions(category, section); track option.key) {
|
||||
<div class="col">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex align-items-center">
|
||||
<h6 class="mb-0">
|
||||
{{option.title}}
|
||||
</h6>
|
||||
<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">
|
||||
<i-bs name="info-circle"></i-bs>
|
||||
</a>
|
||||
@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 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>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
@@ -8,7 +8,11 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { OutputTypeConfig } from 'src/app/data/paperless-config'
|
||||
import {
|
||||
ConfigCategory,
|
||||
ConfigSection,
|
||||
OutputTypeConfig,
|
||||
} from 'src/app/data/paperless-config'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@@ -158,4 +162,24 @@ describe('ConfigComponent', () => {
|
||||
component.resetOption('barcodes_enabled')
|
||||
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,8 +74,20 @@ export class ConfigComponent
|
||||
return Object.values(ConfigCategory)
|
||||
}
|
||||
|
||||
getCategoryOptions(category: string): ConfigOption[] {
|
||||
return PaperlessConfigOptions.filter((o) => o.category === category)
|
||||
getCategorySections(category: string): string[] {
|
||||
return [
|
||||
...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
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -52,10 +52,10 @@ describe('CustomFieldsValuesComponent', () => {
|
||||
})
|
||||
|
||||
it('should set selectedFields and map values correctly', () => {
|
||||
component.value = { 1: 'value1', 3: 0, 4: false }
|
||||
component.selectedFields = [1, 2, 3, 4]
|
||||
expect(component.selectedFields).toEqual([1, 2, 3, 4])
|
||||
expect(component.value).toEqual({ 1: 'value1', 2: null, 3: 0, 4: false })
|
||||
component.value = { 1: 'value1' }
|
||||
component.selectedFields = [1, 2]
|
||||
expect(component.selectedFields).toEqual([1, 2])
|
||||
expect(component.value).toEqual({ 1: 'value1', 2: null })
|
||||
})
|
||||
|
||||
it('should return the correct custom field by id', () => {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export class CustomFieldsValuesComponent extends AbstractInputComponent<Object>
|
||||
this._selectedFields = newFields
|
||||
// map the selected fields to an object with field_id as key and value as value
|
||||
this.value = newFields.reduce((acc, fieldId) => {
|
||||
acc[fieldId] = this.value?.[fieldId] ?? null
|
||||
acc[fieldId] = this.value?.[fieldId] || null
|
||||
return acc
|
||||
}, {})
|
||||
this.onChange(this.value)
|
||||
|
||||
@@ -963,12 +963,24 @@ describe('DocumentDetailComponent', () => {
|
||||
component.reprocess()
|
||||
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
||||
openModal.componentInstance.confirmClicked.next()
|
||||
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] })
|
||||
expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }, false)
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).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', () => {
|
||||
initNormally()
|
||||
const reprocessSpy = jest.spyOn(documentService, 'reprocessDocuments')
|
||||
|
||||
@@ -97,6 +97,7 @@ import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import * as UTIF from 'utif'
|
||||
import { DocumentDetailFieldID } from '../admin/settings/settings.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 { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
@@ -1402,7 +1403,7 @@ export class DocumentDetailComponent
|
||||
}
|
||||
|
||||
reprocess() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
let modal = this.modalService.open(ReprocessConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Reprocess confirm`
|
||||
@@ -1413,7 +1414,10 @@ export class DocumentDetailComponent
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.documentsService
|
||||
.reprocessDocuments({ documents: [this.document().id] })
|
||||
.reprocessDocuments(
|
||||
{ documents: [this.document().id] },
|
||||
modal.componentInstance.remoteOcr
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
|
||||
@@ -1122,6 +1122,7 @@ describe('BulkEditorComponent', () => {
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
remote_ocr: false,
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||
|
||||
@@ -51,6 +51,7 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { flattenTags } from 'src/app/utils/flatten-tags'
|
||||
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
|
||||
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 { 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'
|
||||
@@ -909,7 +910,7 @@ export class BulkEditorComponent
|
||||
}
|
||||
|
||||
reprocessSelected() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
let modal = this.modalService.open(ReprocessConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Reprocess confirm`
|
||||
@@ -923,7 +924,10 @@ export class BulkEditorComponent
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.executeDocumentAction(
|
||||
modal,
|
||||
this.documentService.reprocessDocuments(this.getSelectionQuery())
|
||||
this.documentService.reprocessDocuments(
|
||||
this.getSelectionQuery(),
|
||||
modal.componentInstance.remoteOcr
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</pngx-page-header>
|
||||
<form [formGroup]="savedViewsForm" (ngSubmit)="save()">
|
||||
<ul class="list-group mb-3" formGroupName="savedViews">
|
||||
@for (view of pagedSavedViews(); track view) {
|
||||
@for (view of savedViews(); track view) {
|
||||
<li class="list-group-item py-3">
|
||||
<div [formGroupName]="view.id">
|
||||
<div class="row">
|
||||
@@ -81,11 +81,6 @@
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { signal } from '@angular/core'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { Subject, of, throwError } from 'rxjs'
|
||||
@@ -223,44 +222,6 @@ describe('SavedViewsComponent', () => {
|
||||
).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', () => {
|
||||
const confirmClicked = new Subject<any>()
|
||||
const modalRef = {
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
computed,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbModal, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
|
||||
@@ -49,7 +42,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
AsyncPipe,
|
||||
NgbPaginationModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
@@ -66,14 +58,6 @@ export class SavedViewsComponent
|
||||
DisplayMode = DisplayMode
|
||||
|
||||
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({})
|
||||
public savedViewsForm: FormGroup = new FormGroup({
|
||||
savedViews: this.savedViewsGroup,
|
||||
@@ -100,11 +84,9 @@ export class SavedViewsComponent
|
||||
private reloadViews(): void {
|
||||
this.loading.set(true)
|
||||
this.savedViewService
|
||||
.list(1, 100000, null, false, { full_perms: true })
|
||||
.list(null, null, null, false, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ export const ConfigCategory = {
|
||||
AI: $localize`AI Settings`,
|
||||
}
|
||||
|
||||
export const ConfigSection = {
|
||||
RemoteOCR: $localize`Remote OCR`,
|
||||
}
|
||||
|
||||
export const LLMEmbeddingBackendConfig = {
|
||||
OPENAI_LIKE: 'openai-like',
|
||||
HUGGINGFACE: 'huggingface',
|
||||
@@ -65,6 +69,15 @@ export const LLMBackendConfig = {
|
||||
OLLAMA: 'ollama',
|
||||
}
|
||||
|
||||
export const RemoteOCREngineConfig = {
|
||||
AZURE_AI: 'azureai',
|
||||
}
|
||||
|
||||
export const RemoteOCRModeConfig = {
|
||||
ALWAYS: 'always',
|
||||
WORKFLOW_ONLY: 'workflow_only',
|
||||
}
|
||||
|
||||
export interface ConfigOption {
|
||||
key: string
|
||||
title: string
|
||||
@@ -72,6 +85,7 @@ export interface ConfigOption {
|
||||
choices?: Array<{ id: string; name: string }>
|
||||
config_key?: string
|
||||
category: string
|
||||
section?: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
@@ -181,6 +195,43 @@ export const PaperlessConfigOptions: ConfigOption[] = [
|
||||
config_key: 'PAPERLESS_OCR_USER_ARGS',
|
||||
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',
|
||||
title: $localize`Application Logo`,
|
||||
@@ -398,6 +449,10 @@ export interface PaperlessConfig extends ObjectWithId {
|
||||
barcode_enable_tag: boolean
|
||||
barcode_tag_mapping: object
|
||||
barcode_tag_split: boolean
|
||||
remote_ocr_engine: string
|
||||
remote_ocr_api_key: string
|
||||
remote_ocr_endpoint: string
|
||||
remote_ocr_mode: string
|
||||
ai_enabled: boolean
|
||||
llm_embedding_backend: string
|
||||
llm_embedding_model: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PdfEditorEditMode } from '../components/common/pdf-editor/pdf-editor-edit-mode'
|
||||
import { PdfZoomScale } from '../components/common/pdf-viewer/pdf-viewer.types'
|
||||
import { RemoteOCRModeConfig } from './paperless-config'
|
||||
import { User } from './user'
|
||||
|
||||
export interface UiSettings {
|
||||
@@ -94,6 +95,8 @@ export const SETTINGS_KEYS = {
|
||||
OUTLOOK_OAUTH_URL: 'outlook_oauth_url',
|
||||
EMAIL_ENABLED: 'email_enabled',
|
||||
AI_ENABLED: 'ai_enabled',
|
||||
REMOTE_OCR_CONFIGURED: 'remote_ocr:configured',
|
||||
REMOTE_OCR_MODE: 'remote_ocr:mode',
|
||||
}
|
||||
|
||||
export const SETTINGS: UiSetting[] = [
|
||||
@@ -347,4 +350,14 @@ export const SETTINGS: UiSetting[] = [
|
||||
type: 'string',
|
||||
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,6 +284,21 @@ describe(`DocumentService`, () => {
|
||||
expect(req.request.method).toEqual('POST')
|
||||
expect(req.request.body).toEqual({
|
||||
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,9 +349,13 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
})
|
||||
}
|
||||
|
||||
reprocessDocuments(selection: DocumentSelectionQuery) {
|
||||
reprocessDocuments(
|
||||
selection: DocumentSelectionQuery,
|
||||
remoteOcr: boolean = false
|
||||
) {
|
||||
return this.http.post(this.getResourceUrl(null, 'reprocess'), {
|
||||
...selection,
|
||||
remote_ocr: remoteOcr,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { environment } from 'src/environments/environment'
|
||||
import { CustomFieldDataType } from '../data/custom-field'
|
||||
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
|
||||
import { SavedView } from '../data/saved-view'
|
||||
import { RemoteOCRModeConfig } from '../data/paperless-config'
|
||||
import { SETTINGS_KEYS, UiSettings } from '../data/ui-settings'
|
||||
import { PermissionsService } from './permissions.service'
|
||||
import { CustomFieldsService } from './rest/custom-fields.service'
|
||||
@@ -434,4 +435,26 @@ describe('SettingsService', () => {
|
||||
).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,6 +19,7 @@ import {
|
||||
} from 'src/app/utils/color'
|
||||
import { DEFAULT_APP_TITLE, environment } from 'src/environments/environment'
|
||||
import { DEFAULT_DISPLAY_FIELDS, DisplayField } from '../data/document'
|
||||
import { RemoteOCRModeConfig } from '../data/paperless-config'
|
||||
import { SavedView } from '../data/saved-view'
|
||||
import {
|
||||
PAPERLESS_GREEN_HEX,
|
||||
@@ -687,6 +688,17 @@ export class SettingsService {
|
||||
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 {
|
||||
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
+2024
-2030
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
@@ -394,10 +394,16 @@ def delete(doc_ids: list[int]) -> Literal["OK"]:
|
||||
return "OK"
|
||||
|
||||
|
||||
def reprocess(doc_ids: list[int]) -> Literal["OK"]:
|
||||
def reprocess(doc_ids: list[int], *, remote_ocr: bool = False) -> 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:
|
||||
update_document_content_maybe_archive_file.apply_async(
|
||||
kwargs={"document_id": document_id},
|
||||
kwargs={"document_id": document_id, "remote_ocr": remote_ocr},
|
||||
headers={"trigger_source": PaperlessTask.TriggerSource.MANUAL},
|
||||
)
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ from documents.utils import copy_basic_file_stats
|
||||
from documents.utils import copy_file_with_basic_stats
|
||||
from documents.utils import run_subprocess
|
||||
from paperless.config import OcrConfig
|
||||
from paperless.config import RemoteOCRConfig
|
||||
from paperless.models import ArchiveFileGenerationChoices
|
||||
from paperless.parsers import ParserContext
|
||||
from paperless.parsers import ParserProtocol
|
||||
@@ -451,12 +452,19 @@ class ConsumerPlugin(
|
||||
except Exception as 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
|
||||
parser_class: type[ParserProtocol] | None = (
|
||||
get_parser_registry().get_parser_for_file(
|
||||
mime_type,
|
||||
self.filename,
|
||||
self.working_copy,
|
||||
allow_remote=allow_remote,
|
||||
)
|
||||
)
|
||||
if not parser_class:
|
||||
|
||||
@@ -34,6 +34,7 @@ class DocumentMetadataOverrides:
|
||||
skip_asn_if_exists: bool = False
|
||||
version_label: str | None = None
|
||||
actor_id: int | None = None
|
||||
remote_ocr: bool = False
|
||||
|
||||
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
|
||||
"""
|
||||
@@ -57,6 +58,8 @@ class DocumentMetadataOverrides:
|
||||
self.actor_id = other.actor_id
|
||||
if other.skip_asn_if_exists:
|
||||
self.skip_asn_if_exists = True
|
||||
if other.remote_ocr:
|
||||
self.remote_ocr = True
|
||||
if other.version_label is not None:
|
||||
self.version_label = other.version_label
|
||||
|
||||
|
||||
@@ -1744,7 +1744,7 @@ class DeleteDocumentsSerializer(DocumentSelectionSerializer):
|
||||
|
||||
|
||||
class ReprocessDocumentsSerializer(DocumentSelectionSerializer):
|
||||
pass
|
||||
remote_ocr = serializers.BooleanField(required=False, default=False)
|
||||
|
||||
|
||||
class BulkEditSerializer(
|
||||
@@ -2086,6 +2086,13 @@ class BulkEditSerializer(
|
||||
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):
|
||||
if "password" not in parameters:
|
||||
raise serializers.ValidationError("password not specified")
|
||||
@@ -2150,6 +2157,8 @@ class BulkEditSerializer(
|
||||
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
|
||||
elif method == bulk_edit.remove_password:
|
||||
self.validate_parameters_remove_password(parameters)
|
||||
elif method == bulk_edit.reprocess:
|
||||
self._validate_parameters_reprocess(parameters)
|
||||
|
||||
return attrs
|
||||
|
||||
@@ -3213,13 +3222,6 @@ class WorkflowActionSerializer(serializers.ModelSerializer[WorkflowAction]):
|
||||
{"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 (
|
||||
"type" in attrs
|
||||
and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL
|
||||
|
||||
+10
-1
@@ -66,6 +66,7 @@ from documents.utils import compute_checksum
|
||||
from documents.utils import identity
|
||||
from documents.workflows.utils import get_workflows_for_trigger
|
||||
from paperless.config import AIConfig
|
||||
from paperless.config import RemoteOCRConfig
|
||||
from paperless.logging import consume_task_id
|
||||
from paperless.parsers import ParserContext
|
||||
from paperless.parsers.registry import get_parser_registry
|
||||
@@ -337,10 +338,17 @@ def bulk_update_documents(document_ids) -> None:
|
||||
|
||||
|
||||
@shared_task
|
||||
def update_document_content_maybe_archive_file(document_id) -> None:
|
||||
def update_document_content_maybe_archive_file(
|
||||
document_id,
|
||||
*,
|
||||
remote_ocr: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Re-creates OCR content and thumbnail for a document, and archive file if
|
||||
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)
|
||||
|
||||
@@ -350,6 +358,7 @@ def update_document_content_maybe_archive_file(document_id) -> None:
|
||||
mime_type,
|
||||
document.original_filename or "",
|
||||
document.source_path,
|
||||
allow_remote=remote_ocr or RemoteOCRConfig().remote_ocr_by_default,
|
||||
)
|
||||
|
||||
if not parser_class:
|
||||
|
||||
@@ -72,6 +72,10 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
||||
"barcode_enable_tag": None,
|
||||
"barcode_tag_mapping": 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,
|
||||
"llm_embedding_backend": None,
|
||||
"llm_embedding_model": None,
|
||||
@@ -870,6 +874,49 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
||||
config.refresh_from_db()
|
||||
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:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -532,7 +532,29 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
||||
m.assert_called_once()
|
||||
args, kwargs = m.call_args
|
||||
self.assertEqual(args[0], [self.doc1.id])
|
||||
self.assertEqual(len(kwargs), 0)
|
||||
self.assertEqual(kwargs, {"remote_ocr": False})
|
||||
|
||||
@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")
|
||||
def test_api_set_storage_path(self, m) -> None:
|
||||
@@ -1553,6 +1575,29 @@ 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")
|
||||
def test_edit_pdf(self, m) -> None:
|
||||
self.setup_mock(m, "edit_pdf")
|
||||
|
||||
@@ -60,6 +60,10 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||
},
|
||||
"email_enabled": False,
|
||||
"ai_enabled": False,
|
||||
"remote_ocr": {
|
||||
"configured": False,
|
||||
"mode": "always",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -154,6 +158,50 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
||||
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(
|
||||
OAUTH_CALLBACK_BASE_URL="http://localhost:8000",
|
||||
GMAIL_OAUTH_CLIENT_ID="abc123",
|
||||
|
||||
@@ -422,11 +422,6 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
json.dumps(
|
||||
{
|
||||
"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",
|
||||
@@ -434,10 +429,6 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
action = WorkflowAction.objects.get(id=response.data["id"])
|
||||
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(
|
||||
self.ENDPOINT_TRIGGERS,
|
||||
|
||||
@@ -1782,3 +1782,56 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
||||
|
||||
self.assertIn("wrong password", str(exc.exception))
|
||||
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"])
|
||||
|
||||
@@ -1559,6 +1559,72 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
|
||||
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):
|
||||
def test_update_skip_asn_if_exists(self) -> None:
|
||||
base = DocumentMetadataOverrides()
|
||||
@@ -1566,6 +1632,20 @@ class TestMetadataOverrides(TestCase):
|
||||
base.update(incoming)
|
||||
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:
|
||||
base = DocumentMetadataOverrides(
|
||||
actor_id=1,
|
||||
|
||||
@@ -287,6 +287,45 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
|
||||
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):
|
||||
@override_settings(
|
||||
AI_ENABLED=True,
|
||||
|
||||
@@ -2000,55 +2000,6 @@ class TestWorkflows(
|
||||
r"Doc added in \w{3,}",
|
||||
) # 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:
|
||||
"""
|
||||
GIVEN:
|
||||
|
||||
@@ -236,8 +236,10 @@ from paperless import version
|
||||
from paperless.celery import app as celery_app
|
||||
from paperless.config import AIConfig
|
||||
from paperless.config import GeneralConfig
|
||||
from paperless.config import RemoteOCRConfig
|
||||
from paperless.models import ApplicationConfiguration
|
||||
from paperless.parsers.registry import get_parser_registry
|
||||
from paperless.parsers.remote import RemoteEngineConfig
|
||||
from paperless.serialisers import GroupSerializer
|
||||
from paperless.serialisers import UserSerializer
|
||||
from paperless.views import StandardPagination
|
||||
@@ -2267,7 +2269,7 @@ class ChatStreamingView(GenericAPIView[Any]):
|
||||
if not has_perms_owner_aware(request.user, "view_document", document):
|
||||
return HttpResponseForbidden("Insufficient permissions")
|
||||
|
||||
documents = Document.objects.filter(pk=document.pk)
|
||||
documents = [document]
|
||||
else:
|
||||
documents = Document.objects.filter(
|
||||
id__in=permitted_document_ids(request.user),
|
||||
@@ -4010,6 +4012,11 @@ class UiSettingsView(GenericAPIView[Any]):
|
||||
|
||||
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:
|
||||
manager = PaperlessMailOAuth2Manager()
|
||||
if settings.GMAIL_OAUTH_ENABLED:
|
||||
|
||||
@@ -105,8 +105,7 @@ def apply_assignment_to_document(
|
||||
field=field,
|
||||
document=document,
|
||||
).first()
|
||||
# empty string is indistinguishable from no value in the UI
|
||||
if instance and args[value_field_name] not in (None, ""):
|
||||
if instance and args[value_field_name] is not None:
|
||||
setattr(instance, value_field_name, args[value_field_name])
|
||||
instance.save()
|
||||
elif not instance:
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Afrikaans\n"
|
||||
"Language: af_ZA\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Waarde moet geldige JSON wees."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Ongeldige gepasmaakte veldnavraaguitdrukking"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Ongeldige uitdrukking lys. Moet nie leeg wees nie."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Ongeldige logiese uitdrukking {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr ""
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr ""
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Ongeldige kleur."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Lêertipe %(type)s word nie ondersteun nie"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Ongeldige veranderlike bespeur."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1634,36 +1634,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Amharic\n"
|
||||
"Language: am_ET\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "መዝገባት"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "የሚሰራው እሴት \"JSON\" መሆን አለበት"
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "ልክ ያልሆነ የተወሰነ የቦታ መጠይቅ አገላለጽ"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "ልክ ያልሆነ የመግለጫ ዝርዝር። ባዶ መሆን የለበትም።"
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "ልክ ያልሆነ የሎጂክ ኦፕሬተር {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "ከፍተኛው የጥያቄ ሁኔታዎች/መጠን ብዛት አልፏል።"
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} ይሄ ታዐማኒነት ያለው ልማድ አይደለም።"
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "ጥያቄን አይደግፍም expr {expr!r}."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "ከፍተኛው የጥገኝነት ጥልቀት አልፏል።"
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "ይህ ልማድ አልተገኘም"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr ""
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1634,36 +1634,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Arabic\n"
|
||||
"Language: ar_SA\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "المستندات"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "يجب أن تكون القيمة JSON."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "تعبير استعلام غير صالح للحقول المخصصة"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "قائمة عبارة خاطئة."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "تجاوز الحد الأقصى لعدد شروط الاستعلام."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} حقل مخصص غير صالح."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} لا يدعم تعبير الاستعلام {expr!r}."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "لم يتم العثور على حقل مخصص"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr ""
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "لون خاطئ."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "نوع الملف %(type)s غير مدعوم"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "اكتشاف متغير خاطئ."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1635,36 +1635,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Belarusian\n"
|
||||
"Language: be_BY\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Дакументы"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr ""
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr ""
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr ""
|
||||
msgid "workflow runs"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Няправільны колер."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Тып файла %(type)s не падтрымліваецца"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Выяўлена няправільная зменная."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1634,36 +1634,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"Language: bg_BG\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Стойността трябва да е валидна JSON."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Невалидна заявка на персонализираното полето"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Списък с невалиден израз. Не може да е празно."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Невалиден логически оператор {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Надвишен е максимален брой за заявки."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} не е валидно персонализирано поле."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} не поддържа заявка expr {expr!r}."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Надвишена е максималната дълбочина на вмъкване."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Персонализирано поле не е намерено"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "стартиране на работния процес"
|
||||
msgid "workflow runs"
|
||||
msgstr "стартиране на работните процеси"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Невалиден цвят."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Файловия тип %(type)s не се поддържа"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Засечена е невалидна променлива."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1635,36 +1635,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-12 12:23\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Catalan\n"
|
||||
"Language: ca_ES\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Documents "
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Valor ha de ser un JSON valid."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Expressió de camp de consulta invàlid"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Expressió de llista invàlida. No ha d'estar buida."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Invàlid operand lògic {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Condicions de consulta excedits."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} no és un camp personalitzat vàlid."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} no suporta expressió de consulta {expr!r}."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Màxima profunditat anidada excedida."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Camp personalitzat no trobat"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "data del flux"
|
||||
msgid "workflow runs"
|
||||
msgstr "flux corrents"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr "Permisos insuficients."
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Color Invàlid."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Tipus arxiu %(type)s no suportat"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr "ID de camp personalizat ha de ser enter: %(id)s"
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr "Camp personalitzat amb ID %(id)s no existeix"
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
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."
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr "Alguns camps personalitzats no existeixen o s'han especificat dues vegades."
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Variable detectada invàlida."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr "Duplicat d'identificadors de documents no permès."
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(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}"
|
||||
msgstr "No s'ha pogut analitzar l'URI {value}"
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr "Invalid more_like_id"
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr "Configuració AI invàlida."
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
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."
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr "Permisos insuficients per compartir document %(id)s."
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr "Paquet ja s'està processant."
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
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."
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr "El paquet d'enllaç no està disponible."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Czech\n"
|
||||
"Language: cs_CZ\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenty"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Hodnota musí být platný JSON."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Neplatný výraz dotazu na vlastní pole"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Neplatný seznam výrazů. Nesmí být prázdný."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Neplatný logický operátor {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Překročen maximální počet podmínek dotazu."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} není platné vlastní pole."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} nepodporuje výraz dotazu {expr!r}."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Překročena maximální hloubka větvení."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Vlastní pole nebylo nalezeno"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "spuštění pracovního postupu"
|
||||
msgid "workflow runs"
|
||||
msgstr "spuštění pracovních postupů"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr "Nedostatečná oprávnění."
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Neplatná barva."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Typ souboru %(type)s není podporován"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr "Vlastní ID pole musí být celé číslo: %(id)s"
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr "Vlastní pole s ID %(id)s neexistuje"
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
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."
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr "Některá vlastní pole neexistují nebo byla zadána dvakrát."
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Zjištěna neplatná proměnná."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1636,36 +1636,36 @@ msgstr "URI schéma '{parts.scheme}' není povoleno. Povolená schémata: {',\n"
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr "Nelze zpracovat URI {value}"
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr "Nedostatečná oprávnění ke sdílení dokumentu %(id)s."
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Danish\n"
|
||||
"Language: da_DK\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenter"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Værdien skal være gyldig JSON."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Ugyldigt tilpasset feltforespørgselsudtryk"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Ugyldig udtryksliste. Må ikke være tom."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Ugyldig logisk operatør {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Maksimalt antal forespørgselsbetingelser overskredet."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} er ikke et gyldigt tilpasset felt."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} understøtter ikke forespørgsel expr {expr!r}."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Maksimal indlejringsdybde overskredet."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Tilpasset felt ikke fundet"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "workflow-kørsel"
|
||||
msgid "workflow runs"
|
||||
msgstr "workflow-kørsler"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Ugyldig farve."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Filtype %(type)s understøttes ikke"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Ugyldig variabel fundet."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1635,36 +1635,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:28\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German, Switzerland\n"
|
||||
"Language: de_CH\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Wert muss gültiges JSON sein."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Ungültiger benutzerdefinierter Feldabfrageausdruck"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Ungültiger logischer Operator {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Maximale Anzahl an Abfragebedingungen überschritten."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} ist kein gültiges Zusatzfeld."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Maximale Verschachtelungstiefe überschritten."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Benutzerdefiniertes Feld nicht gefunden"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "Arbeitsablauf-Ausführung"
|
||||
msgid "workflow runs"
|
||||
msgstr "Arbeitsablauf wird ausgeführt"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr "Unzureichende Berechtigungen."
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Ungültige Farbe."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Dateityp %(type)s nicht unterstützt"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr "Feld-ID eines benutzerdefinierten Felds muss eine Ganzzahl sein: %(id)s"
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr "Benutzerdefiniertes Feld mit ID %(id)s existiert nicht"
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
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."
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr "Einige benutzerdefinierte Felder existieren nicht oder wurden zweimal angegeben."
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Ungültige Variable erkannt."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(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}"
|
||||
msgstr "URI {value} kann nicht gelesen werden"
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr "Ungültige more_like_id"
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr "Ungültige KI-Konfiguration."
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
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."
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr "Paket wird bereits verarbeitet."
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
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."
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr "Das Freigabelink-Paket ist nicht verfügbar."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Wert muss gültiges JSON sein."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Ungültiger Zusatzfeld-Abfrageausdruck"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Ungültige Ausdrucksliste. Darf nicht leer sein."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Ungültiger logischer Operator {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Maximale Anzahl an Abfragebedingungen überschritten."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "{name!r} ist kein gültiges Zusatzfeld."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "{data_type} unterstützt den Abfrageausdruck {expr!r} nicht."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Maximale Verschachtelungstiefe überschritten."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Zusatzfeld nicht gefunden"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "Arbeitsablauf-Ausführung"
|
||||
msgid "workflow runs"
|
||||
msgstr "Arbeitsablauf wird ausgeführt"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr "Unzureichende Berechtigungen."
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Ungültige Farbe."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Dateityp %(type)s nicht unterstützt"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr "Zusatzfeld-ID muss eine Ganzzahl sein: %(id)s"
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr "Zusatzfeld mit ID %(id)s existiert nicht"
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
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."
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr "Einige Zusatzfelder existieren nicht oder wurden zweimal angegeben."
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Ungültige Variable erkannt."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr "Doppelte Dokumentbezeichner sind nicht erlaubt."
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(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}"
|
||||
msgstr "URI {value} kann nicht gelesen werden"
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr "Ungültige more_like_id"
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr "Ungültige KI-Konfiguration."
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr "Zeitüberschreitung bei der KI-Backendanfrage."
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
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."
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr "Unzureichende Berechtigungen, um Dokument %(id)s zu teilen."
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr "Paket wird bereits verarbeitet."
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
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."
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr "Das Freigabelink-Paket ist nicht verfügbar."
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: paperless-ngx\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-10 02:25+0000\n"
|
||||
"PO-Revision-Date: 2026-08-10 02:27\n"
|
||||
"POT-Creation-Date: 2026-07-31 16:14+0000\n"
|
||||
"PO-Revision-Date: 2026-07-31 16:15\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Greek\n"
|
||||
"Language: el_GR\n"
|
||||
@@ -21,39 +21,39 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr "Έγγραφα"
|
||||
|
||||
#: documents/filters.py:471
|
||||
#: documents/filters.py:472
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Η τιμή πρέπει να είναι σε έγκυρη μορφή JSON."
|
||||
|
||||
#: documents/filters.py:490
|
||||
#: documents/filters.py:491
|
||||
msgid "Invalid custom field query expression"
|
||||
msgstr "Μη έγκυρη έκφραση προσαρμοσμένου ερωτήματος πεδίου"
|
||||
|
||||
#: documents/filters.py:500
|
||||
#: documents/filters.py:501
|
||||
msgid "Invalid expression list. Must be nonempty."
|
||||
msgstr "Μη έγκυρη λίστα έκφρασης. Πρέπει να είναι μη κενή."
|
||||
|
||||
#: documents/filters.py:521
|
||||
#: documents/filters.py:522
|
||||
msgid "Invalid logical operator {op!r}"
|
||||
msgstr "Μη έγκυρος λογικός τελεστής {op!r}"
|
||||
|
||||
#: documents/filters.py:535
|
||||
#: documents/filters.py:536
|
||||
msgid "Maximum number of query conditions exceeded."
|
||||
msgstr "Υπέρβαση μέγιστου αριθμού συνθηκών ερωτήματος."
|
||||
|
||||
#: documents/filters.py:599
|
||||
#: documents/filters.py:600
|
||||
msgid "{name!r} is not a valid custom field."
|
||||
msgstr "Το προσαρμοσμένο πεδίο {name!r} δεν είναι ένα έγκυρο."
|
||||
|
||||
#: documents/filters.py:636
|
||||
#: documents/filters.py:637
|
||||
msgid "{data_type} does not support query expr {expr!r}."
|
||||
msgstr "Το {data_type} δεν υποστηρίζει το ερώτημα expr {expr!r}s."
|
||||
|
||||
#: documents/filters.py:755 documents/models.py:136
|
||||
#: documents/filters.py:752 documents/models.py:136
|
||||
msgid "Maximum nesting depth exceeded."
|
||||
msgstr "Υπέρβαση μέγιστου βάθους εμφώλευσης."
|
||||
|
||||
#: documents/filters.py:1079
|
||||
#: documents/filters.py:1094
|
||||
msgid "Custom field not found"
|
||||
msgstr "Το προσαρμοσμένο πεδίο δε βρέθηκε"
|
||||
|
||||
@@ -1338,48 +1338,48 @@ msgstr "εκτέλεση ροής εργασίας"
|
||||
msgid "workflow runs"
|
||||
msgstr "εκτελέσεις ροής εργασίας"
|
||||
|
||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
||||
#: documents/serialisers.py:2767 documents/views.py:299 documents/views.py:2555
|
||||
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||
#: documents/serialisers.py:2776 documents/views.py:299 documents/views.py:2558
|
||||
#: paperless_mail/serialisers.py:155
|
||||
msgid "Insufficient permissions."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:709
|
||||
#: documents/serialisers.py:710
|
||||
msgid "Invalid color."
|
||||
msgstr "Άκυρο χρώμα."
|
||||
|
||||
#: documents/serialisers.py:2244
|
||||
#: documents/serialisers.py:2248
|
||||
#, python-format
|
||||
msgid "File type %(type)s not supported"
|
||||
msgstr "Ο τύπος αρχείου %(type)s δεν υποστηρίζεται"
|
||||
|
||||
#: documents/serialisers.py:2288
|
||||
#: documents/serialisers.py:2292
|
||||
#, python-format
|
||||
msgid "Custom field id must be an integer: %(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2295
|
||||
#: documents/serialisers.py:2299
|
||||
#, python-format
|
||||
msgid "Custom field with id %(id)s does not exist"
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2312 documents/serialisers.py:2322
|
||||
#: documents/serialisers.py:2316 documents/serialisers.py:2326
|
||||
msgid "Custom fields must be a list of integers or an object mapping ids to values."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2317
|
||||
#: documents/serialisers.py:2321
|
||||
msgid "Some custom fields don't exist or were specified twice."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2464
|
||||
#: documents/serialisers.py:2468
|
||||
msgid "Invalid variable detected."
|
||||
msgstr "Εντοπίστηκε μη έγκυρη μεταβλητή."
|
||||
|
||||
#: documents/serialisers.py:2823
|
||||
#: documents/serialisers.py:2832
|
||||
msgid "Duplicate document identifiers are not allowed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/serialisers.py:2853 documents/views.py:4509
|
||||
#: documents/serialisers.py:2862 documents/views.py:4517
|
||||
#, python-format
|
||||
msgid "Documents not found: %(ids)s"
|
||||
msgstr ""
|
||||
@@ -1635,36 +1635,36 @@ msgstr ""
|
||||
msgid "Unable to parse URI {value}"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:292 documents/views.py:2552
|
||||
#: documents/views.py:292 documents/views.py:2555
|
||||
msgid "Invalid more_like_id"
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1566
|
||||
#: documents/views.py:1567
|
||||
msgid "Invalid AI configuration."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:1575
|
||||
#: documents/views.py:1576
|
||||
msgid "AI backend request timed out."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:2377 documents/views.py:2698
|
||||
#: documents/views.py:2380 documents/views.py:2701
|
||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4522
|
||||
#: documents/views.py:4529
|
||||
#, python-format
|
||||
msgid "Insufficient permissions to share document %(id)s."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4568
|
||||
#: documents/views.py:4575
|
||||
msgid "Bundle is already being processed."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4629
|
||||
#: documents/views.py:4636
|
||||
msgid "The share link bundle is still being prepared. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: documents/views.py:4639
|
||||
#: documents/views.py:4646
|
||||
msgid "The share link bundle is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user