Compare commits

..

41 Commits

Author SHA1 Message Date
panda 7e3a90a2b8 chore: update rag submodule + add run_e2e.py 2026-05-26 09:29:51 +08:00
panda 00f718fbda fix: prompt 添加字段声明和 font 标签格式的强制规则 2026-05-26 08:23:39 +08:00
panda 6e6199bd26 fix: namespace prefix regex for textField/field counting in fidelity check 2026-05-25 23:56:31 +08:00
panda cacff6f63a fix: ExtractionResult.to_dict() 序列化 all_elements 完整文本元素层 2026-05-25 22:25:23 +08:00
panda 963c5e41c8 fix: nodes.py 调用 detect_annotations 前将 bbox 从 [x_min,y_min,x_max,y_max] 转为 {x,y,w,h}
annotation_detector._correlate_with_ocr 期望 bbox 格式为 {x,y,w,h},
但 OcrTextElement.to_dict() 返回 [x_min,y_min,x_max,y_max]。
Bug3 的根因在 nodes.py 而非 layout_analyzer。
2026-05-25 22:24:29 +08:00
panda c9344a2715 fix: elements -> all_elements 提取完整原始文本元素层 2026-05-25 22:03:53 +08:00
panda 6d5cfaf29a docs: add jaspersoft-fix evaluation report 2026-05-25 12:09:49 +08:00
panda 0839ba92da WIP: uncommitted rag changes and test image 2026-05-25 12:07:28 +08:00
panda 0adae3e06d fix: strip ns0: namespace prefix in _extract_jrxml() 2026-05-25 00:11:43 +08:00
panda 573ce012e7 docs: update all project documentation to reflect current codebase
- README.md: complete overhaul — project structure, architecture, env vars, test listing, Java/JDBC pipeline
- CLAUDE.md: fix field count (~28→~40), migrate Streamlit references to Vue 3, update known issues
- frontend/README.md: add missing StreamingMessage.vue and NodeProgress.vue components
2026-05-24 22:46:48 +08:00
panda 520c8b19d0 fix: 五轮修正失败根因修复 - 评分公式去掉field_coverage权重, namespace无条件检查, OCR自动发现文档类型 2026-05-24 22:44:37 +08:00
panda f25a93b539 WIP: baseline on fix/retry-failure-root-causes 2026-05-24 22:38:30 +08:00
panda 2d5183d2bd fix: OCR fidelity scoring reform — prevent false fail from language-mismatched field names
Root cause (from review): field_coverage compared English JRXML field names
against Chinese OCR field names with set intersection — always zero. Combined
with 0.5 weight in score formula, caused valid JRXML (XSD pass, 82% element
coverage) to score 0.41 < 0.5 → fail → correction loop → progressive destruction.

Changes:
- Scoring weight: element_coverage 0.8 + field_coverage 0.2 (was 0.5+0.5)
- Validate node: only fail on fidelity when BOTH score<0.5 AND element_coverage<0.4
- Field name regex: \w+ → [^"]+ to support non-ASCII field names
- Field matching: also try _sanitize_field_name conversion (Chinese→_uXXXX_)
- correction.md: namespace check always active, not conditional on error keywords
2026-05-24 15:36:40 +08:00
panda 4e14334030 fix: per-node max_tokens + validation 502 guard + correct_jrxml output validity
- backend/llm.py: per-node max_tokens via get_llm(max_tokens=N), LLM_MAX_TOKENS env var (default 8192)
- agent/nodes.py: 5 generation nodes use max_tokens=32768, generate_skeleton retries at 65536
- agent/nodes.py: fix ns:field regex (<field → <[\w:]*field) to handle namespace prefixes
- agent/nodes.py: fix correct_jrxml never writing back to state["current_jrxml"]
- agent/nodes.py: correct_jrxml rejects non-JRXML output (no <jasperReport tag)
- agent/nodes.py: _strip_continuation_wrapper strips markdown/prefixes from continuation rounds
- agent/nodes.py: _extract_jrxml iterates multiple markdown code blocks, skips fragments
- agent/graph.py: route_after_validate skips correction loop when service_unavailable
- agent/graph.py: route_after_save skips validation for empty JRXML
- backend/validation.py: returns service_unavailable: True for ConnectError and HTTP 5xx
- Docs: CLAUDE.md v14 changelog, README.md LLM_MAX_TOKENS, .env.example LLM_MAX_TOKENS
2026-05-24 15:20:25 +08:00
panda e362f530ea chore: remove 13 stale files and clean up project structure
Removed:
- app.py (deprecated Streamlit UI, replaced by api_server.py + frontend/)
- start_agent_jrxml.py (old launcher, replaced by start.py)
- test_reorder.py, e2e_test.py (ad-hoc/outdated test scripts)
- ocr_raw_positions.json (debug output)
- ARCHITECTURE.md, CODE_GUIDE.md, RAG_INTEGRATION.md, ROADMAP.md (superseded by CLAUDE.md)
- EVALUATION_REPORT.md (auto-generated)
- scripts/init_kb.py (replaced by init_default_kb.py)
- validation_service/validate.bat (redundant, start.py covers it)
- sessions/*.json (34 test session files, already gitignored)

Updated:
- CLAUDE.md: removed stale file entries from key mapping table
- README.md: updated init script reference and removed validate.bat
- .gitignore: removed EVALUATION_REPORT.md entry
2026-05-24 09:07:15 +08:00
panda bd5bfbac2d fix: band-level windowed refine_layout + programmatic map_fields to prevent 91.5% content loss
Root cause: LLM receiving full 34k-char JRXML would regenerate from scratch
instead of modifying coordinates in-place, shrinking output to ~3k chars.

Solution (programmatic node control, not prompt engineering):

- New agent/jrxml_windower.py: decompose JRXML into header (never sent to
  LLM) + individual bands. Split bands >4000 chars at element boundaries.
  Reassemble with element count validation (>10% change = rollback).

- Rewrite refine_layout: per-band windowed LLM processing (~2-4k chars
  each). LLM cannot "reimagine" the entire report.

- Rewrite map_fields: 100% programmatic regex $F{field_N} -> real name
  replacement. Zero LLM calls, zero content loss.

- _sanitize_field_name: non-ASCII chars escaped to _uXXXX_ format for
  valid JRXML identifiers.

- Tests: 48 new unit tests (windower 28 + map_fields 20). All passing.
  Full suite 385 tests, zero regressions.
2026-05-24 08:55:38 +08:00
panda bb6cc6e241 feat: add Java JRXML-to-PNG rendering pipeline with pixel-level SSIM comparison
- lib/java/: Java renderer (JrxmlRenderer) using JasperReports 6.21.0
  - JrxmlDebug for diagnostics, JrxmlGen for format reference
  - download_jars.sh for one-time dependency setup
- agent/nodes.py: _render_jrxml_to_png() and _compute_pixel_similarity()
  - Pixel comparison integrates into validate node (SSIM < 0.4 fails)
  - Pixel fidelity context injected into correct_jrxml for targeted fixes
- tests/test_pixel_comparison.py: 15 unit tests (render, SSIM, integration)
- .gitignore: exclude lib/java/*.jar, lib/java/*.class, tmp/
- CLAUDE.md: v11 changelog documenting the rendering pipeline
- All non-LLM tests pass (97/97)
2026-05-23 15:09:55 +08:00
panda 9de75d2f25 fix: escape $F{field_N} in correction.md to prevent Python format KeyError
$F{field_N} was being parsed by str.format() as a replacement field,
causing KeyError and crashing correct_jrxml node.
Changed to $F{{field_N}} (double braces -> literal brace in output).
2026-05-23 11:27:31 +08:00
panda 0af774ae9d fix: failure recovery forces modify_report intent bypassing LLM classify
- process_input sets _failure_recovery flag when injecting pending_failure_context
- classify_intent skips LLM classification when flag is set, directly routes to modify_jrxml
- Smart truncation for intent classify: keep head 200 + tail 300 chars instead of head 500
  (prevents user's actual message from being truncated away by long injected context)
- This fixes the bug where "retry" or pasted error messages were misclassified as
  consult_question or initial_generation after max retry exhaustion
2026-05-23 11:18:02 +08:00
panda 23cdfa8c2b fix: map_fields empty-retry + correction prompt field_N guidance
- map_fields: retry with simplified prompt on empty LLM response
- correction.md: add explicit guidance for undeclared field_N errors
  (add <field> declarations + try OCR name replacement)
- MAX_RETRY=5 now effective (was overridden by .env:3)
2026-05-23 11:15:09 +08:00
panda 1210b926c3 fix: MAX_RETRY 5 + rolling continuation + namespace-aware JRXML extraction
- MAX_RETRY: 3→5 (graph.py:35, nodes.py:25) with env override
- Rolling continuation: _generate_with_continuation() auto-detects
  truncated JRXML and sends anchor-based continuation, max 3 rounds
- JRXML extraction: regex/end-tag now namespace-prefix aware
  (ns0:jasperReport, ns:jasperReport, etc.)
- All 5 generation nodes refactored to use continuation helper
- Tests updated: scenario1 accepts ns-prefixed root, max_retry
  verifies graph termination
- stop_reason capture + WARNING log on max_tokens truncation
- Correction prompt now injects OCR context + layout schema
2026-05-23 10:58:46 +08:00
panda 83e801a0b8 fix: auto-inject JasperReports namespace before XSD validation
AI-generated JRXML often omits the xmlns declaration on the root element.
The XSD schema requires targetNamespace, so validation would fail with
"Element 'jasperReport': No matching global declaration available".

_ensure_jr_namespace() detects missing xmlns and injects it before
schema validation, making the validator tolerant of namespace-free JRXML.
2026-05-23 09:44:08 +08:00
panda c2cae5665e fix: replace complex bat scripts with Python launcher + minimal bat wrappers
Root cause: Windows batch files written with LF endings caused cmd.exe to
misparse labels and Chinese characters, producing garbled "not a command"
errors. The Python launcher avoids encoding issues entirely.

- start.py: reliable cross-platform launcher (kill ports, start 3 services,
  wait for health, print status)
- start.bat / start_all.bat: minimal 4-line ASCII wrappers
- stop.bat: inline Python for port-based process killing
2026-05-23 09:32:32 +08:00
panda c8924c625c fix: rewrite startup scripts with reliable helpers, stderr logging, visible windows
- Replace /MIN (hidden window) with normal windows so errors are visible
- Redirect stderr to logs/*.log for post-mortem
- Extract killport/wait_health/wait_port into callable helpers
- Use !N! (delayed expansion) for retry counters
- stop.bat now shows which PIDs it kills with port labels
- Remove nested-quote issue by cd'ing before npm start
2026-05-23 09:25:45 +08:00
panda 9a4f51d378 fix: add retry limit to startup wait loops to prevent infinite hang
Each service wait loop now fails after 30 retries (~60s) instead of
spinning forever when a port is occupied by a stuck process.
Also added cleanup label that kills partially-started services on failure.
2026-05-23 09:20:55 +08:00
panda 40adf50702 fix: add chcp 65001 and .venv check to startup scripts 2026-05-23 09:15:44 +08:00
panda 751df5c4a9 fix: resolve quoting issue in start_all.bat frontend launch, add node_modules check 2026-05-23 09:11:53 +08:00
panda 93ad5e8876 fix: address audit findings — session_id validation, streaming reset, state isolation
- Replace truncated 12-char UUID with full 32-char UUID (128-bit entropy)
- Add validate_session_id() regex check to prevent path traversal
- Add _check_session_id() guard on all 6 API endpoints
- Change _step_counter from module global to contextvars.ContextVar
- Filter None values from node_state before merging into agent_state
- Log save_session failures instead of silently swallowing them
- Add finishStreaming() in catch/finally blocks to prevent UI lockup
- Fix broken multiline docstring in chat() endpoint
2026-05-23 09:08:53 +08:00
panda 1952d75f13 test: add unit/integration/E2E test suites, fix create_session bug, update docs
- Unit tests: test_session.py (27), test_error_kb.py (24), test_agent.py hardened
- Integration tests: test_api_integration.py (25) with FastAPI TestClient
- E2E tests: main-flows.spec.ts (8) with Playwright + API mocking
- Bug fix: backend/session.py create_session() missing session_id parameter
- Config: frontend/playwright.config.ts, npm run test:e2e
- Docs: update CLAUDE.md v9, .gitignore for test artifacts/eval reports
2026-05-23 08:38:29 +08:00
panda b444303055 docs: CLAUDE.md v8 — prompt escape fix + installed plugins/skills reference 2026-05-22 23:01:59 +08:00
panda 1e5ce9725b feat: FastAPI+SSE API server, JRXML auto-reorder, session integrity fixes 2026-05-22 17:53:59 +08:00
panda 1144a86d02 fix: session persistence, multi-turn memory, OCR pipeline, download UX (v7)
- graph.stream() state fix: agent_state now properly accumulates node updates

- atomic session save (tempfile + os.replace)

- uploaded_file_path injection for OcrExtractor + annotation_detector

- download section always visible; refreshFromApi auto-reloads after generation

- node_start/complete unfiltered for full progress visibility

- modification_request without status=='pass' check
2026-05-22 11:13:25 +08:00
panda 4dfc418fc5 fix: escape {field_N} braces in prompt templates to prevent .format() KeyError
$F{field_1} literal text in skeleton_generation/refine_layout/field_mapping
prompts was being parsed as Python .format() placeholder, causing KeyError
on every image-based initial_generation request. Escaped with double braces
so .format() outputs literal {field_1} for the LLM.
2026-05-22 08:12:56 +08:00
panda 339d415322 fix: crash 'list' object has no attribute 'keys' on image upload, output disappearing on error
Root cause: layout_schema.regions is a list of region dicts, not a dict.
_log_ocr_layers() was calling .keys() on it, causing agent_error.

Also fixed: ProcessSection now stays visible after streaming ends (error or
completion), so generated content is not lost. Header shows ✓/✕/pulse indicators.
Error handler now refreshes session state for partial JRXML download.
2026-05-22 00:01:54 +08:00
panda d600cbf285 feat: add quick action buttons (preview/undo/reset) to sidebar
Sidebar now has 快捷操作 section matching Streamlit app functionality:
- 预览 — sends "预览报表" to preview current JRXML
- 撤销 — sends "撤销上一步修改" to revert last change
- 重置 — sends "重新来,清空当前报表" to reset session

Session store now tracks history_states for undo availability check.
2026-05-21 23:54:57 +08:00
panda a364e1de81 feat: 5-issue fix — OCR image parse bug + Vue frontend feature parity + streaming UX
Fix 1 (CRITICAL): file_parser.py suffix normalization ".jpg", api_server.py Path.suffix
Fix 2: Sidebar version history download, ProcessSection replaces old components
Fix 3: OCR content/position layer structured logging in agent/nodes.py
Fix 4: collapsible process sections with per-section stream routing + auto-fold
Fix 5: agent_complete total_duration_ms, SummaryCard duration display

- backend/file_parser.py: normalize suffix to always include leading dot
- api_server.py: step_index in node_start, total_duration_ms in agent_complete
- agent/nodes.py: _log_ocr_layers() for [内容层]/[位置层]/[合并] logging
- frontend: ProcessSection.vue (NEW), chat.ts sections model, Sidebar versions
- CLAUDE.md: updated component list and v6 changelog
2026-05-21 23:43:21 +08:00
panda 60e2f520ba fix: image files silently falling to text parser due to suffix dot mismatch
api_server.py passed "jpg" (no dot) from rsplit, but file_parser.py
parser dict keys all have dots (".jpg"), causing image files to fall
through to _parse_text() which fails on binary data, skipping ALL OCR
and layout analysis. Every image upload was affected.

- file_parser.py: normalize file_type to always have leading dot
- api_server.py: use Path.suffix instead of manual rsplit
2026-05-21 23:05:27 +08:00
panda 83c7da7517 fix: system env vars silently overriding .env — load_dotenv(override=True)
Root cause: load_dotenv() default override=False meant system-level
ANTHROPIC_BASE_URL (https://api.deepseek.com/anthropic) took precedence
over .env's OPENAI_BASE_URL (https://api.minimaxi.com/anthropic). All
Anthropic API calls went to DeepSeek with a MiniMax key, causing 401.

Changes:
- backend/llm.py: load_dotenv(override=True) — .env always wins
- .env.example: add explicit ANTHROPIC_API_KEY + ANTHROPIC_BASE_URL
- CLAUDE.md: document env var priority pitfall
2026-05-21 22:36:43 +08:00
panda aa1d8a6c52 fix: logging KeyError with reserved 'filename' key, pytest return-not-none warnings
- api_server.py: rename 'filename' to 'file_name' in upload_file log extra
  dict to avoid collision with Python logging's reserved LogRecord attribute
- test_e2e_ocr.py: replace return statements with assert in test functions
  to fix PytestReturnNotNoneWarning
2026-05-21 22:28:07 +08:00
panda 960312b088 fix: start.bat nested quote parsing with path containing spaces
cmd /k "cd /d "%~dp0" && ..." breaks because inner quotes around
%~dp0 close the outer quoted string prematurely when the path
contains spaces (D:\Idea Project\...). Fix: remove outer quotes,
escape && as ^&^& so it passes through to the new cmd instance.
2026-05-21 22:14:17 +08:00
panda 7c1aa7d934 docs: update architecture docs for Vue 3 + FastAPI separation, add one-click start.bat
- CLAUDE.md: remove duplicate architecture section, fix MAX_RETRY 5→3
- README.md: update architecture diagram to 3-tier, add start.bat instructions
- ROADMAP.md: add 阶段六 layered generation v5 (items 16-20)
- start.bat: one-click startup with auto port-kill and path-with-spaces fix
- package-lock.json: updated from npm install
2026-05-21 22:10:22 +08:00
136 changed files with 52339 additions and 2857 deletions
+46
View File
@@ -0,0 +1,46 @@
{
"permissions": {
"allow": [
"Bash(git submodule *)",
"Bash(python -c \"import py_compile; py_compile.compile\\('scripts/init_kb.py', doraise=True\\); print\\('init_kb.py OK'\\)\")",
"Bash(python -c \"import py_compile; py_compile.compile\\('agent/nodes.py', doraise=True\\); print\\('nodes.py OK'\\)\")",
"Bash(python -c \"import py_compile; py_compile.compile\\('backend/embeddings.py', doraise=True\\); print\\('embeddings.py OK'\\)\")",
"Bash(python *)",
"Bash(PYTHONIOENCODING=utf-8 python batch_chunker.py jrxml_source)",
"Bash(taskkill /F /IM python.exe)",
"Bash(pkill -f embed_chunks)",
"Bash(pip show *)",
"Bash(streamlit run *)",
"Bash(curl -s http://localhost:8001/validate -X POST -H \"Content-Type: application/json\" -d '{\"jrxml\":\"\"}')",
"Bash(STREAMLIT_SERVER_HEADLESS=true streamlit run app.py --server.port 8501)",
"Bash(git add *)",
"Bash(git commit -m ' *)",
"Bash(pip install *)",
"Bash(git push *)",
"Bash(claude mcp *)",
"mcp__zai-mcp-server__extract_text_from_screenshot",
"mcp__MiniMax__understand_image",
"Bash(curl -s http://localhost:8001/health)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8501)",
"Bash(curl -s -X POST http://localhost:8001/validate -H \"Content-Type: application/json\" -d \"{\\\\\"jrxml\\\\\": \\\\\"\\\\\"}\")",
"Bash(curl -s -X POST http://localhost:8001/validate -H \"Content-Type: application/json\" -d \"{\\\\\"jrxml\\\\\": \\\\\"<?xml version=\\\\\\\\\\\\\"1.0\\\\\\\\\\\\\"?><jasperReport name=\\\\\\\\\\\\\"test\\\\\\\\\\\\\" pageWidth=\\\\\\\\\\\\\"595\\\\\\\\\\\\\" pageHeight=\\\\\\\\\\\\\"842\\\\\\\\\\\\\"><queryString><![CDATA[SELECT 1]]></queryString></jasperReport>\\\\\"}\")",
"Bash(curl -s -X POST http://localhost:8001/validate -H \"Content-Type: application/json\" -d \"{\\\\\"jrxml\\\\\": \\\\\"<?xml version=\\\\\\\\\\\\\"1.0\\\\\\\\\\\\\"?><jasperReport name=\\\\\\\\\\\\\"test\\\\\\\\\\\\\" pageWidth=\\\\\\\\\\\\\"595\\\\\\\\\\\\\" pageHeight=\\\\\\\\\\\\\"842\\\\\\\\\\\\\"><queryString><![CDATA[SELECT 1]]></queryString><detail><band height=\\\\\\\\\\\\\"50\\\\\\\\\\\\\"/></detail></jasperReport>\\\\\"}\")",
"Bash(curl -s -X POST http://localhost:8001/validate -H 'Content-Type: application/json' -d '{\"jrxml\": \"<?xml version=\\\\\"1.0\\\\\"?><jasperReport name=\\\\\"test\\\\\" pageWidth=\\\\\"595\\\\\" pageHeight=\\\\\"842\\\\\"><queryString><![CDATA[SELECT name FROM users]]></queryString><field name=\\\\\"name\\\\\" class=\\\\\"java.lang.String\\\\\"/><detail><band height=\\\\\"50\\\\\"><textField><reportElement x=\\\\\"0\\\\\" y=\\\\\"0\\\\\" width=\\\\\"100\\\\\" height=\\\\\"20\\\\\"/><textFieldExpression><![CDATA[$F{name}]]></textFieldExpression></textField></band></detail></jasperReport>\"}')",
"Bash(curl -s -o /dev/null -w \"Streamlit: %{http_code}\\\\n\" http://localhost:8501)",
"Bash(grep -v \"Complete$\")",
"Bash(git pull *)",
"Bash(pip search *)",
"Read",
"Write",
"Edit",
"Bash",
"Git",
"Npm",
"Pip",
"Grep",
"Glob",
"Bash(rm -rf components/* assets/* style.css)",
"Bash(mkdir -p api stores components utils)"
]
}
}
+12 -4
View File
@@ -2,12 +2,20 @@
LLM_BACKEND=cloud
# 云端提供商:openai 或 anthropic
LLM_PROVIDER=openai
LLM_PROVIDER=anthropic
# 云端配置(OpenAI 兼容
# Anthropic 兼容 APIMiniMax 等,优先使用
ANTHROPIC_API_KEY=sk-xxxx
ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic
# OpenAI 兼容 APIfallback,当 ANTHROPIC_* 未设置时使用)
OPENAI_API_KEY=sk-xxxx
OPENAI_BASE_URL=https://api.openai.com/v1
LLM_MODEL=gpt-4o
LLM_MODEL=MiniMax-M2.7
# 默认 max_tokens(各生成节点可覆盖为更高值)
LLM_MAX_TOKENS=8192
# 本地大语言模型(Ollama
LOCAL_LLM_MODEL=qwen2.5-coder:7b
@@ -43,7 +51,7 @@ RAG_USE_FP16=true
RAG_BATCH_SIZE=64
# 最大自动修正尝试次数
MAX_RETRY=3
MAX_RETRY=5
# 上下文压缩阈值(token 数)
CONTEXT_MAX_TOKENS=6000
+19
View File
@@ -13,6 +13,25 @@ db/chroma/
sessions/
logs/
db/
# 自动评测 (Mavis AI)
.mavis/
# 上传文件
uploads/
# Java JARs & compiled classes
lib/java/*.jar
lib/java/*.class
# 渲染临时文件
tmp/
# OCR 临时输出
ocr_raw_positions.json
# Playwright E2E 测试产物
frontend/test-results/
# RAG 管线中间产物 (rag 子模块内)
rag/jrxml_chunker_output/
rag/embeddings/
+372 -44
View File
@@ -6,19 +6,6 @@
**一句话**:用户用中文描述报表需求 → LLM 生成 JRXML → 自动验证 → 失败则自动修正(最多5次) → 重试耗尽后失败上下文自动注入下一轮 → 返回可编译的 JRXML 文件。
## 架构
```
前端 (Vue 3 + Vite, 端口 5173)
│ 聊天界面 + 统一输入框 + 流式显示 + 文件上传/粘贴/拖拽
▼ HTTP + SSE (Server-Sent Events)
后端 API (FastAPI, 端口 8000)
│ REST 接口 + SSE 流式推送
│ 包装 LangGraph Agent 不变
▼ HTTP
验证服务 (FastAPI, 端口 8001) — 不变
```
## 启动命令
**方式 1 — 一键启动(Windows)**:双击 `start.bat`,自动打开三个窗口分别运行验证服务、后端 API、前端开发服务器。停止用 `stop.bat`
@@ -57,13 +44,15 @@ cd frontend && npm run dev
│ ├── api/client.ts SSE 客户端 + fetch 封装
│ ├── stores/chat.ts Pinia: 消息/流式/节点进度
│ ├── stores/session.ts Pinia: 会话管理
│ ├── stores/kb.ts Pinia: KB 状态管理(多租户知识库)
│ ├── components/
│ │ ├── Sidebar.vue 会话列表 + 下载
│ │ ├── Sidebar.vue 会话列表 + 下载 + 历史版本
│ │ ├── ChatMessages.vue 消息列表渲染
│ │ ├── StreamingMessage.vue 流式文本展示
│ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴)
│ │ ├── NodeProgress.vue 节点进度指示
│ │ ── SummaryCard.vue 结果摘要卡片
│ │ ├── ProcessSection.vue 过程折叠区(替代 StreamingMessage + NodeProgress
│ │ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴/芯片,含 .jrxml
│ │ ├── SummaryCard.vue 结果摘要卡片(含耗时)
│ │ ── KbSelector.vue KB 下拉选择器(对话中切换知识库)
│ │ └── KbManager.vue KB 管理面板(创建/上传/构建/删除)
│ └── utils/format.ts 工具函数
▼ HTTP + SSE (Server-Sent Events)
@@ -86,7 +75,7 @@ validation_service/ (FastAPI, 端口 8001) — 不变
|------|------|---------|
| `api_server.py` | FastAPI SSE 后端,REST API + 流式推送 | **高** |
| `frontend/src/` | Vue 3 聊天 UI(替代旧 app.py | **高** |
| `agent/state.py` | AgentState 类型定义(~28 字段) | 低 |
| `agent/state.py` | AgentState 类型定义(~40 字段) | 低 |
| `agent/nodes.py` | 18 个工作流节点 + 流式生成 + 错误记录 | **高** |
| `agent/graph.py` | 状态图编译 + 路由函数 + node_start 回调 | 中 |
| `prompts/loader.py` | Prompt 加载器(从 .md 文件热重载) | 低 |
@@ -101,10 +90,15 @@ validation_service/ (FastAPI, 端口 8001) — 不变
| `backend/annotation_detector.py` | 批注检测: 圈选(cv2 HoughCircles) + 箭头(HoughLinesP聚类) + OCR关联 + LLM格式化 | 中 |
| `backend/embeddings.py` | 嵌入模型工厂 (HuggingFace/OpenAI) | 低 |
| `backend/validation.py` | 验证服务 HTTP 客户端 | 低 |
| `backend/session.py` | 会话 JSON 文件 CRUD | 低 |
| `backend/session.py` | 会话 JSON 文件 CRUD(含 kb_id | 低 |
| `backend/kb_manager.py` | 用户+知识库 CRUD(多租户,原子 JSON 持久化) | 中 |
| `backend/kb_searcher.py` | 知识库隔离搜索 + 模板检索(per-KB ChromaDB | 中 |
| `backend/kb_parser.py` | KB 解析管道:文件解析→字段提取→chunk切割→向量嵌入 | 中 |
| `backend/field_matcher.py` | OCR↔KB 字段匹配:Embedding 粗筛 + LLM 精确确认 | 中 |
| `agent/datasource.py` | 数据源模式解析:$P{{xxx}} 参数 vs JDBC 直连 | 低 |
| `agent/jrxml_windower.py` | JRXML Band 级窗口化引擎:拆解/切分/重组/元素计数校验 | 中 |
| `validation_service/main.py` | FastAPI 验证服务 | 低 |
| `scripts/init_kb.py` | 知识库初始化/模型下载 | 低 |
| `app.py` | ~~旧 Streamlit UI~~(已由 api_server.py + frontend/ 替代) | 废弃 |
| `scripts/init_default_kb.py` | 多租户默认 KB 初始化(默认用户 + 预置 KB) | 低 |
## 关键约定
@@ -151,20 +145,17 @@ validation_service/ (FastAPI, 端口 8001) — 不变
- `retrieve` 节点自动注入历史修正案例
- 流程:correct_jrxml 保存 last_error_case → validate 通过时自动入库
### 文件上传
- **对话区域上传v3**: `st.file_uploader` 位于聊天输入框上方,支持图片/PDF/DOCX/XLSX/文本
- **粘贴/拖拽(v3**: 全局 paste/drop 事件监听 + `sessionStorage` + 轮询桥接组件,Ctrl+V 粘贴或拖拽文件到页面任意位置
- **文件预览芯片(v3)**: 上传后显示在对话区域,可逐文件移除(自动清理临时文件)
### 文件上传(已迁移至 Vue 3 前端)
- **对话区域上传**: `UnifiedInput.vue` 统一输入框支持文本 + 文件拖拽/粘贴/选择按钮,支持图片/PDF/DOCX/XLSX/文本/`.jrxml`
- **文件预览芯片**: 上传后显示在对话区域,可逐文件移除(自动清理临时文件)
- 侧边栏多文件上传(可逐文件移除,向后兼容保留)
- 支持: PDF(pdfplumber+PIL) / DOCX(python-docx) / XLSX(openpyxl, v3) / 图片(PIL+EasyOCR优先→PaddleOCR回退) / 纯文本
- 支持: PDF(pdfplumber+PIL) / DOCX(python-docx) / XLSX(openpyxl) / 图片(PIL+EasyOCR优先→PaddleOCR回退) / 纯文本
- 上传文本自动注入下一条消息前缀
- 根据 `can_use_vision()` 判断是否走原生多模态(当前 MiniMax 不支持)
### 对话区域文件粘贴/拖拽技术方案(v3
- `st.html()` 注入全局 paste/drop/dragover 监听器 → 文件转 base64 → 写入 `sessionStorage`
- `components.html(height=0)` 桥接组件每 800ms 轮询 `sessionStorage``Streamlit.setComponentValue` 回传 Python
- Python 解码 base64 → 临时文件 → `parse_file` + `analyze_layout` 双层 OCR 解析
- 上限:单文件 20MB,单次最多 10 个文件
### 对话区域文件粘贴/拖拽 (v3, 已迁移至 Vue 3)
原 Streamlit 方案(`st.html()` 注入 + `sessionStorage` 桥接)已废弃。当前由 `UnifiedInput.vue` 原生处理 paste/drop/dragover 事件,通过 `stores/chat.ts` 上传文件到 `/api/upload``_process_files()``api_server.py` 中统一处理。
### A4 模板识别
- `backend/layout_analyzer.py` — 三种处理路径:
@@ -181,9 +172,6 @@ validation_service/ (FastAPI, 端口 8001) — 不变
### 预览修复
- `route_after_save` 新增意图判断:预览/导出跳过验证直通 finalize
### Ctrl+C 修复
- JS 注入拦截 Streamlit 裸 `c` 键清缓存,保留 Ctrl+C 复制
### 结构化日志系统
- `backend/logger.py` — JSON 格式化 + trace_id + 国际时区
- `_LLMLoggingWrapper` — 包装所有 LLM 后端,记录完整 prompt/response
@@ -200,12 +188,11 @@ validation_service/ (FastAPI, 端口 8001) — 不变
- `process_input` 节点在上传图片时自动触发 OCR 字段提取
- 结果持久化到会话文件(`save_session_node` / `load_session_node`
### 多模态聊天输入 + 多格式文件 (v4)
- `app.py``st.chat_input` 替换为 `st_multimodal_chatinput`(支持 Ctrl+V 粘贴 + 拖拽 + 文件按钮)
- `_process_uploaded_file()` — 提取共享文件处理逻辑(侧边栏 + 聊天共用,消除 ~70 行重复代码)
- 新增文件格式支持: XLSX (openpyxl)、XLS (xlrd)、DOC (olefile)
- 剪贴板粘贴文件通过 base64 解码 + MIME type → 扩展名推断
- 侧边栏上传器类型列表中新增 xlsx/xls/doc
### 多模态聊天输入 + 多格式文件 (v4, 已迁移至 Vue 3)
原 Streamlit `st_multimodal_chatinput` 组件已废弃。当前由 `UnifiedInput.vue` 实现粘贴/拖拽/文件选择,`api_server.py:_process_files()` 统一处理上传文件(含 `.jrxml` 模板提取)。
新增文件格式支持: XLSX (openpyxl)、XLS (xlrd)、DOC (olefile)
### 批注检测 (v4)
- `backend/annotation_detector.py` — 识别用户在手写单据上的圈选和箭头标记
@@ -236,9 +223,9 @@ validation_service/ (FastAPI, 端口 8001) — 不变
## 已知注意点
- **环境变量优先级**: `backend/llm.py` 使用 `load_dotenv(override=True)` 确保 `.env` 值**始终覆盖**系统环境变量。曾因系统级 `ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic` 覆盖 `.env` 中的 MiniMax URL,导致 401 认证失败。新增 LLM 相关环境变量时,必须在 `.env` 中显式设置 `ANTHROPIC_*` 变量(而非仅设 `OPENAI_*` fallback),否则残留的系统环境变量会污染请求目标。
- **Anthropic SDK**: 使用原始 `anthropic` 包(非 `langchain-anthropic`),因为需要直连 MiniMax 兼容端点。API Key 优先读 `ANTHROPIC_API_KEY`fallback `OPENAI_API_KEY`。Anthropic SDK 会自动将 key 放入 `x-api-key` header。
- **MiniMax 模型名称**: `MiniMax-M2.7`(不是 `minimax-2.7`),大小写敏感。
- **Streamlit headless**: Windows 下必须设 `STREAMLIT_SERVER_HEADLESS=true` 跳过邮箱采集提示。
- **日志分析**: 通过 `trace_id` 字段可追踪一次请求的全链路。LLM 调用日志在 `logs/llm.log`,包含完整 prompt 和 response(各截断 10000 字符)。
- **验证服务结构检查**: 字段引用一致性 (`$F{field}` vs `<field>` 声明)、SQL SELECT 存在性、pageWidth/pageHeight/name 属性。
- **XSD 校验可选**: 需要 `validation_service/schemas/jasperreport_7_0_6.xsd` 存在。
@@ -249,9 +236,350 @@ validation_service/ (FastAPI, 端口 8001) — 不变
- **MAX_RETRY**: 默认 5 次。重试耗尽后 `pending_failure_context` 记录失败信息,下次用户输入时自动注入。
- **验证最小内容检查**: 验证服务额外检查至少 1 个 `<band>` + 1 个 `<textField>``<staticText>`,拦截空壳 JRXML。
- **XLSX 支持 (v3)**: 需要 `openpyxl>=3.1.0`(已加入 requirements.txt)。表格按工作表逐行读取,单元格用 `|` 分隔。
- **粘贴功能限制**: 文件以 base64 编码在 sessionStorage 中传递,单文件上限 20MB。文件建议使用 file_uploader 按钮
- **粘贴/拖拽**: `UnifiedInput.vue` 原生处理 paste/drop 事件,单文件上限 20MB。文件通过 `/api/upload` 上传至 `uploads/` 目录
- **torchvision**: `transformers` 库的懒加载需要 `torchvision`,已作为依赖安装。
- **opencv-python-headless**: 批注检测(圈选/箭头)依赖,通过 `pip install -r requirements.txt` 安装。
- **st-multimodal-chatinput**: Streamlit 聊天输入增强组件,替代 `st.chat_input`,支持粘贴/拖拽文件。返回 base64 编码文件内容
- **前端文件输入**: `UnifiedInput.vue` 原生处理文本输入 + 文件拖拽/粘贴/选择,替代原 Streamlit `st-multimodal-chatinput` 组件
- **xlwt**: 仅在测试中使用(生成 .xls 测试文件)。
- **分层精确生成**: 3 阶段管线仅在 `layout_schema.total_rows > 0` 时触发。文本请求和 `modify_report` 等意图不受影响,走原有 `generate` 节点。中间阶段(骨架/精调)跳过验证,只有最终 mapped 结果进入 `validate`
## 新增功能 (v6)
### 5-Issue Fix — 图片解析 Bug + 前端功能补全
**Fix 1 — 图片后缀 dot 缺失**: `file_parser.py` 后缀规范化(`"jpg"``".jpg"`),`api_server.py` 使用 `Path.suffix` 替代 `rsplit`。所有图片上传之前均因后缀不匹配回退到文本解析器,OCR/布局分析从未实际触发。
**Fix 2 — Vue 前端功能补全**:
- `ProcessSection.vue` 替代 `StreamingMessage.vue` + `NodeProgress.vue`,使用 `<details>`/`<summary>` 原生可折叠区域
- `Sidebar.vue` 新增历史版本下载列表(`jrxml_versions` 索引下载)
- `UnifiedInput.vue` 已集成文件拖拽/粘贴/芯片/移除(v5 已完成)
**Fix 3 — OCR 两层日志**: `agent/nodes.py` 新增 `_log_ocr_layers()``[内容层]` OCR 文本+字段提取,`[位置层]` 布局 schema 列×行+区域分类,`[合并]` 管线选择(3阶段 vs 单阶段)
**Fix 4 — 全过程流式输出+自动折叠**:
- `api_server.py` `node_start` 事件携带 `step_index`
- `chat.ts` 新增 `ProcessSection[]` 模型:per-section stream routing、完成自动折叠、运行中自动展开
- `ProcessSection.vue` 渲染步骤编号/标签/耗时/内容(XML 代码高亮)
**Fix 5 — 消息耗时显示**: `api_server.py` `agent_complete` 事件新增 `total_duration_ms``SummaryCard.vue` 显示总耗时,`chat.ts` 暴露 `lastDurationMs` + `formatDuration()`
## 已安装的 Claude Code 插件/Skills
| 插件 | 来源 | 关键 Skill |
|------|------|-----------|
| `superpowers` | `obra/superpowers-marketplace` | `tdd-workflow`(红-绿-重构)、`verification-loop`(修复验证)、`systematic-debugging`(根因分析) |
| `example-skills` | `anthropics/skills` | `webapp-testing`Playwright E2E 浏览器自动化)、`skill-creator` |
**测试工作流**:需求澄清 → TDD 红-绿-重构 → `webapp-testing` 浏览器验证 → `verification-loop` 确认 → 提交。
**E2E 测试前置条件**Chrome 已安装 (`C:\Program Files\Google\Chrome\Application\chrome.exe`)Playwright MCP Bridge 扩展需手动安装。
## 更新 (v7 — 2026-05-22)
### 会话持久化 & 多轮对话记忆修复
**原子写入** (`backend/session.py`): `save_session` 改用 tempfile + os.replace 原子写入,防止进程崩溃时 JSON 截断导致会话损坏。
**graph.stream 状态修复** (`api_server.py`): LangGraph 的 `graph.stream()`
只产出事件,不修改传入的 `agent_state``_run_graph_sync` 改为手动收集每个节点的
返回 dict 并 `agent_state.update()`,确保 done 事件到达时 agent_state 已是完整状态。
此修复解决了第二次请求时 `current_jrxml` 为空、导致多轮对话"失忆"的问题。
**save_session 调用时机**: 从 `stream_and_save` 末尾移至 `_sse_generator` 中 done 分支
yield `agent_complete` 之前),消除前端 `refreshFromApi()` 的竞态。
### OCR 管线打通
**uploaded_file_path 传递** (`api_server.py`): `_process_files` 返回的 `uploaded_paths`
注入 `agent_state["uploaded_file_path"]`,使 `process_input` 节点的 `OcrExtractor` 字段
精确提取和 `annotation_detector` 批注检测得以触发。此前 `uploaded_file_path` 始终为空,
第二层 OCR 从未执行。
### 前端体验改进
**下载区常驻** (`Sidebar.vue`): 下载区域始终可见,无文件时显示灰色"暂无下载文件",
生成完成后自动出现下载链接。
**侧边栏自动刷新** (`stores/session.ts`, `App.vue`): 新增 `refreshFromApi()` 方法,
`agent_complete` 后自动从 API 重新加载会话状态,下载按钮无需手动刷新即可出现。
**节点进度完整展示** (`api_server.py`): 移除 `node_complete` 事件的 SKIP_NODES 过滤,
所有节点(包括加载会话等内部节点)的 start/complete 事件均正常发送,前端可看到
完整流转(running → done)。
### modification_request 宽松化
原有 `status == "pass"` 条件去除:只要 `current_jrxml` 存在即设置
`user_modification_request`,确保修改意图的请求能携带完整上下文。
## 更新 (v8 — 2026-05-22)
### Prompt 花括号转义修复
**问题**: `skeleton_generation.md``$F{field_1}` 是给 LLM 看的占位字段名指令,
但 Python `.format()``{field_1}` 当作格式化占位符,因缺少对应 kwarg 抛出 `KeyError: 'field_1'`
所有图片上传触发的 `generate_skeleton` 节点均因此崩溃。
**修复**: 3 个 prompt 文件中 6 处 `{field_N}` / `{...}` 转义为 `{{field_N}}` / `{{...}}`
- `prompts/skeleton_generation.md``$F{field_1}``$F{{field_1}}`
- `prompts/field_mapping.md` — 4 处
- `prompts/refine_layout.md` — 1 处
Python 将 `{{` 输出为字面量 `{`LLM 看到的内容不变。
## 更新 (v9 — 2026-05-22)
### 测试基础设施全面补齐
**单元测试** (76 测试):
- `tests/test_session.py` — 27 测试:会话 CRUD、原子写入、唯一 ID、损坏 JSON 跳过
- `tests/test_error_kb.py` — 24 测试:指纹去重、关键词提取(中/英/JRXML)、ErrorKB CRUD、搜索、统计
- `tests/test_agent.py` — 5 个软断言强化为严格断言(`status`/`current_jrxml` 存在性检查)
- 已有测试:`test_ocr_extraction.py`49)、`test_layered_generation.py`19)、`test_validation.py`6)、`test_file_parser_formats.py`4)、`test_annotation_detector.py`7)、`test_e2e_ocr.py`3
**集成测试** (25 测试, `tests/test_api_integration.py`):
- FastAPI TestClient 全覆盖:健康检查、配置、会话 CRUD、文件上传、下载、Chat SSE、安全边界(路径穿越/非法 JSON/大 payload
- Mock LangGraph graph 避免真实 LLM 调用
**E2E 测试** (8 测试, `frontend/tests/e2e/main-flows.spec.ts`):
- Playwright 浏览器自动化:页面加载、侧边栏、会话管理、聊天流程、输入 UX
- 全量 API Mock`page.route`)无需后端运行
- 配置: `frontend/playwright.config.ts`, `npm run test:e2e`
**运行测试**:
```bash
# 全部单元+集成测试
cd D:\Idea Project\jaspersoft && python -m pytest tests/ -v
# 仅 E2E(需要前端 dev server
cd frontend && npx playwright test
```
### Bug 修复: create_session 参数缺失
`backend/session.py``create_session()` 新增可选参数 `session_id: Optional[str] = None`
`api_server.py:507` 调用 `create_session(session_id=session_id)` 时之前会抛出 `TypeError`
## 更新 (v10 — 2026-05-23)
### 5-Fix — 生成可靠性全面加固
**问题诊断**: 上传车辆历史卡片图片后,`map_fields` 节点 LLM 返回 0 字符,导致 ~11,500 字符的骨架 JRXML 被空字符串覆盖,修正循环无法恢复,最终输出 934 字符的占位桩(与原始图片内容完全不符)。
**Fix 1 — 空响应保护**: 所有 5 个生成节点(`generate_skeleton`, `refine_layout`, `map_fields`, `modify_jrxml`, `correct_jrxml`)增加空响应守卫。LLM 返回空字符串时拒绝更新 `current_jrxml`,保留前一有效版本。
**Fix 2 — max_tokens 扩容**: `backend/llm.py``max_tokens` 从 4096 → 8192。MiniMax-M2.7 支持最大 131K 输出 token8192 在生成复杂 JRXML(通常 5000-15000 字符)时提供充裕空间。
**Fix 3 — 快照回退**: 5 个生成节点在 LLM 输出 JRXML 短于 200 字符时,回退到生成前的 `prev_jrxml` 版本,防止 LLM 输出无意义短文本污染状态。
**Fix 4 — 修正循环注入 OCR 上下文**: `correct_jrxml` 节点将 OCR 提取结果(`ocr_context`)和布局 schema`layout_schema_text`)注入修正 prompt。此前修正节点"盲修"——只看到 JRXML 和编译错误,不理解原始单据的字段结构和布局意图。
**Fix 5 — 滚动续写机制**: 当 LLM 输出因 `max_tokens` 限制被截断(JRXML 不以 `</jasperReport>` 结尾),自动发送续写请求(附最后 800 字符锚点),最多 3 轮(1 正常 + 2 续写)。
- `backend/llm.py``MiniMaxLLM.stream()` 捕获 `stop_reason``_LLMLoggingWrapper``max_tokens` 截断时记录 WARNING
- `agent/nodes.py` — 新增 `_generate_with_continuation()` 辅助函数,5 个生成节点全部重构使用
- `_extract_jrxml()` — 正则表达式支持命名空间前缀 JRXML(`<\w+:jasperReport`
- 内容去重:续写文本直接拼接,依赖 `_extract_jrxml` 提取完整 XML
**MAX_RETRY 调整**: 默认值从 3 → 5(环境变量 `MAX_RETRY`),配合续写机制确保复杂报表有充分修正机会。
**JRXML 提取命名空间兼容**: `_extract_jrxml()``_generate_with_continuation()` 的完整性检查统一支持 `</ns0:jasperReport>` 等命名空间前缀闭合标签。
## 更新 (v11 — 2026-05-23)
### Java 渲染管线 + 像素级对比
**目标**: 将 JRXML 渲染为 PNG 图片,与用户上传的原始图片进行 SSIM(结构相似性)像素级对比。
**Java 依赖** (`lib/java/`):
| JAR | 用途 |
|-----|------|
| `jasperreports-6.21.0.jar` (5.8MB) | 核心库,**必须用 6.x**7.x 仅支持 Jackson XML 格式) |
| `commons-digester-2.1.jar` | XML 解析(6.x 使用 Digester 2.x |
| `commons-logging-1.3.5.jar`, `commons-collections4-4.5.0.jar`, `commons-beanutils-1.10.1.jar`, `commons-lang3-3.17.0.jar` | 基础依赖 |
| `itext-2.1.7.jar` | PDF 生成 |
| `jfreechart-1.5.5.jar` | 图表 |
| `ecj-3.38.0.jar` | Eclipse JDT 编译器(报表表达式编译) |
**Java 工具** (`lib/java/`):
| 文件 | 用途 |
|------|------|
| `JrxmlRenderer.java` | JRXML → PNG 渲染器 |
| `JrxmlDebug.java` | 诊断:SAX/JRXmlLoader/compile 三层测试 |
| `JrxmlGen.java` | 参考:程序化构建 JasperDesign → 序列化为 XML |
**Python 渲染封装** (`agent/nodes.py`):
- `_render_jrxml_to_png(jrxml, output_path, scale)` — 调用 Java `JrxmlRenderer`
- `_compute_pixel_similarity(rendered_png, reference_image)` — OpenCV + scikit-image SSIM 对比
**像素对比流程**: validate 节点 XSD 通过 → 有 `uploaded_file_path` → Java 渲染 → SSIM 对比 → SSIM < 0.4 且 diff > 60% → 标记 fail → 注入 correct_jrxml 修正上下文
**手动渲染**: `java -cp ".;jasperreports-6.21.0.jar;..." JrxmlRenderer input.jrxml output.png 2.0`
### 内容保真度 + 修正去重 (v10 补充)
- `_check_ocr_fidelity(jrxml, state)` — OCR 字段名/元素数/列数三重检查
- `correct_jrxml` 去重检测:输入输出相同 → `retry_count += 2`
- `prompts/correction.md` — 一次只修复第1个错误 + 输出不可与输入相同 + 命名空间严格指定
- `prompts/skeleton_generation.md`, `prompts/modification.md` — 明确命名空间约束
### consult_answer 前端显示修复
- `api_server.py``agent_complete` SSE 事件新增 `consult_answer` 字段
## 更新 (v12 — 2026-05-23)
### 多租户知识库系统
**核心架构**:用户自行维护多套知识库,每套 KB 拥有独立的文件存储、JSON 元数据和 ChromaDB 向量集合。会话可绑定不同 KB,LLM 基于 KB 中的字段定义和 JRXML 模板生成报表。
**存储架构**
```
kb_data/
├── users.json # 用户注册表
└── {user_id}/
├── profile.json
└── {kb_id}/
├── meta.json # KB 元数据 + 字段定义 + 模板索引
├── raw/ # 原始上传文件
├── chunks.json # RAG chunks(含 JRXML 模板完整文本)
└── chroma/ # KB 专属 ChromaDB
```
**新增后端模块**
| 文件 | 职责 |
|------|------|
| `backend/kb_manager.py` | 用户+KB CRUDcreate_user/list_users/create_kb/list_kbs/get_kb/delete_kb/update_kb_meta/get_kb_raw_dir/get_kb_chroma_path。原子 JSON 写入(tempfile + os.replace |
| `backend/kb_parser.py` | KB 解析管道:`parse_jrxml_fields()` XML 提取参数/字段/查询 → `process_file_for_kb()` 处理多种格式(jrxml/zip/tar/pdf/docx/xlsx/md`chunk_file_results()` 切割 → `build_kb_from_files()` 全管线(parse→chunk→embed→update meta |
| `backend/kb_searcher.py` | `KBChromaSearcher` 类:per-KB ChromaDB 懒连接。`search()` 语义搜索、`search_templates()` 仅搜索 JRXML 模板 chunk、`add_chunks()` 向量写入。全局 searcher 缓存 `_searchers: dict` |
| `backend/field_matcher.py` | OCR↔KB 字段匹配:1) Embedding 粗筛(余弦相似度 top-3)2) LLM 精确确认。返回 `{"工单号": "billNo", ...}` 映射 |
| `agent/datasource.py` | 数据源模式:`resolve_datasource_mode()` 检测用户意图 → `"parameter"`(默认 $P{xxx})或 `"jdbc"`(SQL 直连)。未配置 DB 时生成反问消息 |
| `scripts/init_default_kb.py` | 默认 KB 初始化:创建默认用户 → 解析 `rag/jrxml_source/` 下的 17 个 JRXML + 16 个 MD → chunk + embed → ChromaDB |
**新增 API 端点**api_server.py):
```
POST /api/users # 创建用户
GET /api/users # 用户列表
GET /api/users/{user_id} # 用户详情
DELETE /api/users/{user_id} # 删除用户
GET /api/users/{user_id}/kbs # KB 列表
POST /api/users/{user_id}/kbs # 创建 KB
GET /api/kbs/{kb_id} # KB 详情
DELETE /api/kbs/{kb_id} # 删除 KB
POST /api/kbs/{kb_id}/upload # 上传文件到 KB
POST /api/kbs/{kb_id}/build # 构建 KBchunk→embed
GET /api/kbs/{kb_id}/status # KB 状态
GET /api/kbs/{kb_id}/fields # KB 字段+模板列表
GET /api/kbs/{kb_id}/search?q=&type= # KB 语义搜索
PUT /api/sessions/{session_id}/kb # 绑定会话-KB
GET /api/sessions/{session_id}/kb # 获取会话绑定的 KB
```
**三条模板获取路径**
1. **管理页预上传**:用户上传文件到 KB → 解析管道 → chunks + ChromaDB → 对话选择 KB → retrieve 节点从 KB 检索
2. **对话框即时上传**:用户拖入 `.jrxml``_parse_jrxml_file()` → 注入 `agent_state["uploaded_template_jrxml"]` → 生成节点直接使用该模板
3. **口头引用模板**:用户说"根据标准结算单模板" → `_detect_template_intent()` 正则匹配 → `retrieve()` 在 KB 中搜索模板 → 注入 `kb_template_jrxml`
**模板上下文注入**:所有 6 个生成节点(generate/generate_skeleton/refine_layout/map_fields/modify_jrxml/correct_jrxml)通过 `_build_template_context(state)` 获取模板上下文,优先级:聊天上传 > KB 检索 > KB 字段定义。6 个 prompt 模板全部新增 `{template_context}` 占位符。
**前端新增**
| 文件 | 职责 |
|------|------|
| `stores/kb.ts` | Pinia store:用户列表、KB 列表、当前选择、字段/模板缓存、CRUD 操作、会话绑定 |
| `components/KbSelector.vue` | 对话顶部 KB 下拉选择器 + 管理按钮 |
| `components/KbManager.vue` | 模态面板:创建 KB、上传文件(支持 .jrxml/.md/.xlsx/.docx/.pdf/.csv/.zip 等)、构建、删除 |
**API Server 增强**
- `_process_files()` 检测 `.jrxml` 文件 → 提取参数/字段/查询/页面尺寸 → 注入 `uploaded_template_jrxml` + `uploaded_template_params`
- `agent/state.py` 新增 10 个字段:`kb_id`, `kb_fields`, `kb_field_mapping`, `uploaded_template_jrxml`, `uploaded_template_params`, `kb_template_jrxml`, `kb_template_name`, `datasource_mode`, `db_config`
**字段匹配管线**`_match_ocr_to_kb` → 尚未集成到节点):OCR 提取中文字段名 → `match_ocr_to_kb()` 两阶段匹配 → 结果为 `{"工单号": "billNo"}``format_field_mapping_context()` 注入 prompt → LLM 使用 `$P{billNo}` 而非 `$P{工单号}`
## 更新 (v13 — 2026-05-24)
### 3 阶段管道内容丢失修复 — Band 级窗口化 + 程序化字段映射
**问题**`generate_skeleton` 生成 34k 字符骨架 JRXML → `refine_layout` 将完整 34k 发给 LLM → LLM 重新生成简化版(~3k 字符,丢失 91.5%)。`map_fields` 同样存在字段映射时内容丢失问题。
**根因**:LLM 看到完整 JRXML 时倾向于"重新生成"而非"在原基础上修改坐标/字段名"。提示词调控无法可靠解决。
**修复方案**(按用户要求 — 程序化节点控制,不靠 LLM 提示词):
#### `refine_layout`Band 级窗口化 LLM 精调
新增 `agent/jrxml_windower.py` — JRXML 拆解/切分/重组引擎:
| 函数 | 用途 |
|------|------|
| `decompose_jrxml()` | ET 安全解析 → 分离 headerfield 声明/queryString 等,不发给 LLM+ 所有 band |
| `split_band_into_windows()` | 超过 4000 字符的 band 在元素闭合标签处切分为多个窗口 |
| `reassemble_band_windows()` | 合并同一 band 的多个窗口结果 |
| `reassemble_jrxml()` | header + 所有修改后 band + footer → 完整 JRXML |
| `count_elements()` | 正则计数 textField/staticText/field(兼容命名空间前缀) |
| `validate_element_count()` | 校验元素数变化,>10% 回退到前一版本 |
**LLM 每次只看到 ~2-4k 字符片段**,无法"重写整个报表"。header 部分完全不发给 LLM,原样保留。
#### `map_fields`:完全程序化替换(零 LLM 调用)
`_programmatic_map_fields()` — 纯正则替换 `$F{field_N}` → OCR 真实字段名,100% 确定性。
`_sanitize_field_name()` — 非 ASCII 字符(中文/日文)转义为 `_uXXXX_` Unicode 码点格式,确保 JRXML 合法。
#### 新增测试
| 文件 | 用例数 | 覆盖 |
|------|--------|------|
| `tests/test_jrxml_windower.py` | 28 | 拆解/往返重组/窗口切分/元素计数/命名空间/多 section 多 band |
| `tests/test_programmatic_map_fields.py` | 20 | 字段声明替换/引用替换/中文转义/坐标保留/部分映射/空字段跳过 |
完整测试套件(385 项)无回归。
## 更新 (v14 — 2026-05-24)
### max_tokens per-node + 修正循环死锁修复
**问题 A — max_tokens 自限**: `backend/llm.py` 硬编码 `max_tokens=8192`。MiniMax M2.7 的 reasoning token 吃光 8192 输出预算后骨架生成为空(0 个可见字符)。其他节点(correct_jrxml/modify_jrxml)输入 68K+ 字符时输出也被截断。
**问题 B — ns:field 命名空间前缀正则失配**: `_programmatic_map_fields()` 正则 `<field\b` 匹配不到 `<ns0:field name="field_1">`,导致字段声明保持占位符但引用被替换为 OCR 字段名,校验报"used in expressions but not declared"。
**问题 C — 验证服务 502 修正死循环**: 验证服务(port 8001)未启动时,`validate_jrxml()` 返回 502。错误消息被当作 JRXML 校验错误送入 `explain_error → correct_jrxml`LLM 尝试"修复"网络错误产出 HTML/markdown 等垃圾,循环 5 轮直到 retry_count 耗尽。
**问题 D — correct_jrxml 从未写回 current_jrxml**: 修正后的 JRXML 只写入 `conversation_history`,从不更新 `state["current_jrxml"]`,导致每轮 validate 看到同一份原始 JRXML,修正完全无效。这是 5 轮 jrxml_length 始终 4441 不变的根本原因。
**修复方案**:
#### 1. per-node max_tokens`backend/llm.py` + `agent/nodes.py`
- `get_llm(caller, max_tokens=None)` — 新增可选 `max_tokens` 参数,透传到 `_build_raw_llm`
- `MiniMaxLLM.__init__()` — 存储 `self._max_tokens`
- `LLM_MAX_TOKENS` 环境变量覆盖默认 8192
- 5 个生成节点 max_tokens 提升到 32768`generate`, `generate_skeleton`, `refine_layout`, `modify_jrxml`, `correct_jrxml`
- `generate_skeleton` 空响应自动重试(max_tokens=65536
#### 2. ns:field 正则修复(`agent/nodes.py:548`
- `<field\b``<[\w:]*field\b` 兼容 `<ns0:field>`, `<field>` 等所有命名空间前缀
#### 3. 验证服务不可用防护
- `backend/validation.py` — 区分 ConnectError/HTTPStatusError(5xx):返回 `service_unavailable: True`
- `agent/nodes.py:validate` — 透传 `state["service_unavailable"]`
- `agent/graph.py:route_after_validate``service_unavailable` 时直接 `finalize`,不进入修正循环
#### 4. correct_jrxml 输出合法性守卫
- 新增 JRXML 有效性检查:输出不含 `<jasperReport` 且不含 `<?xml` 时,回退到前一版本
- **Bug 修复**: `state["current_jrxml"] = jrxml` 写回修正结果
#### 5. 连续输出提取增强
- `_strip_continuation_wrapper()` — 剥离续写响应中 LLM 重新添加的 markdown 代码块和自然语言前缀
- `_extract_jrxml()` — 逐一检查多个 markdown 代码块,跳过非 JRXML 片段
- `_generate_with_continuation()` — 续写轮次自动应用 `_strip_continuation_wrapper`
#### 新增环境变量
| 变量 | 描述 | 默认值 |
|------|------|--------|
| `LLM_MAX_TOKENS` | 默认 max_tokens(各节点可覆盖) | 8192 |
-1327
View File
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
# jaspersoft-fix 评测报告
**项目路径**: `D:\Idea Project\jaspersoft-fix`
**评测时间**: 2026-05-25
**评测维度**: 代码质量 · 安全与稳定性 · 工程实践 · 产品设计
---
## 综合评分
| 维度 | 评分 | 主要问题 |
|------|------|----------|
| 代码质量与架构 | 7.3/10 | nodes.py 1709行 God Module、无文件锁并发风险 |
| 安全与稳定性 | P0×1 + P1×2 + P2×4 | llm.log 写全量 prompt、session 并发覆盖、无 magic bytes 校验 |
| 工程实践 | 3.5/5 | 原子写入优秀、trace_id 传播良好、无 E2E 测试 |
| 产品设计 | 4.2/5 | natural_explanation 透明、非 fix 报告误报进度不透明 |
---
## 一、代码质量与架构(7.3/10)
亮点:**原子写入**tempfile+fsync+replace)设计优秀、v5 Band 级分层精确生成架构、前端 Vue3+Pinia 结构清晰。
主要问题:
| 问题 | 严重度 | 说明 |
|------|--------|------|
| nodes.py 过胖 | P1 | 1709行,14个工作流节点,应拆分到 `agent/utils.py` |
| session.py 无文件锁 | P0 | 多用户并发写同一 session 会互相覆盖(无 flock/fcntl |
| 废弃 Vue 组件 | P1 | `StreamingMessage.vue`/`NodeProgress.vue` 仍在 frontend/components |
---
## 二、安全与稳定性
| 等级 | 数量 | 问题 |
|------|------|------|
| **P0** | 1 | `llm.log` 写全量 prompt`prompt[:10000]`),API Key 可能泄露 |
| **P1** | 2 | session 并发无锁(见 P0);文件上传无 magic bytes 校验 |
| **P2** | 4 | LLM prompt 注入风险;ChromaDB 无认证;CORS 宽松;无 API 认证 |
已做好:`.env` 隔离、`sessions/` gitignore、SQL 注入防护(参数化查询)、hex session_id 校验防路径穿越。
**⚠️ llm.log 泄露风险**
`backend/llm.py` 第 47-49 行写 `prompt[:10000]` 到日志,第 66-67 行写 `response[:10000]`。prompt 中若含用户上传的文档内容(包含敏感字段名)或 API 调用上下文,可能被记录。需要脱敏。
---
## 三、工程实践(3.5/5
亮点:原子写入(tempfile+fsync+replace)优秀、日志 trace_id 传播(contextvars)、JSONFormatter 结构化日志、`nodes.py` 的 namespace 检查修复(五轮修正失败根因)。
主要问题:
| 问题 | 严重度 |
|------|--------|
| 会话并发无文件锁 | P0 — 多用户并发写同一 session 会互相覆盖 |
| 无 E2E 测试 | P1 — 无 Playwright 测试 |
| 废弃 Vue 组件未删除 | P1 — `StreamingMessage.vue`/`NodeProgress.vue` |
| 冷启动慢(llm.py 初始化) | P2 |
---
## 四、产品设计(4.2/5
亮点:错误修正循环设计优秀、五轮自动修正+失败上下文注入、`SummaryCard.vue` 正确展示 `natural_explanation`(非 fix 报告误报"进度不透明"是错的)。
主要问题:
| 优先级 | 问题 |
|--------|------|
| P0 | 会话并发无文件锁(影响稳定性) |
| P1 | `export_pdf` 未实现(需标记"敬请期待" |
| P1 | 意图分类无用户确认机制 |
| P2 | 流式输出无 XML 语法高亮 |
| P2 | 空白状态无引导示例 |
---
## 优先修复路线图
### P0(立即修复)
1. **会话并发文件锁**:在 `save_session()``fcntl.flock()` 保护先读后写
2. **LLM 日志脱敏**prompt/response 中截断或替换 API Key 为 `[REDACTED]`
### P1(近期处理)
3. 删除废弃 Vue 组件(`StreamingMessage.vue`/`NodeProgress.vue`
4. 实现 `export_pdf` 或标记"敬请期待"
5. 意图分类结果标签化供用户确认
6. 添加 Playwright E2E 测试
### P2(有空再搞)
7. 流式输出 XML 语法高亮
8. 空白状态引导示例
---
## 与 jaspersoft(非 fix)的关键差异
| 项目 | jaspersoft(非 fix | jaspersoft-fix |
|------|---------------------|----------------|
| commit | `2d5183d` OCR fidelity reform | `0839ba9` WIPrag + test image |
| namespace 前缀 | 未处理 | 已修复 `_extract_jrxml()` |
| 五轮修正失败根因 | 旧评分公式 | 已修复(去掉 field_coverage 权重) |
| OCR 自动发现文档类型 | 需手动 | 已实现 |
| 进度透明度 | 非 fix 报告误报"不透明" | 实际展示 natural_explanation ✅ |
---
*评测时间: 2026-05-25 (Asia/Hong_Kong)*
*评测工具: Mavis AI Agent*
+176 -114
View File
@@ -1,42 +1,60 @@
# JRXML 生成代理
一个本地桌面应用程序,帮助非技术用户通过多轮自然语言对话创建 JasperReports 模板(JRXML)。
一个本地桌面应用程序,通过多轮自然语言对话帮助非技术用户创建 JasperReports 模板(JRXML)。
## 功能
- **多轮聊天**:通过对话优化报表 -- 添加列、更改标题、添加汇总
- **自动验证**:每次生成或修改后都会验证 JRXML
- **自动修正**如果验证失败,代理会分析错误并自动修正(最多 3 次)
- **模板检索**:使用 Chroma 向量数据库检索相关的 JRXML 示例以获得更好的生成效果
- **文件上传**:支持图片(OCR识别)、PDF、Word、Excel、文本文件等
- **聊天粘贴/拖拽**:支持直接在对话框中 Ctrl+V 粘贴或拖拽文件(图片/PDF/Excel/Word
- **单据OCR识别**:上传报表单据图片后自动提取所有字段(4策略优先级 + 置信度评分
- **批注检测**:识别手写单据上的圈选和箭头标记,自动定位用户要修改的字段
- **分层精确生成**A4 报表图片先提取布局 schema,再分 3 阶段(骨架→精调→字段映射)生成,避免 OCR 元素过多导致 prompt 溢出
- **下载**:导出已验证的、可供 JasperReports 使用的 JRXML 文件
- **多轮对话**:通过对话优化报表 添加列、更改标题、添加汇总
- **自动验证**:每次生成或修改后验证 JRXML(结构检查 + XSD 校验 + 像素级对比)
- **自动修正**:验证失败分析错误并自动修正(最多 5 次)
- **错误自增长知识库**:修正案例指纹去重入库,避免重复犯错
- **模板检索**ChromaDB 语义搜索 JRXML 示例和中文文档
- **文件上传**:对话框拖拽/粘贴/选择,支持图片PDF、Word、Excel、文本
- **单据 OCR 识别**:上传报表图片后自动提取字段(4 策略优先级 + 置信度)
- **批注检测**:识别手写单据上的圈选和箭头标记
- **分层精确生成**3 阶段管线(骨架→精调→字段映射)Band 级窗口化防止内容丢失
- **多租户知识库**:独立 KB 管理,含字段定义 + JRXML 模板 + ChromaDB 向量检索
- **Java 渲染管线**JRXML → PNG 渲染 + SSIM 像素级对比
- **下载**:导出经过验证的 JRXML 文件,含历史版本追溯
## 架构
```
Streamlit 界面 (app.py)
|
LangGraph 代理 (agent/)
|-- retrieve (Chroma/embeddings)
|-- generate / generate_skeleton → refine_layout → map_fields (分层生成)
|-- validate (FastAPI service)
|-- explain + correct (auto-fix loop)
|-- modify (multi-turn edits)
|
前端 (Vue 3 + Vite, 端口 5173)
│ Pinia stores (chat / session / kb)
│ 9 components (Sidebar, ChatMessages, ProcessSection, UnifiedInput,
SummaryCard, KbSelector, KbManager, StreamingMessage, NodeProgress)
▼ HTTP + SSE (Server-Sent Events)
后端 API (FastAPI, 端口 8000)
│ REST + SSE 流式推送
│ 包装 LangGraph Agent ──► agent/ (18 节点状态机)
│ ├─ process_input (文件解析 + OCR + 批注检测)
│ ├─ manage_context (token 计数 + 压缩)
│ ├─ classify_intent (8 类意图识别)
│ ├─ retrieve (RAG + 错误 KB + KB 模板搜索)
│ ├─ generate / generate_skeleton → refine_layout → map_fields
│ ├─ validate (XSD + 结构 + 像素对比)
│ ├─ explain_error + correct_jrxml (自动修正循环, 最多 5 次)
│ └─ modify_jrxml / consult / undo / reset / preview / finalize
├── backend/ 服务层
│ ├─ llm (Anthropic SDK / OpenAI / Ollama)
│ ├─ session (原子 JSON 持久化)
│ ├─ validation (验证服务客户端)
│ ├─ kb_manager / kb_parser / kb_searcher (多租户知识库)
│ ├─ field_matcher (OCR↔KB 字段匹配)
│ ├─ ocr_extractor / layout_analyzer / annotation_detector (OCR 管线)
│ ├─ rag_adapter / error_kb / embeddings (向量检索)
│ └─ file_parser / logger / jrxml_reorder
FastAPI 验证服务 (:8001)
|-- Structural checks (field references, SQL, page dimensions)
|-- XSD schema validation (if jasperreport.xsd available)
└─ 结构检查 + XSD Schema + 最小内容校验
```
## 前置要求
- Python 3.11+
- 完整的编译验证需要:JDK 21 + JasperReports 7.0.6
- OpenAI 兼容 API 密钥(或本地 Ollama
- JDK 21+Java JRXML→PNG 渲染管线使用)
- Anthropic 兼容 API 密钥(MiniMax M2.7 等
## 快速开始
@@ -52,29 +70,32 @@ pip install -r requirements.txt
cp .env.example .env
```
编辑 `.env` 配置您的 API 密钥和偏好设置。
编辑 `.env` 配置 API 密钥和偏好设置。
### 3. 初始化知识库
```bash
python scripts/init_kb.py
python scripts/init_default_kb.py
```
### 4. 启动验证服务
### 4. 启动服务
**一键启动(推荐)**:双击 `start.bat`,自动启动验证服务、后端 API、前端开发服务器。停止用 `stop.bat`
**手动启动**(需要三个终端):
在一个终端中运行:
```bash
python -m uvicorn validation_service.main:app --port 8001
# 终端 1 — 验证服务(必须先启动)
python -m uvicorn validation_service.main:app --port 8001 --host 0.0.0.0
# 终端 2 — 后端 APISSE + REST
python -m uvicorn api_server:app --port 8000 --host 0.0.0.0
# 终端 3 — 前端开发服务器
cd frontend && npm install && npm run dev
```
### 5. 启动 Streamlit 界面
在另一个终端中运行:
```bash
streamlit run app.py
```
在浏览器中打开 http://localhost:8501。
浏览器打开 `http://localhost:5173`
## 使用示例
@@ -87,91 +108,132 @@ streamlit run app.py
第三轮 - 修改:
> "将标题改为 '2024 员工目录' 并加粗"
每一轮都会自动验证和修正 JRXML。
## 验证服务(当前限制)
由于完整的 JasperReports 7.0.6 编译需要 JDK 21,当前的验证执行以下检查:
1. 结构检查:字段声明一致性、SQL 查询存在性、页面尺寸、报表名称
2. XSD schema 验证:如果 `validation_service/schemas/jasperreport_7_0_6.xsd` 可用
要进行完整的编译验证,请将 `jasper-validator.jar` 放在 `validation_service/` 目录并更新 `main.py`
## 测试
```bash
pytest tests/test_validation.py -v
pytest tests/test_agent.py -v
pytest tests/ -v
```
每一轮自动验证和修正 JRXML。上传报表图片后自动触发 OCR 识别 + 分层精确生成。
## 项目结构
```
jrxml-agent/
app.py Streamlit 聊天界面(多模态输入
agent/
state.py AgentState 定义(28 字段)
nodes.py 图节点(generate, generate_skeleton, refine_layout 等,18 节点)
graph.py LangGraph 状态机(含分层生成路由)
backend/
llm.py LLM 工厂(Anthropic SDK / OpenAI / Ollama
logger.py 集中日志模块(JSON + trace_id
embeddings.py 嵌入模型工厂
validation.py 验证服务客户端
rag_adapter.py RAG 语义搜索适配器
error_kb.py 错误自增长知识库
file_parser.py 文件解析器(PDF/DOCX/XLSX/XLS/DOC/图片/文本
layout_analyzer.py A4 模板布局分析(含布局 schema 提取
ocr_extractor.py OCR 字段精确提取(4 策略 + 置信度)
annotation_detector.py 批注检测(圈选 + 箭头 + OCR 关联)
session.py 会话持久化 CRUD
prompts/
loader.py Prompt 加载器(热重载
*.md 10 个 Prompt 模板文件
validation_service/
main.py FastAPI 验证服务器
validate.bat Windows 启动器
jaspersoft/
api_server.py FastAPI SSE 后端(REST + 流式推送
start.bat / stop.bat 一键启停脚本
start.py Python 启动器
agent/ LangGraph 工作流引擎
state.py AgentState 类型定义(~40 字段)
nodes.py 18 个工作流节点实现
graph.py 状态图编译 + 9 个路由函数
datasource.py 数据源模式解析(参数 vs JDBC)
jrxml_windower.py Band 级拆解/切分/重组引擎
backend/ 后端服务层
llm.py LLM 工厂(Anthropic SDK / OpenAI / Ollama
logger.py 结构化 JSON 日志(trace_id + UTC+8
validation.py 验证服务 HTTP 客户端
session.py 会话 JSON 原子持久化 CRUD
embeddings.py 嵌入模型工厂(HuggingFace / OpenAI
rag_adapter.py RAG 语义搜索适配器
error_kb.py 错误自增长知识库(指纹去重 + ChromaDB
file_parser.py 多格式文件解析(PDF/DOCX/XLSX/XLS/DOC/图片/文本)
layout_analyzer.py A4 模板布局分析(布局 schema 提取 + 列聚类)
ocr_extractor.py OCR 字段精确提取(4 策略 + 置信度)
annotation_detector.py 批注检测(圈选 + 箭头 + OCR 关联)
kb_manager.py 多租户知识库 CRUD(用户 + KB)
kb_parser.py KB 解析管道(解析→chunk→embed
kb_searcher.py Per-KB ChromaDB 搜索适配器
field_matcher.py OCR↔KB 字段匹配(Embedding + LLM 两阶段)
jrxml_reorder.py JRXML 元素重排序(XSD sequence 合规)
prompts/ LLM Prompt 模板(热重载)
loader.py Prompt 加载器
*.md 10 个 Prompt 模板文件
frontend/ Vue 3 + Vite 前端
src/
api/client.ts SSE 客户端 + fetch 封装
stores/
chat.ts Pinia: 消息/流式/节点进度
session.ts Pinia: 会话管理
kb.ts Pinia: 知识库状态
components/
Sidebar.vue 会话列表 + 下载 + 历史版本
ChatMessages.vue 消息列表渲染
ProcessSection.vue 过程折叠区(<details>/<summary>
StreamingMessage.vue 流式消息显示
NodeProgress.vue 节点进度指示器
UnifiedInput.vue 统一输入框(文本 + 文件拖拽/粘贴/芯片)
SummaryCard.vue 结果摘要卡片(含耗时)
KbSelector.vue KB 下拉选择器
KbManager.vue KB 管理面板(上传/构建/删除)
validation_service/ 独立验证服务(端口 8001)
main.py FastAPI 验证端点
schemas/
jasperreport_7_0_6.xsd JasperReports XSD Schema
lib/java/ Java JRXML 渲染管线
JrxmlRenderer.java JRXML → PNG 渲染器
JrxmlDebug.java 诊断工具
JrxmlGen.java 参考:程序化 JasperDesign
jasperreports-6.21.0.jar 核心 JasperReports 库
tests/ Python 测试(19 文件, ~385 测试)
test_session.py 会话 CRUD27
test_ocr_extraction.py OCR 字段提取(49
test_jrxml_windower.py Band 窗口化(28
test_api_integration.py API 集成(25
test_error_kb.py 错误 KB24
test_programmatic_map_fields.py 字段映射(20
test_layered_generation.py 分层生成(19
test_annotation_detector.py 批注检测(7
test_validation.py 验证服务(6)
test_file_parser_formats.py 文件解析(4
test_e2e_ocr.py OCR E2E3
test_agent.py / test_kb_*.py /
data/
sample_templates/ 知识库的 JRXML 模板
corrections/ 错误修正案例
logs/
app.log 应用日志(节点流转、路由、用户交互)
llm.log LLM 调用日志(完整 prompt / response
sample_templates/ JRXML 样本模板
corrections/ 错误修正案例
scripts/
init_kb.py Chroma 知识库初始化脚本
tests/
test_validation.py 验证服务测试
test_agent.py 代理集成测试
test_e2e_ocr.py OCR 端到端测试
test_ocr_extraction.py OCR 字段提取单元测试
test_annotation_detector.py 批注检测测试
test_file_parser_formats.py 多格式解析测试
test_layered_generation.py 分层生成测试
requirements.txt
.env.example
README.md
init_default_kb.py 多租户默认知识库初始化
```
## 环境变量
| 变量 | 描述 | 默认值 |
|----------|-------------|---------|
| LLM_BACKEND | cloud local | cloud |
| LLM_PROVIDER | openai 或 anthropic | openai |
| OPENAI_API_KEY | API 密钥(OpenAI 或 MiniMax | - |
| OPENAI_BASE_URL | API 基础 URL | https://api.openai.com/v1 |
| ANTHROPIC_API_KEY | Anthropic 兼容 API 密钥(优先 | - |
| ANTHROPIC_BASE_URL | Anthropic 兼容 Base URL | https://api.minimaxi.com/anthropic |
| LLM_MODEL | 模型名称 | MiniMax-M2.7 |
| LOCAL_LLM_MODEL | Ollama 模型 | qwen2.5-coder:7b |
| EMBED_BACKEND | local 或 cloud | local |
| LOCAL_EMBED_MODEL | 嵌入模型 | Qwen/Qwen3-Embedding-0.6B |
| VALIDATION_SERVICE_URL | 验证端点 | http://localhost:8001/validate |
| CHROMA_PERSIST_DIR | Chroma 存储位置 | ./db/chroma |
| MAX_RETRY | 自动修正尝试次数 | 3 |
| CONTEXT_MAX_TOKENS | 上下文压缩阈值 | 6000 |
| LOG_DIR | 日志目录 | ./logs |
| LOG_LEVEL | 日志级别 | DEBUG |
| SESSIONS_DIR | 会话持久化目录 | ./sessions |
|------|------|--------|
| `LLM_BACKEND` | LLM 后端: cloud / local | cloud |
| `LLM_PROVIDER` | 云端提供商: anthropic / openai | anthropic |
| `ANTHROPIC_API_KEY` | Anthropic 兼容 API 密钥(优先 | - |
| `ANTHROPIC_BASE_URL` | Anthropic 兼容 Base URL | https://api.minimaxi.com/anthropic |
| `OPENAI_API_KEY` | OpenAI 兼容 API 密钥(fallback | - |
| `OPENAI_BASE_URL` | OpenAI 兼容 Base URL | https://api.openai.com/v1 |
| `LLM_MODEL` | 模型名称 | MiniMax-M2.7 |
| `LLM_MAX_TOKENS` | 默认 max_tokens(各节点可覆盖) | 8192 |
| `LOCAL_LLM_MODEL` | Ollama 模型 | qwen2.5-coder:7b |
| `EMBED_BACKEND` | 嵌入模型后端: local / cloud | local |
| `LOCAL_EMBED_MODEL` | 本地嵌入模型 | Qwen/Qwen3-Embedding-0.6B |
| `VALIDATION_SERVICE_URL` | 验证服务端点 | http://localhost:8001/validate |
| `CHROMA_PERSIST_DIR` | ChromaDB 持久化目录 | ./db/chroma |
| `MAX_RETRY` | 自动修正最大尝试次数 | 5 |
| `CONTEXT_MAX_TOKENS` | 上下文压缩阈值 | 6000 |
| `CONTEXT_KEEP_RECENT` | 保留最近 N 轮对话 | 4 |
| `SESSIONS_DIR` | 会话持久化目录 | ./sessions |
| `LOG_DIR` | 日志目录 | ./logs |
| `LOG_LEVEL` | 日志级别 | DEBUG |
| `HISTORY_MAX_SNAPSHOTS` | 状态快照保留数 | 10 |
| `OCR_USE_GPU` | OCR GPU 加速 | false |
| `OCR_CONFIDENCE_THRESHOLD` | OCR 置信度最低阈值 | 0.5 |
| `RAG_EMBED_MODEL` | RAG 嵌入模型 | paraphrase-multilingual-MiniLM-L12-v2 |
| `RAG_JRXML_SOURCE` | JRXML 模板源目录 | ./rag/jrxml_source |
| `RAG_COLLECTION_NAME` | ChromaDB 集合名 | jrxml_chunks |
## 测试
```bash
# 全部单元 + 集成测试
cd D:\Idea Project\jaspersoft && python -m pytest tests/ -v
# 仅 E2E 测试(需要前端 dev server 运行)
cd frontend && npx playwright test
```
-202
View File
@@ -1,202 +0,0 @@
# 改进路线图
## 阶段一:代码质量(低风险,快速交付)
### 1. Prompt 拆分 ✓
- [x] 创建 `prompts/` 目录
- [x] 7 个 prompt 各拆为独立 `.md` 文件
- [x] `nodes.py` 改为从文件加载
- [x] 支持热重载(文件变更无需重启)
### 2. 修复无效代码 ✓
- [x] `backend/llm.py``get_num_tokens()` 修复为正确 API
- [x] `backend/embeddings.py` — 修复 docstring 函数名不一致
- [x] `backend/llm.py` — 统一 LLM 接口基类 `_BaseLLM`
---
## 阶段二:用户体验(核心改造)
### 3. 流式输出 + 节点平铺 ✓
- [x] `backend/llm.py` — LLM 工厂支持 `stream()` 统一接口
- [x] `agent/nodes.py` — generate/modify/correct 节点使用流式 + `get_stream_writer()`
- [x] `app.py` — 使用 `stream_mode=["updates", "custom"]` 捕获流式事件
- [x] 节点状态平铺(处理过程 expander 逐节点展示)
- [x] 流式完成后节点自动折叠
- [x] 完成后单独展示「总结卡片」
### 4. 错误自增长知识库 ✓
- [x] `backend/error_kb.py` — ErrorKB 类(ChromaDB 持久化)
- [x] 错误指纹去重(标准化 + MD5
- [x] `correct_jrxml` — 保存修正前状态到 `last_error_case`
- [x] `validate` — 修正成功时自动记录(仅新错误,自动去重)
- [x] `retrieve` — 搜索错误知识库,注入历史修正案例
- [x] 记录内容:错误 + 修正前后 JRXML + prompt + 工具链 + 模型
### 5. 文件上传支持 ✓
- [x] `backend/file_parser.py` — 统一解析接口
- [x] 图片 → PIL 元信息 + PaddleOCR(可选安装后自动识别)
- [x] PDF → pdfplumber / PyMuPDF 文本提取
- [x] DOCX → python-docx 文本提取
- [x] 纯文本 (.txt/.csv/.json/.xml) → 直接读取
- [x] `can_use_vision()` — 根据模型名判断是否支持原生多模态
- [x] `app.py` — 侧边栏文件上传组件(多文件,可移除)
- [x] 上传文本自动注入下一条消息前缀
### 6. A4 图片模板识别 ✓
- [x] `backend/layout_analyzer.py` — 完整布局分析模块
- [x] A4 比例判定:exact(±3%) / close(±8%) / not_a4 三档
- [x] PaddleOCR 布局分析:逐元素提取坐标(x,y,w,h)、字号、文本
- [x] 行分组:Y 轴容差自动聚类
- [x] 结构化输出:`图片模板共 X 行,第 1 行有 Y 个元素,其中元素 a 长...高...字体...内容是...`
- [x] 检测门槛:≥2 个 OCR 元素 + A4 比例 → 标记为模板
- [x] `app.py` — 上传图片/PDF 时自动触发布局分析,替换为布局描述
### 7. 会话历史 JRXML 下载 ✓
- [x] `agent/state.py` — 新增 `jrxml_versions` 字段
- [x] `agent/nodes.py``finalize` 节点追加版本记录
- [x] `app.py` — 侧边栏"历史版本"折叠区,每版本独立下载按钮
### 8. 预览功能修复 ✓
- [x] 根因:`preview_report` 路由到 `save_session``validate` 触发不必要的验证修正循环
- [x] 修复:`route_after_save` — 预览/导出意图跳过验证直接 `finalize`
---
## 阶段三:细节修复
### 9. Ctrl+C 修复 ✓
- [x] `app.py` — 注入 JS 拦截裸 `c` 键,保留 Ctrl+C 复制行为
---
## 阶段四:可观测性
### 10. 结构化日志系统 ✓
- [x] `backend/logger.py` — 集中日志配置模块
- [x] JSON 格式化(每行一条记录,便于 jq/pandas 分析)
- [x] 请求级 trace_idcontextvars 自动传播,一次用户请求贯穿全链路)
- [x] 独立 LLM 日志文件 `logs/llm.log`(记录完整 prompt 和 response
- [x] 时区:UTC+8(中国时区)
- [x] 日志轮转(单文件 10MB,保留 5 备份)
- [x] `backend/llm.py``_LLMLoggingWrapper` 包装所有 LLM 后端
- [x] 记录每次 invoke/stream 的请求 prompt、响应内容、耗时、模型、调用来源
- [x] 异常时也记录完整 prompt
- [x] `agent/nodes.py``@log_node` 装饰器覆盖 17 个节点
- [x] 入口/出口/异常三个阶段的日志
- [x] 自动记录 state 关键字段摘要(session_id、intent、status、jrxml_length 等)
- [x] 每个节点耗时(duration_ms
- [x] `agent/graph.py``@_log_route` 装饰器覆盖 8 个路由函数
- [x] 记录每次路由决策(来源 → 目标)
- [x] `app.py` — 用户交互日志
- [x] 收到用户输入(含上传文件信息)
- [x] 代理执行开始/完成(含最终 intent、status、jrxml_length
- [x] 异常时记录错误详情
- [x] 会话新建/切换/删除操作日志
- [x] `backend/session.py` — 会话创建/删除日志
- [x] `backend/validation.py` — 验证完成/连接失败日志
- [x] `.env.example` — 新增 `LOG_DIR``LOG_LEVEL` 配置项
- [x] `.gitignore` — 新增 `logs/` 忽略规则
---
## 执行顺序建议
```
1. Prompt 拆分 ──► 2. 无效代码修复
3. 流式输出 + 节点平铺
┌─────────────┼─────────────┐
▼ ▼ ▼
4. 错误自增长 5. 文件上传 7. 下载历史
│ │
▼ ▼
6. A4 模板识别 8. 预览修复
9. Ctrl+C 修复
10. 结构化日志系统
```
---
## 阶段五:OCR 与智能上传 (v3/v4) ✓
### 11. OCR 单据字段精确提取 ✓
- [x] `backend/ocr_extractor.py` — 4 策略优先级提取 (exact_match → kv_pair → regex → table_match)
- [x] PaddleOCR 首次识别后将原始结果(含所有文本元素 + bbox坐标)持久化
- [x] `_format_ocr_context()` — OCR 结果格式化为 LLM prompt 注入
- [x] `process_input` 节点在上传图片时自动触发 OCR 字段提取
- [x] OCR 结果持久化到会话文件
### 12. 多模态聊天输入 ✓
- [x] `app.py``st.chat_input` 替换为 `st_multimodal_chatinput`
- [x] 支持 Ctrl+V 粘贴文件 + 拖拽 + 文件按钮
- [x] `_process_uploaded_file()` — 提取共享文件处理逻辑(消除 ~70 行重复代码)
- [x] 剪贴板文件 base64 解码 + MIME type → 扩展名推断
### 13. 多格式文件支持 ✓
- [x] `backend/file_parser.py` — 新增 XLSX (openpyxl)、XLS (xlrd)、DOC (olefile)
- [x] 侧边栏上传器类型列表中新增 xlsx/xls/doc
- [x] 单元测试: `tests/test_file_parser_formats.py` (4 tests)
### 14. 批注检测 ✓
- [x] `backend/annotation_detector.py` — 圈选 + 箭头 + OCR 关联
- [x] 圆圈检测: 红色通道增强 → HoughCircles
- [x] 箭头检测: Canny → HoughLinesP → 线段聚类 → 端点方向判定
- [x] `format_annotation_context()` — 批注结果格式化为中文提示
- [x] `process_input` 节点在 OCR 提取后自动运行批注检测
- [x] `annotation_result` 字段持久化到 AgentState + 会话文件
- [x] 单元测试: `tests/test_annotation_detector.py` (7 tests)
### 15. OCR 上下文 LLM 注入 ✓
- [x] `prompts/modification.md` — 新增 `{ocr_context}` 占位符
- [x] `modify_jrxml` + `generate` 节点注入 OCR 上下文
- [x] OCR 上下文包含: 结构化字段、全部文本元素(含坐标)、批注检测结果
---
## 阶段六:分层精确生成 (v5) ✓
### 16. 布局 Schema 提取 ✓
- [x] `backend/layout_analyzer.py` — 新增 `extract_layout_schema()` 函数(+107 行)
- [x] X 坐标聚类列检测(avg_width * 0.5 阈值)
- [x] 区域分类:标题/表头/数据/表尾(启发式算法)
- [x] `schema_text` 紧凑中文描述(列定义 + 区域 + 宽度分类)
- [x] 空行/单行/双行边界情况处理
- [x] 单元测试: `tests/test_layered_generation.py::TestExtractLayoutSchema` (9 tests)
### 17. 3 阶段生成管线 ✓
- [x] Phase 1: `generate_skeleton` — 压缩布局 schema → 骨架 JRXML (`$F{field_N}` 占位)
- [x] Phase 2: `refine_layout` — 采样坐标(表头+首行数据+末行)→ 像素级位置精调
- [x] Phase 3: `map_fields` — OCR 字段名 → 替换占位符为真实字段名
- [x] 中间阶段跳过验证(仅最终 mapped 结果进入 validate 循环)
- [x] 流式输出支持(每阶段逐字生成)
- [x] 单元测试: `tests/test_layered_generation.py::TestIntegration` (4 tests)
### 18. 路由与状态 ✓
- [x] `agent/graph.py` — 新增 `route_after_retrieve()` 条件路由
- [x] `layout_schema.total_rows > 0` → 3 阶段,否则 → 原有 1-shot
- [x] `agent/state.py` — 新增 `layout_schema: dict``ocr_elements: list`
- [x] 会话持久化支持(`save_session_node` / `load_session_node`
- [x] 文本请求和其他意图零行为变更
- [x] 单元测试: `tests/test_layered_generation.py::TestRouting` (4 tests)
### 19. Prompt 模板 ✓
- [x] `prompts/skeleton_generation.md` — 骨架生成 prompt
- [x] `prompts/refine_layout.md` — 布局精调 prompt
- [x] `prompts/field_mapping.md` — 字段映射 prompt
- [x] `prompts/loader.py` — 注册 3 个新模板(热重载)
### 20. UI 集成 ✓
- [x] `app.py` — 上传 A4 图片时自动调用 `extract_layout_schema()`
- [x] 新增节点标签:`🏗 生成骨架` / `📐 精调布局` / `🏷 映射字段`
- [x] 3 个新节点的详情渲染
---
阶段一立即可做,无外部依赖。阶段二是主要工作量。阶段三是收尾。阶段四是可观测性基础。阶段五是 OCR 智能增强和用户体验改进。阶段六解决 A4 报表图片 OCR 元素过多(数百个)导致 LLM prompt 超长的问题。
+115
View File
@@ -0,0 +1,115 @@
"""数据源模式解析模块。
默认使用 $P{xxx} 参数模式;用户可选择 JDBC 直连模式。
"""
import json
import os
import re
from typing import Optional
from dotenv import load_dotenv
from agent.state import AgentState
load_dotenv()
def resolve_datasource_mode(state: AgentState) -> str:
"""返回数据源模式: "parameter""jdbc"
优先读取 state 中已设定的模式,否则根据用户输入检测。
"""
existing = state.get("datasource_mode", "")
if existing in ("parameter", "jdbc"):
return existing
user_input = state.get("user_input", "")
if _detect_jdbc_intent(user_input):
return "jdbc"
return "parameter"
def _detect_jdbc_intent(user_input: str) -> bool:
"""检测用户是否想要 JDBC 直连数据库模式。"""
patterns = [
r"(直连|直连数据库|数据库直连)",
r"(从|在)(数据库|DB|MySQL|PostgreSQL|Oracle|SQL Server)\w*",
r"(jdbc|JDBC)",
r"(连接|连)(数据库|DB)",
r"(查询|select|SELECT)\s",
]
for pat in patterns:
if re.search(pat, user_input):
return True
return False
def _sanitize_url(url: str) -> str:
"""剥离 JDBC URL 中的 user:password@ 片段,防止泄露到 LLM prompt。"""
return re.sub(r"://[^@]*@", "://***:***@", url)
def build_datasource_context(mode: str, kb_fields: list, db_config: Optional[dict] = None) -> str:
"""构建数据源上下文字符串,注入生成 prompt。"""
if mode == "jdbc":
if not db_config or not db_config.get("url"):
return (
"[数据源模式: JDBC]\n"
"⚠ 用户想要 JDBC 直连模式,但尚未配置数据库连接信息。\n"
"请先生成带 $P{xxx} 参数占位符的 JRXML,并提醒用户配置 JDBC 连接。"
)
safe_url = _sanitize_url(db_config.get("url", ""))
return (
"[数据源模式: JDBC]\n"
f"连接URL: {safe_url}\n"
f"驱动: {db_config.get('driver', '')}\n"
"请使用 <queryString><![CDATA[...]]></queryString> 中的 SQL 查询。"
)
# parameter mode
if kb_fields:
field_list = "\n".join(
f"| {f['name']} | {f.get('description', '')} | {f.get('type', 'java.lang.String')} |"
for f in kb_fields
)
return (
"[数据源模式: 参数]\n"
"使用 $P{xxx} 参数模式,以下为可用参数:\n"
f"| 参数名 | 含义 | 类型 |\n|---|---|---|\n{field_list}"
)
return "[数据源模式: 参数]\n使用 $P{xxx} 参数模式生成 JRXML。"
def configure_jdbc(state: AgentState, url: str = "", driver: str = "",
username: str = "", password: str = "") -> dict:
"""配置 JDBC 连接并返回更新字段。
注意:db_config 会被存入 AgentState 并持久化到会话文件。
生产环境应使用外部密钥管理服务,避免明文存储密码。
"""
return {
"datasource_mode": "jdbc",
"db_config": {
"url": url,
"driver": driver or "com.mysql.cj.jdbc.Driver",
"username": username,
"password": password,
},
}
def ask_db_config(state: AgentState) -> Optional[str]:
"""如果用户选了 JDBC 模式但未配置 DB 连接,返回反问消息。"""
mode = resolve_datasource_mode(state)
if mode == "jdbc":
db_config = state.get("db_config", {})
if not db_config or not db_config.get("url"):
return (
"您选择了数据库直连模式,请提供以下信息:\n"
"1. JDBC URL(如 jdbc:mysql://localhost:3306/dbname\n"
"2. 数据库用户名\n"
"3. 数据库密码\n"
"4. 驱动类名(可选,默认 com.mysql.cj.jdbc.Driver"
)
return None
+10 -1
View File
@@ -120,6 +120,9 @@ def route_after_save(state: AgentState) -> Literal["validate", "finalize"]:
intent = state.get("intent", "")
if intent in ("preview_report", "export_pdf", "export_jrxml"):
return "finalize"
# JRXML 为空时跳过验证/修正循环(生成失败等场景)
if not state.get("current_jrxml", "").strip():
return "finalize"
return "validate"
@@ -127,6 +130,12 @@ def route_after_save(state: AgentState) -> Literal["validate", "finalize"]:
def route_after_validate(state: AgentState) -> Literal["finalize", "explain_error"]:
if state.get("status") == "pass":
return "finalize"
# JRXML 为空时跳过 explain→correct 修正循环
if not state.get("current_jrxml", "").strip():
return "finalize"
# 验证服务不可用时跳过修正循环,避免对网络错误进行无效修正
if state.get("service_unavailable"):
return "finalize"
return "explain_error"
@@ -256,7 +265,7 @@ def build_graph(on_node_start=None) -> StateGraph:
workflow.add_conditional_edges(
"save_session",
route_after_save,
{"validate": "validate"},
{"validate": "validate", "finalize": "finalize"},
)
# ---- 验证 → 修正循环 ----
+399
View File
@@ -0,0 +1,399 @@
"""JRXML 窗口化拆解与重组工具。
用于 3 阶段生成管道的 refine_layout 和 map_fields 节点:
- 将大段 JRXML 按 band 拆解为独立窗口
- 每个窗口独立发送给 LLM 进行坐标精调
- 重组所有窗口 + 校验元素完整性
调用者: agent/nodes.py (refine_layout, map_fields)
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Optional
import defusedxml.ElementTree as ET
from backend.logger import get_logger
_windower_log = get_logger("jrxml.windower")
# 需要按 section 拆解的 band 容器标签
_SECTION_TAGS = {
"title", "pageHeader", "columnHeader", "detail", "columnFooter",
"pageFooter", "lastPageFooter", "summary", "noData", "background",
}
# 不发给 LLM 的 header 元素(原样保留)
_HEADER_TAGS = {
"property", "propertyExpression", "import", "template", "reportFont",
"style", "subDataset", "scriptlet", "parameter", "queryString",
"field", "sortField", "variable", "filterExpression", "group",
}
@dataclass
class BandInfo:
"""单个 band 的拆解信息。"""
section_name: str # 所属 section 名,如 "title", "detail"
band_index: int # 在该 section 中的序号(0-based
band_xml: str # 完整 <band ...>...</band> 原始 XML
element_count: int # textField + staticText 数量
char_length: int # 字符数
@property
def label(self) -> str:
"""用于日志和 prompt 的标识。"""
if self.band_index > 0:
return f"{self.section_name}_band{self.band_index}"
return self.section_name
@dataclass
class JRXMLParts:
"""JRXML 拆解结果。"""
declaration: str # <?xml version="1.0"?>(如有)
root_open: str # <jasperReport ...>
header_xml: str # fields/params/queryString 等(不发给 LLM
bands: list[BandInfo] # 按出现顺序
footer: str # </jasperReport>
@property
def band_count(self) -> int:
return len(self.bands)
@property
def total_elements(self) -> int:
return sum(b.element_count for b in self.bands)
# ── 拆解 ──────────────────────────────────────────────────────────
def decompose_jrxml(jrxml: str) -> Optional[JRXMLParts]:
"""将 JRXML 字符串拆解为 header + bands + footer 三部分。
使用 defusedxml.ElementTree 进行安全解析。
返回 None 表示解析失败。
"""
try:
root = ET.fromstring(jrxml)
except ET.ParseError as e:
_windower_log.error("JRXML 解析失败: %s", e)
return None
tag = _local_tag(root.tag)
if tag != "jasperReport":
_windower_log.error("根元素不是 jasperReport: %s", tag)
return None
# 提取 XML 声明
declaration = ""
if jrxml.strip().startswith("<?xml"):
decl_end = jrxml.find("?>")
if decl_end != -1:
declaration = jrxml[:decl_end + 2]
# 提取根元素属性来重建 root_open
root_open = _build_root_open(jrxml, root)
# 分离 header 子元素和 section 子元素
header_children = []
section_children = [] # (section_tag, child_elem)
for child in root:
child_tag = _local_tag(child.tag)
if child_tag in _HEADER_TAGS:
header_children.append(child)
elif child_tag in _SECTION_TAGS:
section_children.append((child_tag, child))
# 构建 header_xml:序列化所有 header 子元素
header_parts = []
for child in header_children:
header_parts.append(_elem_to_string(child))
header_xml = "\n".join(header_parts)
# 提取 bands:每个 section 内可能有多个 <band>
bands = []
for sec_tag, sec_elem in section_children:
for bi, band_elem in enumerate(sec_elem):
band_local = _local_tag(band_elem.tag)
if band_local != "band":
continue
band_xml = _elem_to_string(band_elem)
ec = _count_elements_in_text(band_xml)
bands.append(BandInfo(
section_name=sec_tag,
band_index=bi,
band_xml=band_xml,
element_count=ec,
char_length=len(band_xml),
))
# 提取 footer</jasperReport> 闭合标签
footer = _extract_footer(jrxml)
parts = JRXMLParts(
declaration=declaration,
root_open=root_open,
header_xml=header_xml,
bands=bands,
footer=footer,
)
_windower_log.info(
"JRXML 拆解完成: %d bands, %d 个元素, header %d 字符",
len(bands), parts.total_elements, len(header_xml),
)
return parts
# ── 窗口切分 ──────────────────────────────────────────────────────
# 安全的元素边界:在这些闭合标签后切分
_SAFE_SPLIT_CLOSING = re.compile(
r"</(?:[\w:]+:)?(?:textField|staticText|line|rectangle|ellipse|image|"
r"frame|subreport|elementGroup|break|componentElement)>\s*"
)
def split_band_into_windows(band: BandInfo, max_chars: int = 4000) -> list[str]:
"""将一个 band 的 XML 在元素边界处切分为多个窗口。
每个窗口是合法的 XML 片段(完整的 <band>...</band>),
大小不超过 max_chars。
"""
if band.char_length <= max_chars:
return [band.band_xml]
inner = _extract_band_inner(band.band_xml)
if not inner:
return [band.band_xml]
segments = _split_at_boundaries(inner, _SAFE_SPLIT_CLOSING)
if len(segments) <= 1:
return [band.band_xml]
windows = _greedy_aggregate(segments, band.band_xml, max_chars)
return windows
# ── 重组 ──────────────────────────────────────────────────────────
def _recalc_band_height(band_xml: str, margin: int = 20) -> str:
"""根据波段内所有子元素的 y + height 重新计算波段 height。"""
max_bottom = 0
for m in re.finditer(r'<reportElement\b([^>]*)/>', band_xml):
attrs = m.group(1)
ym = re.search(r'\sy\s*=\s*"(\d+)"', attrs)
hm = re.search(r'\sheight\s*=\s*"(\d+)"', attrs)
if ym and hm:
bottom = int(ym.group(1)) + int(hm.group(1))
if bottom > max_bottom:
max_bottom = bottom
if max_bottom == 0:
return band_xml
new_height = max_bottom + margin
return re.sub(
r'(<band\b[^>]*\sheight\s*=\s*)"(\d+)"',
rf'\g<1>"{new_height}"',
band_xml,
count=1,
)
def reassemble_band_windows(modified_windows: list[str]) -> str:
"""将多个窗口的修改结果重新合并为一个 band XML。
策略:取第一个窗口的开头(band 标签)和最后一个窗口的结尾(/band 标签),
中间拼接所有窗口内部的元素内容。
"""
if len(modified_windows) == 1:
return _recalc_band_height(modified_windows[0])
first = modified_windows[0]
band_open_end = first.find(">")
if band_open_end == -1:
return _recalc_band_height("\n".join(modified_windows))
band_open = first[:band_open_end + 1]
last = modified_windows[-1]
band_close = _extract_band_close(last)
inner_parts = []
for win in modified_windows:
inner = _extract_band_inner(win)
if inner:
inner_parts.append(inner)
return _recalc_band_height(band_open + "\n" + "\n".join(inner_parts) + "\n" + band_close)
def reassemble_jrxml(parts: JRXMLParts, modified_bands: dict[str, str]) -> str:
"""将修改后的 bands 与 header/footer 重新组装为完整 JRXML。
modified_bands 的 key 格式为 "{section_name}_band{index}""{section_name}"index=0 时)。
"""
result = []
if parts.declaration:
result.append(parts.declaration)
result.append(parts.root_open)
if parts.header_xml.strip():
result.append(parts.header_xml)
current_section = None
for band in parts.bands:
if band.section_name != current_section:
if current_section is not None:
result.append(f"</{current_section}>")
current_section = band.section_name
result.append(f"<{current_section}>")
modified = modified_bands.get(band.label, band.band_xml)
result.append(modified)
if current_section is not None:
result.append(f"</{current_section}>")
result.append(parts.footer)
return "\n".join(result)
# ── 元素计数与校验 ────────────────────────────────────────────────
_ELEMENT_RE = re.compile(r"<(?:[\w:]+:)?(textField|staticText|field)\b", re.IGNORECASE)
def count_elements(jrxml: str) -> int:
"""正则计数 JRXML 中的 textField + staticText + field 声明。"""
return len(_ELEMENT_RE.findall(jrxml))
def validate_element_count(original: str, modified: str, stage: str) -> dict:
"""校验修改前后的元素数变化。
返回:
{"ok": bool, "original": int, "modified": int, "change_pct": float}
变化 > 10% 时 ok=False,调用方应回退。
"""
orig = count_elements(original)
mod = count_elements(modified)
if orig == 0:
return {"ok": True, "original": 0, "modified": mod, "change_pct": 0}
change = abs(mod - orig) / orig
ok = change <= 0.10
if not ok:
_windower_log.error(
"%s 元素数变化过大: %d%d (%.1f%%)",
stage, orig, mod, change * 100,
)
elif change > 0.05:
_windower_log.warning(
"%s 元素数有差异: %d%d (%.1f%%)",
stage, orig, mod, change * 100,
)
return {"ok": ok, "original": orig, "modified": mod, "change_pct": round(change, 4)}
# ── 内部工具函数 ──────────────────────────────────────────────────
def _local_tag(tag: str) -> str:
"""去除 XML 命名空间前缀。"""
return tag.split("}")[-1] if "}" in tag else tag
def _elem_to_string(elem: ET.Element) -> str:
"""将 ElementTree 元素序列化为字符串(使用 defusedxml 的 tostring)。"""
raw = ET.tostring(elem, encoding="unicode")
return raw.strip()
def _build_root_open(jrxml: str, root: ET.Element) -> str:
"""从原始文本重建 <jasperReport ...> 开头标签。"""
m = re.search(r"<jasperReport\b[^>]*>", jrxml, re.IGNORECASE)
if m:
return m.group(0)
attrs = []
for k, v in root.attrib.items():
attrs.append(f'{k}="{v}"')
return "<jasperReport " + " ".join(attrs) + ">"
def _extract_footer(jrxml: str) -> str:
"""提取 </jasperReport> 闭合标签。"""
m = re.search(r"</(?:[\w:]+:)?jasperReport>\s*$", jrxml, re.IGNORECASE)
if m:
return m.group(0).rstrip()
return "</jasperReport>"
_BAND_CLOSE_RE = re.compile(r"</(?:[\w:]+:)?band>\s*$", re.IGNORECASE)
def _extract_band_close(band_xml: str) -> str:
"""提取 band 的闭合标签(兼容命名空间前缀),如 '</ns0:band>''</band>'"""
m = _BAND_CLOSE_RE.search(band_xml)
return m.group(0).rstrip() if m else "</band>"
def _extract_band_inner(band_xml: str) -> str:
"""提取 <band ...> 和 </ns0:band> 之间的内容(兼容命名空间前缀)。"""
tag_end = band_xml.find(">")
if tag_end == -1:
return ""
close_m = _BAND_CLOSE_RE.search(band_xml)
if not close_m:
return band_xml[tag_end + 1:].strip()
return band_xml[tag_end + 1:close_m.start()].strip()
def _split_at_boundaries(text: str, boundary_re: re.Pattern) -> list[str]:
"""在正则匹配的闭合标签处切分文本。
返回切分后的片段列表(分隔符附加到前一个片段末尾)。
"""
segments = []
last_end = 0
for m in boundary_re.finditer(text):
end = m.end()
segments.append(text[last_end:end])
last_end = end
if last_end < len(text):
segments.append(text[last_end:])
elif not segments:
segments.append(text)
return segments
def _greedy_aggregate(segments: list[str], band_xml: str, max_chars: int) -> list[str]:
"""贪心聚合:将片段组合成不超过 max_chars 的窗口。
每个窗口包上 <band ...> 和 </band> 标签。
"""
tag_end = band_xml.find(">")
band_open = band_xml[:tag_end + 1] if tag_end != -1 else "<band>"
band_close = _extract_band_close(band_xml)
overhead = len(band_open) + len(band_close) + 1 # +1 for \n
windows = []
current = []
current_len = overhead
for seg in segments:
seg_len = len(seg)
if current and current_len + seg_len > max_chars:
windows.append(band_open + "\n" + "".join(current) + "\n" + band_close)
current = [seg]
current_len = overhead + seg_len
else:
current.append(seg)
current_len += seg_len
if current:
windows.append(band_open + "\n" + "".join(current) + "\n" + band_close)
return windows
def _count_elements_in_text(xml_text: str) -> int:
"""统计 XML 文本中的 textField + staticText 数量。"""
return len(_ELEMENT_RE.findall(xml_text))
+943 -112
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -51,3 +51,14 @@ class AgentState(TypedDict, total=False):
# 需求9:分层精确生成
layout_schema: dict # extract_layout_schema() 输出,列+区域结构
ocr_elements: list # OCR 原始行数据(用于阶段二坐标采样)
# 需求10:多租户知识库
kb_id: str # 当前会话绑定的知识库 ID
kb_fields: list # KB 提取的字段定义 [{name, description, type, required}]
kb_field_mapping: dict # OCR 字段 → KB 字段映射 {"工单号": "billNo", ...}
uploaded_template_jrxml: str # 对话中上传的 JRXML 模板原文
uploaded_template_params: list # 解析出的参数 [{name, type}]
kb_template_jrxml: str # 从 KB 检索到的模板 JRXML
kb_template_name: str # 检索到的模板名称
datasource_mode: str # "parameter" 或 "jdbc"
db_config: dict # JDBC 连接配置
+369 -26
View File
@@ -16,6 +16,7 @@ Usage:
import asyncio
import base64
import contextvars
import json
import mimetypes
import os
@@ -29,7 +30,7 @@ from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse
@@ -49,6 +50,13 @@ from backend.session import (
)
from backend.file_parser import parse_file
from backend.layout_analyzer import analyze_layout, extract_layout_schema
from backend.kb_manager import (
create_user, list_users, get_user, delete_user,
create_kb, list_kbs, get_kb, update_kb_meta, delete_kb,
get_kb_raw_dir,
)
from backend.kb_parser import parse_jrxml_fields, build_kb_from_files
from backend.kb_searcher import search_kb, search_templates_in_kb
# ─────────────────────────────────────────────
# 常量(从 app.py 迁移)
@@ -96,22 +104,32 @@ SKIP_NODES = {"load_session", "process_input", "manage_context",
_api_log = get_logger("api")
UPLOADS_DIR = Path(os.getenv("UPLOADS_DIR", "./uploads"))
MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50 MB
def _check_session_id(session_id: str) -> None:
"""校验 session_id 合法性(防路径穿越),非法时抛出 HTTPException(400)。"""
from backend.session import validate_session_id
if not validate_session_id(session_id):
raise HTTPException(status_code=400, detail=f"Invalid session_id: {session_id!r}")
# ─────────────────────────────────────────────
# 图编译(全局单例,带 node_start 回调)
# ─────────────────────────────────────────────
# 当前请求的事件队列(单个用户桌面应用,无并发问题
# 当前请求的事件队列(单个用户桌面应用)
_current_event_queue: Optional[queue.Queue] = None
_step_counter: contextvars.ContextVar[int] = contextvars.ContextVar('_step_counter', default=0)
def _on_node_start(node_name: str):
"""全局 node_start 回调 — 将事件推入当前请求的事件队列。"""
q = _current_event_queue
if q is not None:
_step_counter.set(_step_counter.get() + 1)
q.put(("node_start", {
"node": node_name,
"label": NODE_LABELS.get(node_name, node_name),
"step_index": _step_counter.get(),
}))
@@ -162,10 +180,32 @@ def _extract_detail(node_name: str, node_state: dict) -> str:
def _run_graph_sync(agent_state: AgentState, event_q: queue.Queue):
"""在后台线程中运行 graph.stream(),将所有事件推入队列。"""
"""在后台线程中运行 graph.stream(),将所有事件推入队列。
graph.stream() 只产出事件,不修改传入的 agent_state。
因此需要手动收集每个节点的返回并合并到 agent_state。
"""
try:
for event in _graph.stream(agent_state, stream_mode=["updates", "custom"]):
event_q.put(event)
# 将节点更新合并到 agent_state
if isinstance(event, tuple) and len(event) == 2:
mode, data = event
if mode == "updates" and isinstance(data, dict):
for node_state in data.values():
if isinstance(node_state, dict):
agent_state.update({k: v for k, v in node_state.items() if v is not None})
# 在 graph 完成后立即保存 session,防止 SSE 流中断导致数据丢失
sid = agent_state.get("session_id", "")
if sid:
try:
save_session(sid, agent_state)
except Exception as exc:
_api_log.error("图运行中保存会话失败", extra={
"session_id": sid,
"error": str(exc),
"traceback": traceback.format_exc(),
})
event_q.put(("done", {"reason": "graph_completed"}))
except Exception as exc:
event_q.put(("error", {
@@ -174,10 +214,12 @@ def _run_graph_sync(agent_state: AgentState, event_q: queue.Queue):
}))
async def _sse_generator(agent_state: AgentState) -> str:
async def _sse_generator(agent_state: AgentState, session_id: str = "") -> str:
"""SSE 事件生成器 —— 在后台线程运行图,异步产出 SSE 字符串。"""
global _current_event_queue
_step_counter.set(0)
t_start = time.time()
event_q: queue.Queue = queue.Queue()
_current_event_queue = event_q
@@ -198,6 +240,11 @@ async def _sse_generator(agent_state: AgentState) -> str:
kind = item[0]
if kind == "done":
_current_event_queue = None
total_ms = round((time.time() - t_start) * 1000)
if session_id:
save_session(session_id, agent_state)
versions = agent_state.get("jrxml_versions", [])
last_ver = versions[-1] if versions else {}
yield _sse_line("agent_complete", {
"reason": "done",
"intent": agent_state.get("intent", ""),
@@ -205,8 +252,13 @@ async def _sse_generator(agent_state: AgentState) -> str:
"jrxml_length": len(agent_state.get("current_jrxml", "")),
"error_msg": agent_state.get("error_msg", ""),
"natural_explanation": agent_state.get("natural_explanation", ""),
"consult_answer": agent_state.get("consult_answer", ""),
"retry_count": agent_state.get("retry_count", 0),
"total_duration_ms": total_ms,
"ocr_extraction_result": agent_state.get("ocr_extraction_result", {}),
"versions": len(versions),
"has_failed_version": last_ver.get("status") == "fail" if last_ver else False,
"failed_version_index": len(versions) - 1 if last_ver.get("status") == "fail" else -1,
})
await future
return
@@ -225,13 +277,12 @@ async def _sse_generator(agent_state: AgentState) -> str:
mode, data = item
if mode == "updates":
for node_name, node_state in data.items():
if node_name not in SKIP_NODES:
detail = _extract_detail(node_name, node_state)
yield _sse_line("node_complete", {
"node": node_name,
"label": NODE_LABELS.get(node_name, node_name),
"detail": detail,
})
detail = _extract_detail(node_name, node_state)
yield _sse_line("node_complete", {
"node": node_name,
"label": NODE_LABELS.get(node_name, node_name),
"detail": detail,
})
elif mode == "custom":
cd = data
if cd.get("type") == "stream":
@@ -315,6 +366,7 @@ async def list_sessions():
@app.get("/api/sessions/{session_id}")
async def get_session(session_id: str):
_check_session_id(session_id)
data = get_session_state(session_id)
if data is None:
raise HTTPException(status_code=404, detail="会话不存在")
@@ -329,18 +381,233 @@ async def get_session(session_id: str):
@app.delete("/api/sessions/{session_id}")
async def remove_session(session_id: str):
_check_session_id(session_id)
ok = delete_session(session_id)
if not ok:
raise HTTPException(status_code=404, detail="会话不存在或已删除")
return {"status": "deleted", "session_id": session_id}
# ─────────────────────────────────────────────
# 用户管理
# ─────────────────────────────────────────────
@app.post("/api/users")
async def create_new_user(payload: dict):
name = payload.get("name", "").strip()
if not name:
raise HTTPException(status_code=400, detail="用户名不能为空")
try:
user = create_user(name)
return user
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/users")
async def list_all_users():
return {"users": list_users()}
@app.get("/api/users/{user_id}")
async def get_user_info(user_id: str):
user = get_user(user_id)
if user is None:
raise HTTPException(status_code=404, detail="用户不存在")
return user
@app.delete("/api/users/{user_id}")
async def remove_user(user_id: str):
ok = delete_user(user_id)
if not ok:
raise HTTPException(status_code=404, detail="用户不存在")
return {"status": "deleted", "user_id": user_id}
# ─────────────────────────────────────────────
# 知识库 CRUD
# ─────────────────────────────────────────────
@app.get("/api/users/{user_id}/kbs")
async def list_user_kbs(user_id: str):
return {"kbs": list_kbs(user_id)}
@app.post("/api/users/{user_id}/kbs")
async def create_user_kb(user_id: str, payload: dict):
name = payload.get("name", "").strip()
description = payload.get("description", "")
if not name:
raise HTTPException(status_code=400, detail="知识库名称不能为空")
try:
kb = create_kb(user_id, name, description)
return kb
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/kbs/{kb_id}")
async def get_kb_info(kb_id: str):
kb = get_kb(kb_id)
if kb is None:
raise HTTPException(status_code=404, detail="知识库不存在")
return kb
@app.delete("/api/kbs/{kb_id}")
async def remove_kb(kb_id: str):
ok = delete_kb(kb_id)
if not ok:
raise HTTPException(status_code=404, detail="知识库不存在")
return {"status": "deleted", "kb_id": kb_id}
# ─────────────────────────────────────────────
# 知识库文件上传
# ─────────────────────────────────────────────
@app.post("/api/kbs/{kb_id}/upload")
async def upload_to_kb(kb_id: str, file: UploadFile = File(...)):
kb = get_kb(kb_id)
if kb is None:
raise HTTPException(status_code=404, detail="知识库不存在")
raw_dir = get_kb_raw_dir(kb_id)
if raw_dir is None:
raise HTTPException(status_code=500, detail="知识库存储目录不可用")
raw_dir.mkdir(parents=True, exist_ok=True)
safe_name = Path(file.filename or "upload").name
dest = raw_dir / safe_name
content = await file.read()
if len(content) > MAX_UPLOAD_SIZE:
raise HTTPException(status_code=413, detail="文件大小超过 50MB 上限")
dest.write_bytes(content)
from backend.kb_parser import process_file_for_kb
result = process_file_for_kb(kb_id, str(dest), source_name=safe_name)
_api_log.info("KB文件上传", extra={
"kb_id": kb_id, "file": safe_name, "type": result.get("type"),
})
return {
"filename": safe_name,
"type": result.get("type", ""),
"error": result.get("error"),
}
@app.post("/api/kbs/{kb_id}/build")
async def build_kb(kb_id: str):
"""构建知识库:对已上传的文件执行 chunk → embed 管线。"""
from backend.kb_parser import build_kb_from_files as build_fn
raw_dir = get_kb_raw_dir(kb_id)
if raw_dir is None or not raw_dir.exists():
raise HTTPException(status_code=404, detail="知识库无已上传文件")
files = [str(p) for p in raw_dir.iterdir() if p.is_file()]
if not files:
raise HTTPException(status_code=400, detail="知识库无文件,请先上传")
result = build_fn(kb_id, files)
return result
@app.get("/api/kbs/{kb_id}/status")
async def kb_status(kb_id: str):
kb = get_kb(kb_id)
if kb is None:
raise HTTPException(status_code=404, detail="知识库不存在")
return {
"kb_id": kb_id,
"name": kb.get("name", ""),
"field_count": len(kb.get("fields", [])),
"template_count": len(kb.get("templates", [])),
"file_count": kb.get("file_count", 0),
"chunk_count": kb.get("chunk_count", 0),
"parse_status": kb.get("parse_status", "empty"),
"created_at": kb.get("created_at", ""),
}
@app.get("/api/kbs/{kb_id}/fields")
async def kb_fields(kb_id: str):
kb = get_kb(kb_id)
if kb is None:
raise HTTPException(status_code=404, detail="知识库不存在")
return {"fields": kb.get("fields", []), "templates": kb.get("templates", [])}
@app.get("/api/kbs/{kb_id}/search")
async def kb_search(kb_id: str, q: str = "", type: str = ""):
if not q:
raise HTTPException(status_code=400, detail="查询参数 q 不能为空")
if type == "template":
results = search_templates_in_kb(kb_id, q, k=5)
else:
ctx = search_kb(kb_id, q, k=5)
return {"query": q, "context": ctx}
return {"query": q, "results": results}
# ─────────────────────────────────────────────
# 会话-知识库绑定
# ─────────────────────────────────────────────
@app.put("/api/sessions/{session_id}/kb")
async def bind_session_kb(session_id: str, payload: dict):
_check_session_id(session_id)
kb_id = payload.get("kb_id", "").strip()
data = load_session(session_id)
if data is None:
raise HTTPException(status_code=404, detail="会话不存在")
agent_state = data.get("agent_state", {})
if kb_id:
kb = get_kb(kb_id)
if kb is None:
raise HTTPException(status_code=404, detail="知识库不存在")
agent_state["kb_id"] = kb_id
agent_state["kb_fields"] = kb.get("fields", [])
else:
agent_state.pop("kb_id", None)
agent_state.pop("kb_fields", None)
save_session(session_id, agent_state)
return {"session_id": session_id, "kb_id": kb_id or None}
@app.get("/api/sessions/{session_id}/kb")
async def get_session_kb(session_id: str):
_check_session_id(session_id)
data = load_session(session_id)
if data is None:
raise HTTPException(status_code=404, detail="会话不存在")
agent_state = data.get("agent_state", {})
kb_id = agent_state.get("kb_id", "")
result = {"kb_id": kb_id, "kb_fields": agent_state.get("kb_fields", [])}
if kb_id:
kb = get_kb(kb_id)
if kb:
result["kb_name"] = kb.get("name", "")
result["templates"] = kb.get("templates", [])
return result
# ─────────────────────────────────────────────
# 文件上传
# ─────────────────────────────────────────────
@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...), session_id: str = ""):
if session_id:
_check_session_id(session_id)
file_id = uuid.uuid4().hex[:12]
_ensure_upload_dir(session_id)
@@ -349,6 +616,9 @@ async def upload_file(file: UploadFile = File(...), session_id: str = ""):
dest = _ensure_upload_dir(session_id) / f"{file_id}_{safe_name}"
content = await file.read()
if len(content) > MAX_UPLOAD_SIZE:
raise HTTPException(status_code=413, detail="文件大小超过 50MB 上限")
dest.write_bytes(content)
content_type = file.content_type or mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
@@ -361,7 +631,7 @@ async def upload_file(file: UploadFile = File(...), session_id: str = ""):
}
_api_log.info("文件上传", extra={
"file_id": file_id, "filename": safe_name, "size": len(content),
"file_id": file_id, "file_name": safe_name, "size": len(content),
})
return {
@@ -376,20 +646,47 @@ async def upload_file(file: UploadFile = File(...), session_id: str = ""):
# 文件处理辅助
# ─────────────────────────────────────────────
def _process_files(file_ids: list[str], session_id: str) -> dict:
"""处理上传的文件:解析 → 布局分析 → 提取 schema 文本
def _parse_jrxml_file(file_path: str) -> dict:
"""解析上传的 JRXML 文件,提取模板参数和字段
Returns:
{full_prompt_prefix, uploaded_paths, layout_schema, ocr_text}
{jrxml_text, parameters: [{name, type}], fields: [{name, type}],
query: str, report_name: str, page_width: str, page_height: str}
"""
jrxml_info = parse_jrxml_fields(file_path)
try:
raw_xml = Path(file_path).read_text(encoding="utf-8")
except Exception:
raw_xml = ""
return {
"jrxml_text": raw_xml,
"parameters": jrxml_info.get("parameters", []),
"fields": jrxml_info.get("fields", []),
"query": jrxml_info.get("query", ""),
"report_name": jrxml_info.get("report_name", ""),
"page_width": jrxml_info.get("page_width", ""),
"page_height": jrxml_info.get("page_height", ""),
"error": jrxml_info.get("error"),
}
def _process_files(file_ids: list[str], session_id: str) -> dict:
"""处理上传的文件:解析 → 布局分析 → 提取 schema 文本。
JRXML 文件额外解析为模板上下文注入 agent_state。
Returns:
{full_prompt_prefix, uploaded_paths, layout_schema, ocr_text,
jrxml_template: dict | None}
"""
if not file_ids:
return {"full_prompt_prefix": "", "uploaded_paths": [],
"layout_schema": {}, "ocr_text": ""}
"layout_schema": {}, "ocr_text": "", "jrxml_template": None}
parts = []
uploaded_paths = []
layout_schema = {}
ocr_text = ""
jrxml_template = None
for fid in file_ids:
info = _file_registry.get(fid)
@@ -399,8 +696,33 @@ def _process_files(file_ids: list[str], session_id: str) -> dict:
file_path = info["path"]
uploaded_paths.append(file_path)
suffix = Path(info["filename"]).suffix.lower()
parsed = parse_file(file_path, info["filename"].rsplit(".", 1)[-1] if "." in info["filename"] else "")
# JRXML 文件 → 解析为模板
if suffix == ".jrxml":
jrxml_template = _parse_jrxml_file(file_path)
if jrxml_template.get("error"):
parts.append(f"[JRXML 模板: {info['filename']}]\n解析失败: {jrxml_template['error']}")
else:
params = jrxml_template["parameters"]
fields = jrxml_template["fields"]
param_desc = "\n".join(
f" - {p['name']} ({p.get('type', 'String')})" for p in params
) if params else " (无参数)"
field_desc = "\n".join(
f" - {f['name']} ({f.get('type', 'String')})" for f in fields
) if fields else " (无字段)"
parts.append(
f"[上传的 JRXML 模板: {jrxml_template['report_name'] or info['filename']}]\n"
f"页面尺寸: {jrxml_template['page_width']}x{jrxml_template['page_height']}\n"
f"参数列表:\n{param_desc}\n"
f"字段列表:\n{field_desc}\n"
f"SQL查询: {jrxml_template['query'] or '(无)'}\n"
f"--- XML 内容 ---\n{jrxml_template['jrxml_text']}"
)
continue
parsed = parse_file(file_path, suffix)
if parsed.get("error"):
parts.append(f"[文件: {info['filename']}]\n解析失败: {parsed['error']}")
continue
@@ -443,6 +765,7 @@ def _process_files(file_ids: list[str], session_id: str) -> dict:
"uploaded_paths": uploaded_paths,
"layout_schema": layout_schema,
"ocr_text": ocr_text,
"jrxml_template": jrxml_template,
}
@@ -460,6 +783,7 @@ async def chat(session_id: str, payload: dict):
Returns:
text/event-stream (SSE)
"""
_check_session_id(session_id)
text = payload.get("text", "").strip()
file_ids = payload.get("file_ids", [])
@@ -492,9 +816,17 @@ async def chat(session_id: str, payload: dict):
for line in file_result["ocr_text"].split("\n") if line.strip()]
if ocr_rows:
agent_state["ocr_elements"] = ocr_rows
if file_result.get("uploaded_paths"):
agent_state["uploaded_file_path"] = file_result["uploaded_paths"][0]
# ── 注入 JRXML 模板(对话中上传的模板)──
jrxml_tmpl = file_result.get("jrxml_template")
if jrxml_tmpl and not jrxml_tmpl.get("error"):
agent_state["uploaded_template_jrxml"] = jrxml_tmpl["jrxml_text"]
agent_state["uploaded_template_params"] = jrxml_tmpl["parameters"]
# ── 设置本轮输入 ──
if agent_state.get("current_jrxml") and agent_state.get("status") == "pass":
if agent_state.get("current_jrxml"):
agent_state["user_modification_request"] = full_prompt
agent_state["user_input"] = full_prompt
@@ -510,13 +842,20 @@ async def chat(session_id: str, payload: dict):
# ── 返回 SSE 流 ──
async def stream_and_save():
final_state = None
async for sse_chunk in _sse_generator(agent_state):
# 如果上传了附件,先发送处理状态
if file_ids:
yield _sse_line("node_start", {
"node": "process_attachments",
"label": "正在处理附件",
})
yield _sse_line("node_complete", {
"node": "process_attachments",
"label": "正在处理附件",
"detail": f"已解析 {len(file_ids)} 个文件",
})
async for sse_chunk in _sse_generator(agent_state, session_id):
yield sse_chunk
# 图执行完成后保存会话状态
save_session(session_id, agent_state)
return StreamingResponse(
stream_and_save(),
media_type="text/event-stream",
@@ -534,8 +873,9 @@ async def chat(session_id: str, payload: dict):
# ─────────────────────────────────────────────
@app.get("/api/sessions/{session_id}/download/latest")
async def download_latest(session_id: str):
async def download_latest(session_id: str, background_tasks: BackgroundTasks):
"""下载最新 JRXML 文件。"""
_check_session_id(session_id)
data = load_session(session_id)
if data is None:
raise HTTPException(status_code=404, detail="会话不存在")
@@ -549,6 +889,7 @@ async def download_latest(session_id: str):
encoding="utf-8")
tmp.write(jrxml)
tmp.close()
background_tasks.add_task(os.unlink, tmp.name)
return FileResponse(
tmp.name,
@@ -558,8 +899,9 @@ async def download_latest(session_id: str):
@app.get("/api/sessions/{session_id}/download/{version}")
async def download_version(session_id: str, version: int):
async def download_version(session_id: str, version: int, background_tasks: BackgroundTasks):
"""下载指定版本的 JRXML 文件。"""
_check_session_id(session_id)
data = load_session(session_id)
if data is None:
raise HTTPException(status_code=404, detail="会话不存在")
@@ -576,6 +918,7 @@ async def download_version(session_id: str, version: int):
encoding="utf-8")
tmp.write(jrxml)
tmp.close()
background_tasks.add_task(os.unlink, tmp.name)
return FileResponse(
tmp.name,
@@ -603,4 +946,4 @@ async def download_file(file_id: str):
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("API_PORT", "8000"))
uvicorn.run("api_server:app", host="0.0.0.0", port=port, reload=True)
uvicorn.run("api_server:app", host="0.0.0.0", port=port, reload=False)
-771
View File
@@ -1,771 +0,0 @@
"""Streamlit 多轮对话 UI,用于 JRXML 生成代理。
支持:
- 流式输出LLM 逐字展示
- 节点平铺展开每个处理阶段独立展示
- 完成后自动折叠节点区
- 过程总结卡片
"""
import os
import sys
os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
try:
import torchvision
except Exception:
pass
import time
from pathlib import Path
import streamlit as st
from dotenv import load_dotenv
load_dotenv()
from agent.graph import build_graph, create_initial_state
from backend.session import (
create_session,
load_session,
delete_session,
list_all_sessions,
)
from backend.logger import get_logger, set_trace_id, generate_trace_id
_app_log = get_logger("app")
st.set_page_config(
page_title="JRXML 代理",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded",
)
# 阻止 Streamlit 裸 'c' 键清除缓存,保留 Ctrl+C 复制行为
st.html("""
<script>
(function() {
const parent = window.parent.document;
parent.addEventListener('keydown', function(e) {
// 仅拦截裸 'c' Ctrl/Cmd 组合
if (e.key === 'c' && !e.ctrlKey && !e.metaKey && !e.altKey) {
const tag = parent.activeElement ? parent.activeElement.tagName : '';
if (tag !== 'INPUT' && tag !== 'TEXTAREA' && !parent.activeElement.isContentEditable) {
e.stopImmediatePropagation();
e.preventDefault();
}
}
}, true);
})();
</script>
""")
# ---- 节点名称 → 中文标签 ----
NODE_LABELS = {
"load_session": "📂 加载会话",
"process_input": "📝 记录输入",
"manage_context": "🧠 管理上下文",
"save_state_snapshot": "💾 保存快照",
"classify_intent": "🔍 识别意图",
"retrieve": "📚 检索模板",
"generate": "⚙️ 生成 JRXML",
"modify_jrxml": "🔧 修改 JRXML",
"validate": "✅ 验证",
"explain_error": "🔎 分析错误",
"correct_jrxml": "🛠 自动修正",
"finalize": "📋 完成",
"handle_consult": "💬 咨询回答",
"handle_undo": "↩ 撤销操作",
"handle_reset": "🔄 重置会话",
"save_session": "💾 保存会话",
"generate_skeleton": "🏗 生成骨架",
"refine_layout": "📐 精调布局",
"map_fields": "🏷 映射字段",
}
INTENT_LABELS = {
"initial_generation": "新建报表",
"modify_report": "修改报表",
"preview_report": "预览报表",
"export_pdf": "导出 PDF",
"export_jrxml": "下载 JRXML",
"undo_modification": "撤销修改",
"consult_question": "咨询问题",
"reset_session": "重置会话",
}
SKIP_NODES = {"load_session", "process_input", "manage_context",
"save_state_snapshot", "save_session"}
def _render_jrxml(jrxml: str, max_lines: int = 30):
"""展示 JRXML 代码(折叠、限行)。"""
lines = jrxml.strip().split("\n")
preview = "\n".join(lines[:max_lines])
if len(lines) > max_lines:
preview += f"\n... (共 {len(lines)} 行)"
st.code(preview, language="xml")
# ---- 共享文件上传处理 ----
def _process_uploaded_file(uploaded_file, suffix: str) -> dict:
"""处理单个上传文件:保存临时文件、解析、布局分析。
返回: {"name": str, "text": str, "type": str, "tmp_path": str|None}
"""
import tempfile
from backend.file_parser import parse_file
from backend.layout_analyzer import analyze_layout
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
tmp.write(uploaded_file.getvalue())
tmp_path = tmp.name
result = parse_file(tmp_path, suffix)
parsed_text = result["text"]
parsed_type = result["file_type"]
# 对图片/PDF 进行 A4 模板布局分析
if suffix in (".png", ".jpg", ".jpeg", ".bmp", ".webp", ".pdf"):
layout = analyze_layout(tmp_path)
tt = layout.get("template_type", "unknown")
current_jrxml = st.session_state.agent_state.get("current_jrxml", "")
if tt == "full_a4":
parsed_text = layout["description"]
parsed_type = "a4_template"
# 存储布局 schema 供分层精确生成使用
from backend.layout_analyzer import extract_layout_schema
schema = extract_layout_schema(layout)
st.session_state.agent_state["layout_schema"] = schema
st.session_state.agent_state["ocr_elements"] = layout.get("rows", [])
elif tt == "partial_rows":
parsed_type = "a4_partial"
if current_jrxml.strip():
from backend.layout_analyzer import match_rows_to_jrxml
match = match_rows_to_jrxml(layout, current_jrxml)
parsed_text = (
f"[行片段修改] 上传图片包含 {layout['total_rows']} 行,"
f"视为 A4 报表的一部分。\n\n"
f"{match['description']}\n\n"
f"--- 行结构 ---\n{layout['description']}"
)
else:
parsed_text = layout["description"]
else:
has_ocr = result.get("method") not in ("metadata_only", None)
img_w, img_h = layout["image_size"]
ratio = layout["aspect_ratio"]
if has_ocr:
parsed_text = (
f"[图片上传] 尺寸 {img_w}x{img_h}px, 比例 {ratio}"
f"未检测到 A4 报表结构,图片将被视为参考样式。\n"
f"请根据用户的文字描述生成报表。"
)
else:
parsed_text = (
f"[图片上传] 尺寸 {img_w}x{img_h}px, 比例 {ratio}\n"
f"⚠ OCR 引擎未安装,无法识别图片中的文字内容。\n"
f"请严格根据用户的文字描述来推断图片中的报表需求。\n"
f"(提示:如需图片文字识别,请运行 pip install paddleocr"
)
parsed_type = "image_reference"
elif suffix in (".pdf", ".docx", ".xlsx", ".xls", ".doc"):
parsed_type = suffix.lstrip(".")
keep_temp = (
suffix in (".png", ".jpg", ".jpeg", ".bmp", ".webp")
and result.get("method") not in ("metadata_only", None)
)
return {
"name": uploaded_file.name,
"text": parsed_text,
"type": parsed_type,
"tmp_path": tmp_path if keep_temp else None,
}
# ---- URL 参数 ----
query_params = st.query_params
url_session_id = query_params.get("session_id", "")
# ---- 会话状态初始化 ----
if "messages" not in st.session_state:
st.session_state.messages = []
if "graph" not in st.session_state:
st.session_state.graph = build_graph()
if "pending_action" not in st.session_state:
st.session_state.pending_action = None
if "agent_state" not in st.session_state:
if url_session_id:
data = load_session(url_session_id)
if data and data.get("agent_state"):
st.session_state.agent_state = data["agent_state"]
st.session_state.agent_state["session_id"] = url_session_id
else:
st.session_state.agent_state = create_initial_state()
new_data = create_session(name="", agent_state=st.session_state.agent_state)
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
else:
st.session_state.agent_state = create_initial_state()
new_data = create_session(name="", agent_state=st.session_state.agent_state)
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
current_session_id = st.session_state.agent_state.get("session_id", "")
def run_agent(user_input: str):
"""运行代理图:流式渲染节点进度 + LLM 文本。"""
trace_id = generate_trace_id()
set_trace_id(trace_id)
agent_state = st.session_state.agent_state
session_id = agent_state.get("session_id", "")
_app_log.info(
"代理执行开始",
extra={
"session_id": session_id,
"trace_id": trace_id,
"user_input_preview": user_input[:200],
"user_input_length": len(user_input),
"has_jrxml": bool(agent_state.get("current_jrxml", "").strip()),
"intent": agent_state.get("intent", ""),
},
)
if agent_state.get("current_jrxml") and agent_state.get("status") == "pass":
agent_state["user_modification_request"] = user_input
agent_state["user_input"] = user_input
agent_state["retry_count"] = 0
# ---- UI 占位 ----
progress_placeholder = st.empty() # 实时节点进度
streaming_placeholder = st.empty() # 流式文本
summary_placeholder = st.empty() # 总结卡片
# 初始状态提示
progress_placeholder.info("⏳ 正在分析您的需求...")
executed_nodes: list[dict] = []
stream_text = ""
stream_active = False
final_state = None
def _render_progress(nodes: list[dict]):
"""渲染实时节点进度到占位符。"""
if not nodes:
return
lines = []
for i, node in enumerate(nodes):
icon = "" if i == len(nodes) - 1 else ""
detail = f"{node['detail']}" if node.get("detail") else ""
lines.append(f"{icon} {node['label']}{detail}")
progress_placeholder.markdown("\n\n".join(lines))
try:
for event in st.session_state.graph.stream(
agent_state, stream_mode=["updates", "custom"]
):
mode, data = event
if mode == "updates":
for node_name, node_state in data.items():
label = NODE_LABELS.get(node_name, node_name)
if node_name not in SKIP_NODES:
executed_nodes.append({
"name": node_name,
"label": label,
})
if node_name == "classify_intent":
intent = node_state.get("intent", "")
il = INTENT_LABELS.get(intent, intent)
executed_nodes[-1]["detail"] = f"意图: {il}"
elif node_name == "retrieve":
ctx = node_state.get("retrieved_context", "")
executed_nodes[-1]["detail"] = (
f"找到 {len(ctx)} 字符参考模板" if ctx else "未匹配到模板"
)
elif node_name in ("generate", "modify_jrxml", "correct_jrxml",
"generate_skeleton", "refine_layout", "map_fields"):
jrxml = node_state.get("current_jrxml", "")
executed_nodes[-1]["detail"] = f"生成 {len(jrxml)} 字符 JRXML"
elif node_name == "validate":
status = node_state.get("status", "")
if status == "pass":
executed_nodes[-1]["detail"] = "验证通过 ✓"
else:
err = node_state.get("error_msg", "")
executed_nodes[-1]["detail"] = f"验证失败: {err[:80]}"
elif node_name == "explain_error":
expl = node_state.get("natural_explanation", "")
executed_nodes[-1]["detail"] = expl[:120]
elif node_name == "handle_consult":
ans = node_state.get("consult_answer", "")
executed_nodes[-1]["detail"] = ans[:150]
final_state = node_state
# 每个节点完成后立即更新进度
_render_progress(executed_nodes)
elif mode == "custom":
cd = data
if cd.get("type") == "stream":
stream_text += cd.get("text", "")
stream_active = True
streaming_placeholder.code(stream_text, language="xml")
except Exception as e:
progress_placeholder.empty()
_app_log.error(
f"代理执行异常: {e}",
extra={"session_id": session_id, "error": str(e)},
)
st.error(f"工作流异常: {e}")
return
# ---- 清理临时占位 ----
progress_placeholder.empty()
if stream_active:
streaming_placeholder.empty()
# 清理已处理的临时文件
for p in st.session_state.get("uploaded_temp_paths", []):
try:
Path(p).unlink(missing_ok=True)
except Exception:
pass
st.session_state.uploaded_temp_paths = []
# ---- 总结卡片 ----
# 注:node_state 只含变更字段,用 agent_state(被所有节点就地修改)获取完整状态
final_state = agent_state
if final_state:
st.session_state.agent_state = final_state
intent = final_state.get("intent", "")
status = final_state.get("status", "")
with summary_placeholder.container(border=True):
if intent == "consult_question":
answer = final_state.get("consult_answer", "")
st.info(answer)
st.session_state.messages.append({
"role": "assistant", "content": answer, "type": "consult",
})
elif intent in ("undo_modification", "reset_session"):
st.success("操作已完成")
elif intent in ("preview_report", "export_pdf", "export_jrxml"):
jrxml = final_state.get("current_jrxml", "")
if jrxml:
st.success("✅ 当前报表")
_render_jrxml(jrxml)
st.session_state.messages.append({
"role": "assistant", "content": jrxml, "type": "jrxml",
})
else:
st.warning("⚠ 当前没有报表可以展示。")
elif status == "pass":
jrxml = final_state.get("current_jrxml", "")
st.success("✅ JRXML 生成成功")
st.markdown("**生成结果:**")
_render_jrxml(jrxml)
st.caption("您可以从侧边栏下载文件,或继续对话进行修改。")
st.session_state.messages.append({
"role": "assistant", "content": jrxml, "type": "jrxml",
})
st.session_state.messages.append({
"role": "assistant",
"content": "✅ JRXML 生成成功!您可以从侧边栏下载文件,或继续修改。",
"type": "success",
})
else:
jrxml = final_state.get("current_jrxml", "")
error_msg = final_state.get("error_msg", "未知错误")
explanation = final_state.get("natural_explanation", "")
retries = final_state.get("retry_count", 0)
st.error(f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML")
st.markdown(f"**错误:** {error_msg}")
if explanation:
st.markdown(f"**原因:** {explanation}")
if jrxml:
with st.expander("查看当前 JRXML"):
_render_jrxml(jrxml, max_lines=80)
st.caption("💡 下次输入修改需求时,系统会自动加载失败上下文继续修复。")
st.session_state.messages.append({
"role": "assistant",
"content": f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML。\n\n**错误:** {error_msg}\n\n💡 请直接描述修改需求,系统会自动加载失败上下文。",
"type": "error_explanation",
})
# OCR 字段提取结果展示
ocr_result = agent_state.get("ocr_extraction_result", {})
if ocr_result and ocr_result.get("ocr_available") and ocr_result.get("fields"):
with st.expander("🔍 OCR 单据字段提取结果", expanded=False):
fields = ocr_result.get("fields", [])
non_empty = [f for f in fields if f.get("field_value")]
empty = [f for f in fields if not f.get("field_value")]
if non_empty:
st.markdown("**已提取字段:**")
for f in non_empty:
method = f.get("extraction_method", "")
conf = f.get("confidence", 0)
st.markdown(
f"- **{f['field_name']}**: `{f['field_value']}` "
f"(置信度: {conf:.0%}, 方法: {method}"
)
if empty:
st.caption(
f"未提取到值的字段: {', '.join(f['field_name'] for f in empty)}"
)
st.caption(
f"共检测到 {ocr_result.get('total_elements', 0)} 个文本元素"
)
else:
st.error("未产生结果,请重试。")
_app_log.info(
"代理执行完成",
extra={
"session_id": session_id,
"intent": final_state.get("intent", ""),
"status": final_state.get("status", ""),
"jrxml_length": len(final_state.get("current_jrxml", "")),
"retry_count": final_state.get("retry_count", 0),
},
)
# ---- 侧边栏 ----
with st.sidebar:
st.title("📊 JRXML 代理")
st.markdown("通过自然语言生成 JasperReports 模板。")
st.divider()
# 会话管理
st.markdown("### 会话管理")
sessions = list_all_sessions()
session_options = {}
for s in sessions:
sid = s["session_id"]
name = s.get("session_name", sid)
updated = s.get("updated_at", "")[:16]
session_options[f"{name} ({updated})"] = sid
selected_label = None
for label, sid in session_options.items():
if sid == current_session_id:
selected_label = label
break
selected = st.selectbox(
"切换会话",
options=list(session_options.keys()),
index=list(session_options.keys()).index(selected_label) if selected_label else 0,
key="session_selector",
)
if selected and session_options.get(selected) != current_session_id:
new_sid = session_options[selected]
if st.session_state.get("_last_switched_to") == new_sid:
# 防止同一会话重复切换导致的无限 rerun 循环
st.session_state._last_switched_to = ""
else:
data = load_session(new_sid)
if data and data.get("agent_state"):
_app_log.info(
"切换会话",
extra={"from_session": current_session_id, "to_session": new_sid},
)
data["agent_state"]["session_id"] = new_sid
st.session_state.agent_state = data["agent_state"]
st.session_state.messages = []
st.session_state._last_switched_to = new_sid
st.rerun()
col1, col2 = st.columns(2)
with col1:
if st.button(" 新建", use_container_width=True):
new_data = create_session(name="", agent_state=create_initial_state())
_app_log.info(
"新建会话",
extra={"session_id": new_data["session_id"]},
)
st.session_state.agent_state = create_initial_state()
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
st.session_state.messages = []
st.rerun()
with col2:
if st.button("🗑 删除", use_container_width=True):
if current_session_id:
_app_log.info(
"删除会话",
extra={"session_id": current_session_id},
)
delete_session(current_session_id)
st.session_state.agent_state = create_initial_state()
new_data = create_session(name="", agent_state=st.session_state.agent_state)
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
st.session_state.messages = []
st.rerun()
current_name = st.session_state.agent_state.get("session_name", "")
st.caption(f"当前: {current_name} (`{current_session_id}`)")
st.divider()
st.markdown("### 快捷操作")
has_jrxml = bool(st.session_state.agent_state.get("current_jrxml", "").strip())
has_history = bool(st.session_state.agent_state.get("history_states", []))
qcol1, qcol2 = st.columns(2)
with qcol1:
if st.button("👁 预览", use_container_width=True, disabled=not has_jrxml):
with st.spinner("正在准备预览..."):
run_agent("预览报表")
st.rerun()
with qcol2:
if st.button("↩ 撤销", use_container_width=True, disabled=not has_history):
with st.spinner("正在撤销..."):
run_agent("撤销上一步修改")
st.rerun()
if st.button("🔄 重置会话", use_container_width=True):
with st.spinner("正在重置..."):
run_agent("重新来,清空当前报表")
st.rerun()
st.divider()
st.markdown("### 上传文件")
st.caption("支持图片 (OCR)、PDF、Word、文本文件。内容将附加到您的下一条消息中。")
if "uploaded_files" not in st.session_state:
st.session_state.uploaded_files = [] # [{name, text, type}]
if "uploaded_temp_paths" not in st.session_state:
st.session_state.uploaded_temp_paths = [] # 待清理的临时文件路径
uploaded = st.file_uploader(
"选择文件",
type=["png", "jpg", "jpeg", "bmp", "webp", "pdf", "docx", "xlsx", "xls", "doc",
"txt", "csv", "json", "xml"],
accept_multiple_files=True,
key="file_uploader",
label_visibility="collapsed",
)
if uploaded:
for uf in uploaded:
# 去重
if any(f["name"] == uf.name for f in st.session_state.uploaded_files):
continue
suffix = Path(uf.name).suffix.lower()
result = _process_uploaded_file(uf, suffix)
if result["text"]:
st.session_state.uploaded_files.append({
"name": result["name"],
"text": result["text"],
"type": result["type"],
})
tmp_path = result["tmp_path"]
if tmp_path:
st.session_state.agent_state["uploaded_file_path"] = tmp_path
st.session_state.uploaded_temp_paths.append(tmp_path)
if st.session_state.uploaded_files:
for i, f in enumerate(st.session_state.uploaded_files):
cols = st.columns([5, 1])
with cols[0]:
st.caption(f"📎 {f['name']} ({f['type']}, {len(f['text'])} 字符)")
with cols[1]:
if st.button("", key=f"rm_uf_{i}", help="移除"):
st.session_state.uploaded_files.pop(i)
st.rerun()
st.divider()
st.markdown("### 配置")
llm_backend = os.getenv("LLM_BACKEND", "cloud")
llm_model = os.getenv("LLM_MODEL", os.getenv("LOCAL_LLM_MODEL", "gpt-4o"))
st.caption(f"大语言模型: {llm_backend} / {llm_model}")
st.caption(f"最大重试次数: {os.getenv('MAX_RETRY', '3')}")
st.caption(f"验证服务: {os.getenv('VALIDATION_SERVICE_URL', 'http://localhost:8001/validate')}")
st.divider()
st.markdown("### 下载")
final = st.session_state.agent_state.get("final_jrxml", "")
versions = st.session_state.agent_state.get("jrxml_versions", [])
if final:
st.download_button(
label="📥 下载最新 JRXML",
data=final,
file_name="report.jrxml",
mime="application/xml",
use_container_width=True,
)
if versions:
with st.expander("📋 历史版本", expanded=False):
for i, v in enumerate(reversed(versions)):
ts = v.get("ts", "")[:16]
label = v.get("label", "版本")
status = v.get("status", "")
icon = "" if status == "pass" else ""
dl_label = f"{icon} v{len(versions)-i}{label} ({ts})"
st.download_button(
label=dl_label,
data=v.get("jrxml", ""),
file_name=f"report_v{len(versions)-i}.jrxml",
mime="application/xml",
use_container_width=True,
key=f"dl_v{i}",
)
# ---- 标题 ----
st.title("📝 JRXML 报表生成器")
st.caption("用自然语言描述您的报表需求,我将逐步生成可用的 JRXML 模板。")
# ---- 聊天历史 ----
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
if msg.get("type") == "jrxml":
with st.expander("查看生成的 JRXML", expanded=False):
st.code(msg["content"], language="xml")
elif msg.get("type") == "error_explanation":
st.warning(msg["content"])
elif msg.get("type") == "success":
st.success(msg["content"])
elif msg.get("type") == "consult":
st.info(msg["content"])
else:
st.markdown(msg["content"])
# ---- 聊天输入(支持粘贴/拖拽文件) ----
from st_multimodal_chatinput import multimodal_chatinput
import base64
import io
from pathlib import Path as _Path
# MIME type → 文件扩展名映射(用于剪贴板粘贴无扩展名的文件)
MIME_TO_EXT = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/bmp": ".bmp",
"image/webp": ".webp",
"application/pdf": ".pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"application/vnd.ms-excel": ".xls",
"application/msword": ".doc",
"text/plain": ".txt",
"text/csv": ".csv",
"application/json": ".json",
"text/xml": ".xml",
}
chat_result = multimodal_chatinput()
if chat_result:
prompt = (chat_result.get("textInput") or "").strip()
chat_files = chat_result.get("uploadedFiles") or []
# 处理聊天中上传/粘贴的文件
uploaded_texts = []
uploaded_files_info = []
# 先收集侧边栏已上传的文件
if st.session_state.get("uploaded_files"):
for f in st.session_state.uploaded_files:
uploaded_texts.append(f"[上传文件: {f['name']}]\n{f['text']}")
uploaded_files_info.append({"name": f["name"], "type": f["type"], "length": len(f["text"])})
st.session_state.uploaded_files = []
# 处理聊天中的文件
class _Base64File:
"""包装 base64 文件为类 UploadedFile 接口。"""
def __init__(self, name, data_bytes):
self.name = name
self._data = data_bytes
def getvalue(self):
return self._data
for cf in chat_files:
name = cf.get("name", "clipboard_file")
mime = cf.get("type", "")
content_b64 = cf.get("content", "")
if not content_b64:
continue
try:
data = base64.b64decode(content_b64)
except Exception:
continue
suffix = _Path(name).suffix.lower()
if not suffix and mime in MIME_TO_EXT:
suffix = MIME_TO_EXT[mime]
name = f"{_Path(name).stem}{suffix}"
wrapper = _Base64File(name, data)
result = _process_uploaded_file(wrapper, suffix)
if result["text"]:
uploaded_texts.append(f"[上传文件: {result['name']}]\n{result['text']}")
uploaded_files_info.append({"name": result["name"], "type": result["type"], "length": len(result["text"])})
tmp_path = result["tmp_path"]
if tmp_path:
st.session_state.agent_state["uploaded_file_path"] = tmp_path
st.session_state.uploaded_temp_paths.append(tmp_path)
if prompt or uploaded_texts:
if uploaded_texts:
full_prompt = "\n\n".join(uploaded_texts)
if prompt:
full_prompt += "\n\n---\n用户需求:\n" + prompt
else:
full_prompt = prompt
displayed_prompt = prompt or "(已上传文件,未输入文字)"
_app_log.info(
"收到用户输入",
extra={
"session_id": current_session_id,
"prompt_preview": displayed_prompt[:200],
"prompt_length": len(full_prompt),
"has_uploaded_files": bool(uploaded_files_info),
"uploaded_files": uploaded_files_info,
},
)
st.session_state.messages.append({"role": "user", "content": displayed_prompt})
with st.chat_message("user"):
st.markdown(displayed_prompt)
run_agent(full_prompt)
st.rerun()
+136
View File
@@ -0,0 +1,136 @@
"""OCR 字段 → KB 字段匹配模块。
两阶段匹配
1. Embedding 粗筛相似度 top-3
2. LLM 精确确认
返回映射: {"工单号": "billNo", "客户名称": "customerName", ...}
"""
import json
import os
from typing import Optional
from dotenv import load_dotenv
from backend.logger import get_logger
load_dotenv()
_match_log = get_logger("field_matcher")
def _embed(text: str) -> list:
"""获取文本的向量嵌入。"""
from backend.rag_adapter import _get_searcher
searcher = _get_searcher()
if searcher._model is None:
_ = searcher.model
emb = searcher.model.encode(text, normalize_embeddings=True, show_progress_bar=False)
return emb.tolist()
def _cosine_similarity(a: list, b: list) -> float:
"""余弦相似度(假设向量已归一化,点积即相似度)。"""
return sum(x * y for x, y in zip(a, b))
def match_ocr_to_kb(ocr_fields: list[str], kb_fields: list[dict],
llm=None) -> dict[str, str]:
"""将 OCR 提取的字段名匹配到 KB 字段定义。
Args:
ocr_fields: OCR 提取的中文字段名列表
kb_fields: KB 字段定义 [{"name": "billNo", "description": "工单号", ...}]
llm: 可选的 LLM 实例用于精确确认
Returns:
{"工单号": "billNo", "客户": "customerName", ...}
"""
if not ocr_fields or not kb_fields:
return {}
result = {}
# 阶段 1: Embedding 粗筛
try:
ocr_embs = {f: _embed(f) for f in ocr_fields}
kb_embs = {f["name"]: _embed(f.get("description", f["name"])) for f in kb_fields}
except Exception as e:
_match_log.warning("Embedding 匹配失败,回退到 LLM: %s", e)
return _match_via_llm(ocr_fields, kb_fields, llm)
candidates = {}
for ocr_name, ocr_emb in ocr_embs.items():
scored = []
for kb_name, kb_emb in kb_embs.items():
sim = _cosine_similarity(ocr_emb, kb_emb)
scored.append((kb_name, sim))
scored.sort(key=lambda x: x[1], reverse=True)
candidates[ocr_name] = scored[:3]
# 阶段 2: LLM 精确确认
if llm:
confirmed = _match_via_llm(ocr_fields, kb_fields, llm, candidates)
result.update(confirmed)
else:
for ocr_name, cands in candidates.items():
if cands and cands[0][1] > 0.5:
result[ocr_name] = cands[0][0]
return result
def _match_via_llm(ocr_fields: list[str], kb_fields: list[dict],
llm, candidates: Optional[dict] = None) -> dict[str, str]:
"""使用 LLM 精确确认字段映射。"""
kb_desc = "\n".join(
f"- {f['name']}: {f.get('description', '')} ({f.get('type', 'java.lang.String')})"
for f in kb_fields
)
candidates_hint = ""
if candidates:
cand_lines = []
for ocr_name, cands in candidates.items():
cand_str = ", ".join(f"{n}({s:.2f})" for n, s in cands)
cand_lines.append(f" {ocr_name} -> 候选: {cand_str}")
candidates_hint = (
"向量相似度候选(仅供参考,请根据语义确认):\n"
+ "\n".join(cand_lines)
)
prompt = (
"请将以下 OCR 识别的字段名匹配到知识库定义的字段。\n\n"
f"OCR 字段: {json.dumps(ocr_fields, ensure_ascii=False)}\n\n"
f"知识库字段:\n{kb_desc}\n\n"
f"{candidates_hint}\n\n"
"请以 JSON 对象格式输出映射关系,键为 OCR 字段名,值为 KB 字段名:\n"
'{"工单号": "billNo", "客户名称": "customerName"}'
)
try:
response = llm.invoke(prompt)
content = response.content if hasattr(response, "content") else str(response)
start = content.find("{")
end = content.rfind("}") + 1
if start >= 0 and end > start:
return json.loads(content[start:end])
except Exception as e:
_match_log.warning("LLM 字段匹配失败: %s", e)
return {}
def format_field_mapping_context(mapping: dict[str, str]) -> str:
"""将字段映射格式化为 prompt 上下文字符串。"""
if not mapping:
return ""
lines = ["[字段映射 — OCR -> KB]",
"请在 JRXML 中使用以下参数名:",
"| OCR 字段 | JRXML 参数 |",
"|---|---|"]
for ocr_name, kb_name in mapping.items():
lines.append(f"| {ocr_name} | $P{{{kb_name}}} |")
return "\n".join(lines)
+3 -1
View File
@@ -41,7 +41,9 @@ def parse_file(file_path: str, file_type: str = "") -> dict:
if not path.exists():
return {"text": "", "file_type": file_type, "method": "none", "error": "文件不存在"}
suffix = file_type or path.suffix.lower()
suffix = path.suffix.lower()
if file_type:
suffix = file_type if file_type.startswith(".") else f".{file_type}"
parsers = {
".png": _parse_image,
+201
View File
@@ -0,0 +1,201 @@
"""
JRXML 元素自动排序 JasperReports XSD <xs:sequence> 要求重排子元素
XSD 要求 jasperReport 子元素严格按以下顺序
property, propertyExpression, import, template, reportFont,
style, subDataset, scriptlet, parameter, queryString, field,
sortField, variable, filterExpression, group, background, title,
pageHeader, columnHeader, detail, columnFooter, pageFooter,
lastPageFooter, summary, noData
以及 band 内部的 reportElement 必须在其他元素之前
"""
import re
import xml.etree.ElementTree as ET
from typing import Optional
# JasperReports XSD sequence 顺序(索引越小越靠前)
JASPERREPORT_ORDER = {
"property": 0,
"propertyExpression": 1,
"import": 2,
"template": 3,
"reportFont": 4,
"style": 5,
"subDataset": 6,
"scriptlet": 7,
"parameter": 8,
"queryString": 9,
"field": 10,
"sortField": 11,
"variable": 12,
"filterExpression": 13,
"group": 14,
"background": 15,
"title": 16,
"pageHeader": 17,
"columnHeader": 18,
"detail": 19,
"columnFooter": 20,
"pageFooter": 21,
"lastPageFooter": 22,
"summary": 23,
"noData": 24,
}
# 带命名空间的标签映射(去掉 ns 前缀后匹配)
NS = "http://jasperreports.sourceforge.net/jasperreports"
def _tag_local(tag: str) -> str:
"""提取标签本地名(去掉命名空间前缀)。"""
return tag.split("}")[-1] if "}" in tag else tag
def _sort_key(elem: ET.Element) -> int:
"""排序键:按 JASPERREPORT_ORDER 中的顺序,未知元素放最后。"""
local = _tag_local(elem.tag)
return JASPERREPORT_ORDER.get(local, 999)
def reorder_jrxml_elements(xml_string: str) -> str:
"""重排 JRXML 字符串中的子元素顺序,使其符合 XSD sequence 要求。
处理范围
- jasperReport 的直接子元素
- band 的直接子元素reportElement 在前
返回重排后的 XML 字符串如果解析失败返回原始字符串
"""
try:
root = ET.fromstring(xml_string)
except ET.ParseError:
return xml_string # 无法解析,返回原始
_reorder_children(root)
_reorder_bands(root)
# 序列化回字符串
result = ET.tostring(root, encoding="unicode")
# 恢复 XML 声明、CDATA、命名空间
result = _restore_formatting(xml_string, result)
return result
def _reorder_children(parent: ET.Element):
"""递归重排所有子元素。"""
children = list(parent)
if not children:
return
# 按 XSD 顺序排序
children.sort(key=_sort_key)
# 重建子元素列表
for i, child in enumerate(children):
# ET 不支持直接 reorder,用 remove + insert
pass
# 实际上 ElementTree 不支持直接重排,需要重建
# 我们用更可靠的方式:收集所有子元素,清空,再按顺序添加
sorted_children = sorted(list(parent), key=_sort_key)
# 移除所有子元素
for child in list(parent):
parent.remove(child)
# 按排序后的顺序重新添加(保持 tail 文本在最后)
tail_text = ""
for child in sorted_children:
tail_text = child.tail or ""
child.tail = ""
parent.append(child)
# 恢复最后一个元素的 tail
if sorted_children and tail_text:
sorted_children[-1].tail = tail_text
# 递归处理子元素
for child in parent:
_reorder_children(child)
def _reorder_bands(root: ET.Element):
"""确保 band 内部 reportElement 在其他元素之前。"""
for elem in root.iter():
if _tag_local(elem.tag) == "band":
_ensure_reportelement_first(elem)
def _ensure_reportelement_first(band: ET.Element):
"""在 band 内部,确保 reportElement 元素排在最前面。"""
children = list(band)
report_elements = [c for c in children if _tag_local(c.tag) == "reportElement"]
other_elements = [c for c in children if _tag_local(c.tag) != "reportElement"]
if not report_elements:
return
# 移除所有
for c in list(band):
band.remove(c)
# 先添加 reportElement
tail = ""
for r in report_elements:
r.tail = ""
band.append(r)
# 再添加其他
for o in other_elements:
o.tail = ""
band.append(o)
# 恢复 tail
last = band[-1] if list(band) else None
if last and children:
last.tail = children[-1].tail or ""
def _restore_formatting(original: str, reordered: str) -> str:
"""恢复 XML 声明和 CDATA 段。"""
# 保留原始声明
decl = ""
if original.strip().startswith("<?xml"):
m = re.match(r'<\?xml[^?]*\?>', original)
if m:
decl = m.group()
if decl and not reordered.strip().startswith("<?xml"):
reordered = decl + "\n" + reordered
# 恢复 CDATAET 会把 CDATA 转成普通文本)
# 从原始 XML 提取所有 CDATA 块
cdata_pattern = re.compile(r'<!\[CDATA\[(.*?)\]\]>', re.DOTALL)
cdata_blocks = cdata_pattern.findall(original)
if cdata_blocks:
# 在重排后的 XML 中,对应位置的文本用 CDATA 包裹
def _restore_cdata(match):
nonlocal cdata_blocks
text = match.group(1)
for cdata in cdata_blocks:
if cdata.strip() == text.strip():
return f"<![CDATA[{cdata}]]>"
return match.group(0)
# 替换已转义的文本为 CDATA
reordered = re.sub(
r'(<queryString[^>]*>)\s*(.*?)\s*(</queryString>)',
lambda m: m.group(1) + f"\n <![CDATA[{m.group(2).strip()}]]>\n " + m.group(3),
reordered,
flags=re.DOTALL
)
return reordered
def normalize_jrxml(jrxml_text: str) -> str:
"""规范化 JRXML:排序元素 + 恢复格式。"""
if not jrxml_text or not jrxml_text.strip():
return jrxml_text
result = reorder_jrxml_elements(jrxml_text)
return result
+227
View File
@@ -0,0 +1,227 @@
"""多租户知识库管理模块。
用户 + 知识库 CRUD持久化到 kb_data/ 目录
每个 KB 拥有独立的 JSON 元数据文件和文件存储目录
"""
import json
import os
import re
import uuid
import tempfile
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
from backend.logger import get_logger
load_dotenv()
_kb_log = get_logger("kb_manager")
KB_DATA_DIR = Path(os.getenv("KB_DATA_DIR", "./kb_data"))
_USERS_FILE = KB_DATA_DIR / "users.json"
_VALID_ID_RE = re.compile(r'^[a-fA-F0-9]{12,}$')
def _validate_id(id_str: str, label: str = "id") -> None:
if not _VALID_ID_RE.match(id_str):
raise ValueError(f"Invalid {label}: {id_str!r}")
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def _read_json(fp: Path) -> dict:
with open(fp, "r", encoding="utf-8") as f:
return json.load(f)
def _write_json_atomic(fp: Path, data: dict) -> None:
_ensure_dir(fp.parent)
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False,
dir=fp.parent, encoding="utf-8",
)
try:
json.dump(data, tmp, ensure_ascii=False, indent=2)
tmp.flush()
os.fsync(tmp.fileno())
tmp.close()
os.replace(tmp.name, str(fp))
except Exception:
tmp.close()
Path(tmp.name).unlink(missing_ok=True)
raise
# ── User CRUD ──────────────────────────────────────────────────────────────
def _load_users() -> list[dict]:
_ensure_dir(KB_DATA_DIR)
if _USERS_FILE.exists():
return _read_json(_USERS_FILE)
return []
def _save_users(users: list[dict]) -> None:
_write_json_atomic(_USERS_FILE, users)
def create_user(name: str, user_id: Optional[str] = None) -> dict:
uid = user_id or uuid.uuid4().hex
users = _load_users()
if any(u["user_id"] == uid for u in users):
raise ValueError(f"User {uid} already exists")
user = {"user_id": uid, "name": name, "created_at": _now_iso()}
users.append(user)
_save_users(users)
_ensure_dir(KB_DATA_DIR / uid)
_write_json_atomic(KB_DATA_DIR / uid / "profile.json", user)
_kb_log.info("创建用户", extra={"user_id": uid, "user_name": name})
return user
def list_users() -> list[dict]:
return _load_users()
def get_user(user_id: str) -> Optional[dict]:
_validate_id(user_id, "user_id")
for u in _load_users():
if u["user_id"] == user_id:
return u
return None
def delete_user(user_id: str) -> bool:
_validate_id(user_id, "user_id")
users = _load_users()
filtered = [u for u in users if u["user_id"] != user_id]
if len(filtered) == len(users):
return False
_save_users(filtered)
user_dir = KB_DATA_DIR / user_id
if user_dir.exists():
shutil.rmtree(user_dir)
_kb_log.info("删除用户", extra={"user_id": user_id})
return True
# ── KB CRUD ────────────────────────────────────────────────────────────────
def _kb_dir(kb_id: str) -> Optional[Path]:
_validate_id(kb_id, "kb_id")
for user_dir in KB_DATA_DIR.iterdir():
if user_dir.is_dir() and not user_dir.name.startswith("."):
candidate = user_dir / kb_id
if candidate.is_dir():
return candidate
return None
def _ensure_user_dir(user_id: str) -> Path:
_validate_id(user_id, "user_id")
d = KB_DATA_DIR / user_id
_ensure_dir(d)
return d
def create_kb(user_id: str, name: str, description: str = "",
kb_id: Optional[str] = None) -> dict:
user_dir = _ensure_user_dir(user_id)
kid = kb_id or uuid.uuid4().hex
kb_dir = user_dir / kid
_ensure_dir(kb_dir)
_ensure_dir(kb_dir / "raw")
now = _now_iso()
meta = {
"kb_id": kid, "user_id": user_id, "name": name,
"description": description, "created_at": now, "updated_at": now,
"fields": [], "templates": [], "file_count": 0,
"chunk_count": 0, "parse_status": "empty",
}
_write_json_atomic(kb_dir / "meta.json", meta)
_kb_log.info("创建知识库", extra={"kb_id": kid, "user_id": user_id, "kb_name": name})
return meta
def list_kbs(user_id: str) -> list[dict]:
user_dir = _ensure_user_dir(user_id)
kbs = []
for kb_dir in sorted(user_dir.iterdir(), key=os.path.getmtime, reverse=True):
if kb_dir.is_dir() and not kb_dir.name.startswith("."):
meta_path = kb_dir / "meta.json"
if meta_path.exists():
meta = _read_json(meta_path)
kbs.append({
"kb_id": meta.get("kb_id", kb_dir.name),
"name": meta.get("name", kb_dir.name),
"description": meta.get("description", ""),
"created_at": meta.get("created_at", ""),
"updated_at": meta.get("updated_at", ""),
"field_count": len(meta.get("fields", [])),
"template_count": len(meta.get("templates", [])),
"file_count": meta.get("file_count", 0),
"chunk_count": meta.get("chunk_count", 0),
"parse_status": meta.get("parse_status", "empty"),
})
return kbs
def get_kb(kb_id: str) -> Optional[dict]:
_validate_id(kb_id, "kb_id")
kb_dir = _kb_dir(kb_id)
if kb_dir is None:
return None
meta_path = kb_dir / "meta.json"
return _read_json(meta_path) if meta_path.exists() else None
def update_kb_meta(kb_id: str, updates: dict) -> Optional[dict]:
kb_dir = _kb_dir(kb_id)
if kb_dir is None:
return None
meta_path = kb_dir / "meta.json"
meta = _read_json(meta_path)
meta.update(updates)
meta["updated_at"] = _now_iso()
_write_json_atomic(meta_path, meta)
return meta
def delete_kb(kb_id: str) -> bool:
kb_dir = _kb_dir(kb_id)
if kb_dir is None:
return False
shutil.rmtree(kb_dir)
_kb_log.info("删除知识库", extra={"kb_id": kb_id})
return True
def get_kb_raw_dir(kb_id: str) -> Optional[Path]:
kb_dir = _kb_dir(kb_id)
return kb_dir / "raw" if kb_dir else None
def get_kb_chunks_path(kb_id: str) -> Optional[Path]:
kb_dir = _kb_dir(kb_id)
return kb_dir / "chunks.json" if kb_dir else None
def get_kb_chroma_path(kb_id: str) -> Optional[Path]:
kb_dir = _kb_dir(kb_id)
if kb_dir is None:
return None
chroma_dir = kb_dir / "chroma"
_ensure_dir(chroma_dir)
return chroma_dir
+336
View File
@@ -0,0 +1,336 @@
"""KB 解析管道 — 文件提取→字段解析→chunk 切割→向量嵌入。
调用者: api_server.py (upload endpoint), scripts/init_default_kb.py
"""
import os
import json
import shutil
import zipfile
import tarfile
import tempfile
import defusedxml.ElementTree as ET
from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
from backend.logger import get_logger
from backend.file_parser import parse_file
load_dotenv()
_kb_parse_log = get_logger("kb_parser")
def _find_tag(elem, tag):
for el in elem.iter():
local = el.tag.split("}")[-1] if "}" in el.tag else el.tag
if local == tag:
return el
return None
def _find_all_tags(elem, tag):
results = []
for el in elem.iter():
local = el.tag.split("}")[-1] if "}" in el.tag else el.tag
if local == tag:
results.append(el)
return results
def parse_jrxml_fields(jrxml_path: str) -> dict:
"""解析 JRXML 文件,提取参数和字段定义。"""
try:
tree = ET.parse(jrxml_path)
root = tree.getroot()
except ET.ParseError as e:
return {"error": f"JRXML 解析失败: {e}", "parameters": [], "fields": [],
"report_name": ""}
report_name = root.attrib.get("name", "")
page_width = root.attrib.get("pageWidth", "")
page_height = root.attrib.get("pageHeight", "")
parameters = []
for p in _find_all_tags(root, "parameter"):
params = {"name": p.attrib.get("name", ""),
"type": p.attrib.get("class", "java.lang.String"),
"description": ""}
desc = _find_tag(p, "parameterDescription")
if desc is not None and desc.text:
params["description"] = desc.text.strip()
parameters.append(params)
fields = []
for f in _find_all_tags(root, "field"):
fields.append({"name": f.attrib.get("name", ""),
"type": f.attrib.get("class", "java.lang.String"),
"description": ""})
query_text = ""
query = _find_tag(root, "queryString")
if query is not None and query.text:
query_text = query.text.strip()
return {"report_name": report_name, "page_width": page_width,
"page_height": page_height, "parameters": parameters,
"fields": fields, "query": query_text, "error": None}
def _extract_archive(file_path: str, dest_dir: str) -> list[str]:
extracted = []
resolved_dest = os.path.realpath(dest_dir)
if zipfile.is_zipfile(file_path):
with zipfile.ZipFile(file_path, "r") as zf:
for member in zf.namelist():
member_path = os.path.realpath(os.path.join(dest_dir, member))
if not member_path.startswith(resolved_dest + os.sep):
continue
zf.extract(member, dest_dir)
if not member.endswith("/"):
extracted.append(member_path)
elif tarfile.is_tarfile(file_path):
with tarfile.open(file_path, "r:*") as tf:
for member in tf.getmembers():
member_path = os.path.realpath(os.path.join(dest_dir, member.name))
if not member_path.startswith(resolved_dest + os.sep):
continue
tf.extract(member, dest_dir)
if not member.name.endswith("/"):
extracted.append(member_path)
return extracted
def process_file_for_kb(kb_id: str, file_path: str,
source_name: str = "") -> dict:
from backend.kb_manager import get_kb_raw_dir
raw_dir = get_kb_raw_dir(kb_id)
if raw_dir is None:
return {"error": "KB 不存在"}
fname = source_name or os.path.basename(file_path)
dest = raw_dir / fname
shutil.copy2(file_path, dest)
suffix = Path(fname).suffix.lower()
if suffix == ".jrxml":
jrxml_info = parse_jrxml_fields(file_path)
text = f"[JRXML 模板: {jrxml_info['report_name']}]\n"
text += f"页面: {jrxml_info['page_width']}x{jrxml_info['page_height']}\n"
text += "参数:\n" + "\n".join(
f" {p['name']} ({p['type']})" for p in jrxml_info["parameters"])
text += "\n字段:\n" + "\n".join(
f" {f['name']} ({f['type']})" for f in jrxml_info["fields"])
if jrxml_info["query"]:
text += f"\n查询:\n{jrxml_info['query']}"
try:
raw_xml = Path(file_path).read_text(encoding="utf-8")
except Exception:
raw_xml = ""
return {"filename": fname, "type": "jrxml", "text": text,
"raw_xml": raw_xml, "jrxml_info": jrxml_info, "error": None}
if suffix in (".zip", ".tar", ".gz", ".tgz"):
tmpdir = tempfile.mkdtemp(prefix="kb_extract_")
try:
extracted = _extract_archive(file_path, tmpdir)
sub_results = []
for ep in extracted:
sub = process_file_for_kb(
kb_id, ep, source_name=os.path.basename(ep))
sub_results.append(sub)
return {"filename": fname, "type": "archive", "text": "",
"archive_contents": sub_results, "error": None}
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
parse_result = parse_file(str(dest))
return {"filename": fname, "type": suffix.lstrip("."),
"text": parse_result.get("text", ""),
"error": parse_result.get("error")}
def chunk_file_results(results: list[dict], kb_name: str = "") -> list[dict]:
chunks = []
chunk_idx = 0
for r in results:
if r.get("type") == "archive":
for sub in r.get("archive_contents", []):
chunks.extend(chunk_file_results([sub], kb_name))
continue
fname = r.get("filename", "")
ftype = r.get("type", "")
text = r.get("text", "")
if not text.strip():
continue
if ftype == "jrxml" and r.get("raw_xml"):
jinfo = r.get("jrxml_info", {})
report_name = jinfo.get("report_name", "")
chunks.append({
"id": f"chunk_{chunk_idx}",
"content": f"[JRXML 模板: {report_name}]\n{r['text']}\n\n"
f"<xml>\n{r['raw_xml']}\n</xml>",
"metadata": {"chunk_type": "jrxml_template",
"source_file": fname,
"report_name": report_name,
"kb_name": kb_name,
"param_count": len(jinfo.get("parameters", [])),
"field_count": len(jinfo.get("fields", []))},
})
chunk_idx += 1
continue
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
for para in paragraphs:
if len(para) < 10:
continue
chunk_type = "md_section" if ftype in ("md", "") else f"{ftype}_text"
chunks.append({
"id": f"chunk_{chunk_idx}",
"content": para,
"metadata": {"chunk_type": chunk_type,
"source_file": fname, "kb_name": kb_name},
})
chunk_idx += 1
return chunks
def extract_fields_with_llm(text: str, llm=None) -> list[dict]:
if llm is None:
return _extract_fields_from_table(text)
prompt = (
"请分析以下接口文档内容,提取所有字段定义。\n"
"对每个字段,输出: 字段名, 含义, 类型, 是否必需。\n"
"以 JSON 数组格式输出,每个元素为 {\"name\": \"...\", "
"\"description\": \"...\", \"type\": \"...\", \"required\": false}。\n\n"
f"{text}"
)
try:
response = llm.invoke(prompt)
content = response.content if hasattr(response, "content") else str(response)
start = content.find("[")
end = content.rfind("]") + 1
if start >= 0 and end > start:
return json.loads(content[start:end])
except Exception as e:
_kb_parse_log.warning("LLM 字段提取失败,使用表格回退: %s", e)
return _extract_fields_from_table(text)
def _extract_fields_from_table(text: str) -> list[dict]:
fields = []
lines = text.split("\n")
header_found = False
for line in lines:
line = line.strip()
if not line.startswith("|"):
continue
cells = [c.strip() for c in line.split("|")[1:-1]]
if not cells:
continue
if not header_found:
if any(h in str(c) for c in cells
for h in ["字段", "名称", "含义", "说明", "类型"]):
header_found = True
continue
if all(c.replace("-", "").replace(":", "").replace(" ", "") == ""
for c in cells):
continue
if len(cells) >= 2:
name = cells[0].replace("**", "").replace("L ", "").replace("\\", "").strip()
if not name or name in ("", "---"):
continue
field = {"name": name, "description": "", "type": "java.lang.String",
"required": False}
if len(cells) >= 2 and cells[1]:
field["description"] = cells[1].replace("<br/>", " ").strip()
if len(cells) >= 3 and cells[2]:
field["required"] = cells[2].strip() in ("", "Y", "y", "yes", "Yes", "必填")
if len(cells) >= 4 and cells[3]:
field["type"] = cells[3].strip()
fields.append(field)
return fields
def build_kb_from_files(kb_id: str, file_paths: list[str],
llm=None) -> dict:
from backend.kb_manager import update_kb_meta, get_kb_chunks_path
from backend.kb_searcher import get_kb_searcher
all_results = []
errors = []
for fp in file_paths:
try:
r = process_file_for_kb(kb_id, fp)
all_results.append(r)
if r.get("error"):
errors.append({"file": os.path.basename(fp), "error": r["error"]})
except Exception as e:
errors.append({"file": os.path.basename(fp), "error": str(e)})
chunks = chunk_file_results(all_results)
chunks_path = get_kb_chunks_path(kb_id)
if chunks_path:
chunks_path.parent.mkdir(parents=True, exist_ok=True)
with open(chunks_path, "w", encoding="utf-8") as f:
json.dump(chunks, f, ensure_ascii=False, indent=2)
searcher = get_kb_searcher(kb_id)
if searcher and chunks:
try:
searcher.add_chunks(chunks)
except Exception as e:
errors.append({"file": "embedding", "error": str(e)})
all_fields = []
template_names = []
for r in all_results:
_collect_from_result(r, all_fields, template_names)
for r in all_results:
if r.get("type") in ("archive", "jrxml"):
continue
text = r.get("text", "")
if text.strip():
for ef in extract_fields_with_llm(text, llm):
if not any(f["name"] == ef["name"] for f in all_fields):
all_fields.append(ef)
update_kb_meta(kb_id, {
"fields": all_fields, "templates": template_names,
"file_count": len(file_paths), "chunk_count": len(chunks),
"parse_status": "ready" if not errors else "partial",
})
_kb_parse_log.info("KB 构建完成", extra={
"kb_id": kb_id, "fields": len(all_fields),
"templates": len(template_names), "chunks": len(chunks),
})
return {"status": "ready" if not errors else "partial",
"field_count": len(all_fields), "chunk_count": len(chunks),
"template_count": len(template_names), "errors": errors}
def _collect_from_result(r: dict, all_fields: list, template_names: list) -> None:
jinfo = r.get("jrxml_info")
if jinfo and jinfo.get("report_name"):
template_names.append({"name": jinfo["report_name"],
"file": r.get("filename", "")})
for p in jinfo.get("parameters", []):
field = {"name": p["name"], "description": p.get("description", ""),
"type": p.get("type", "java.lang.String"), "required": False}
if not any(f["name"] == field["name"] for f in all_fields):
all_fields.append(field)
for f in jinfo.get("fields", []):
field = {"name": f["name"], "description": f.get("description", ""),
"type": f.get("type", "java.lang.String"), "required": False}
if not any(fi["name"] == field["name"] for fi in all_fields):
all_fields.append(field)
+170
View File
@@ -0,0 +1,170 @@
"""KB 隔离的 ChromaDB 语义搜索适配器。
每个知识库拥有独立的 ChromaDB collection
调用者: backend/rag_adapter.py, agent/nodes.py, api_server.py
"""
import os
import logging
from pathlib import Path
from typing import Optional
from dotenv import load_dotenv
load_dotenv()
logger = logging.getLogger(__name__)
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
def _resolve(path: str) -> Path:
p = Path(path)
return p if p.is_absolute() else _PROJECT_ROOT / p
class KBChromaSearcher:
"""连接指定 KB 的 ChromaDB,提供语义搜索。"""
def __init__(self, chroma_path: str, collection_name: str = "kb_chunks",
model_name: Optional[str] = None, use_gpu: Optional[bool] = None,
use_fp16: Optional[bool] = None):
self.chroma_path = str(_resolve(chroma_path))
self.collection_name = collection_name
model_path = model_name or os.getenv(
"RAG_EMBED_MODEL", "./rag/models/paraphrase-multilingual-MiniLM-L12-v2")
resolved = _resolve(model_path)
self.model_name = str(resolved) if resolved.exists() else model_path
self.use_gpu = (use_gpu if use_gpu is not None
else os.getenv("RAG_USE_GPU", "true").lower() in ("true", "1"))
self.use_fp16 = (use_fp16 if use_fp16 is not None
else os.getenv("RAG_USE_FP16", "true").lower() in ("true", "1"))
self._model = None
self._client = None
self._collection = None
@property
def model(self):
if self._model is None:
import torch
from sentence_transformers import SentenceTransformer
device = "cuda" if (self.use_gpu and torch.cuda.is_available()) else "cpu"
logger.info("加载嵌入模型: %s (device=%s)", self.model_name, device)
model = SentenceTransformer(self.model_name, device=device)
if device == "cuda" and self.use_fp16:
model = model.half()
self._model = model
return self._model
@property
def client(self):
if self._client is None:
import chromadb
self._client = chromadb.PersistentClient(path=self.chroma_path)
return self._client
@property
def collection(self):
if self._collection is None:
try:
self._collection = self.client.get_collection(self.collection_name)
except Exception:
self._collection = self.client.create_collection(
self.collection_name, metadata={"hnsw:space": "cosine"})
return self._collection
def is_ready(self) -> bool:
try:
self.client.get_collection(self.collection_name)
return True
except Exception:
return False
def search(self, query: str, k: int = 5, threshold: Optional[float] = None) -> list[dict]:
if not self.is_ready():
return []
query_embedding = self.model.encode(
query, normalize_embeddings=True, show_progress_bar=False)
results = self.collection.query(
query_embeddings=[query_embedding.tolist()],
n_results=k, include=["documents", "metadatas", "distances"])
output = []
if not results["ids"] or not results["ids"][0]:
return output
for i, doc_id in enumerate(results["ids"][0]):
dist = results["distances"][0][i]
if threshold is not None and dist > threshold:
continue
output.append({
"id": doc_id,
"content": results["documents"][0][i],
"metadata": results["metadatas"][0][i] or {},
"distance": dist,
})
return output
def search_templates(self, query: str, k: int = 3) -> list[dict]:
results = self.search(query, k=k * 2)
templates = []
for r in results:
meta = r.get("metadata", {})
chunk_type = meta.get("chunk_type", "")
if "jrxml" in chunk_type.lower() or meta.get("report_name"):
templates.append(r)
if len(templates) >= k:
break
return templates
def search_as_context(self, query: str, k: int = 5) -> str:
results = self.search(query, k=k)
if not results:
return ""
parts = []
for r in results:
meta = r.get("metadata", {})
header = f"[类型:{meta.get('chunk_type', 'N/A')}]"
if meta.get("report_name"):
header += f" [报表:{meta['report_name']}]"
parts.append(f"{header}\n{r['content']}")
return "\n\n---\n\n".join(parts)
def add_chunks(self, chunks: list[dict]) -> None:
if not chunks:
return
ids = [c["id"] for c in chunks]
docs = [c["content"] for c in chunks]
metas = [c.get("metadata", {}) for c in chunks]
embeddings = self.model.encode(
docs, normalize_embeddings=True, show_progress_bar=True)
self.collection.upsert(
ids=ids, documents=docs, metadatas=metas,
embeddings=embeddings.tolist())
_searchers: dict = {}
def get_kb_searcher(kb_id: str) -> Optional[KBChromaSearcher]:
from backend.kb_manager import get_kb_chroma_path
if kb_id in _searchers:
return _searchers[kb_id]
chroma_path = get_kb_chroma_path(kb_id)
if chroma_path is None:
return None
searcher = KBChromaSearcher(str(chroma_path))
_searchers[kb_id] = searcher
return searcher
def search_kb(kb_id: str, query: str, k: int = 5) -> str:
searcher = get_kb_searcher(kb_id)
if searcher is None:
return ""
return searcher.search_as_context(query, k=k)
def search_templates_in_kb(kb_id: str, query: str, k: int = 3) -> list[dict]:
searcher = get_kb_searcher(kb_id)
if searcher is None:
return []
return searcher.search_templates(query, k=k)
+60 -23
View File
@@ -8,7 +8,7 @@ from dotenv import load_dotenv
from backend.logger import get_logger
load_dotenv()
load_dotenv(override=True)
_llm_log = get_logger("llm")
@@ -109,19 +109,36 @@ class _LLMLoggingWrapper(_BaseLLM):
resp_text = "".join(full)
resp_len = len(resp_text)
resp_preview = resp_text[:500]
_llm_log.info(
"LLM stream 完成",
extra={
"direction": "response",
"model": self._model,
"backend": self._backend,
"caller": self._caller,
"duration_ms": elapsed,
"response_length": resp_len,
"response_preview": resp_preview,
"response": resp_text[:10000],
},
)
stop_reason = getattr(self._inner, '_last_stop_reason', None)
self._last_stop_reason = stop_reason
if stop_reason == "max_tokens":
_llm_log.warning(
"LLM stream 截断 (max_tokens),输出可能不完整",
extra={
"direction": "response",
"model": self._model,
"backend": self._backend,
"caller": self._caller,
"duration_ms": elapsed,
"response_length": resp_len,
"stop_reason": stop_reason,
},
)
else:
_llm_log.info(
"LLM stream 完成",
extra={
"direction": "response",
"model": self._model,
"backend": self._backend,
"caller": self._caller,
"duration_ms": elapsed,
"response_length": resp_len,
"response_preview": resp_preview,
"response": resp_text[:10000],
"stop_reason": stop_reason,
},
)
except Exception as e:
elapsed = round((time.time() - t0) * 1000)
_llm_log.error(
@@ -139,8 +156,14 @@ class _LLMLoggingWrapper(_BaseLLM):
raise
def _build_raw_llm(caller: str = "") -> tuple[_BaseLLM, str, str]:
"""构造原始 LLM 实例,返回 (实例, model名, backend名)。"""
DEFAULT_MAX_TOKENS = int(os.getenv("LLM_MAX_TOKENS", "8192"))
def _build_raw_llm(caller: str = "", max_tokens: int | None = None) -> tuple[_BaseLLM, str, str]:
"""构造原始 LLM 实例,返回 (实例, model名, backend名)。
max_tokens: 覆盖默认输出 token None 使用 LLM_MAX_TOKENS 环境变量或 8192
"""
backend = os.getenv("LLM_BACKEND", "cloud")
if backend == "local":
from langchain_ollama import ChatOllama
@@ -166,32 +189,43 @@ def _build_raw_llm(caller: str = "") -> tuple[_BaseLLM, str, str]:
base_url = os.getenv("ANTHROPIC_BASE_URL") or os.getenv("OPENAI_BASE_URL", "https://api.minimaxi.com/anthropic")
model = os.getenv("LLM_MODEL", "MiniMax-M2.7")
temperature = 0.1
max_tokens = 4096
_default_max_tokens = max_tokens if max_tokens is not None else DEFAULT_MAX_TOKENS
client = Anthropic(api_key=api_key, base_url=base_url, timeout=120)
class MiniMaxLLM(_BaseLLM):
def __init__(self):
self._last_stop_reason = None
self._max_tokens = _default_max_tokens
def invoke(self, prompt: str) -> Any:
resp = client.messages.create(
model=model,
max_tokens=max_tokens,
max_tokens=self._max_tokens,
temperature=temperature,
messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}],
)
for block in resp.content:
if block.type == "text":
block_type = getattr(block, "type", "")
if block_type == "text":
return type("Response", (), {"content": block.text})()
return type("Response", (), {"content": ""})()
def stream(self, prompt: str):
self._last_stop_reason = None
with client.messages.stream(
model=model,
max_tokens=max_tokens,
max_tokens=self._max_tokens,
temperature=temperature,
messages=[{"role": "user", "content": [{"type": "text", "text": prompt}]}],
) as s:
for text in s.text_stream:
yield text
try:
final_msg = s.get_final_message()
self._last_stop_reason = getattr(final_msg, 'stop_reason', None)
except Exception:
pass
def get_num_tokens(self, text: str) -> int:
resp = client.messages.count_tokens(
@@ -223,9 +257,12 @@ def _build_raw_llm(caller: str = "") -> tuple[_BaseLLM, str, str]:
return OpenAIWrapper(), model, f"cloud/openai/{model}"
def get_llm(caller: str = "") -> _BaseLLM:
"""返回带日志的 LLM 实例。caller 用于标识调用来源(如 generate、classify_intent)。"""
inner, model, backend = _build_raw_llm(caller)
def get_llm(caller: str = "", max_tokens: int | None = None) -> _BaseLLM:
"""返回带日志的 LLM 实例。caller 用于标识调用来源(如 generate、classify_intent)。
max_tokens: 覆盖默认输出 token 用于骨架生成等需要大量输出的节点
"""
inner, model, backend = _build_raw_llm(caller, max_tokens=max_tokens)
return _LLMLoggingWrapper(inner, model=model, backend=backend, caller=caller)
+130 -15
View File
@@ -98,6 +98,14 @@ class ExtractionResult:
}
for f in self.fields
],
"all_elements": [
{
"text": e.text,
"bbox": e.bbox,
"confidence": e.confidence,
}
for e in self.all_elements
],
"total_elements": len(self.all_elements),
"errors": self.errors,
}
@@ -136,13 +144,13 @@ class OcrExtractor:
def extract(
self,
file_path: str,
target_fields: list[str],
target_fields: Optional[list[str]] = None,
) -> dict:
"""执行两阶段 OCR 字段提取。
Args:
file_path: 图片文件路径支持 png/jpg/jpeg/bmp/webp
target_fields: 需要提取的字段名称列表 ["发票代码", "发票号码", "合计金额"]
target_fields: 需要提取的字段名称列表为空或 None 时自动发现文档中所有键值对
Returns:
提取结果字典格式见 ExtractionResult.to_dict()
@@ -168,20 +176,40 @@ class OcrExtractor:
return result.to_dict()
result.ocr_available = True
for field_name in target_fields:
extracted = self._extract_field(field_name, elements)
if extracted:
result.fields.append(extracted)
else:
result.fields.append(
ExtractedField(
field_name=field_name,
field_value="",
bbox=[],
confidence=0.0,
extraction_method="none",
if target_fields:
# 有预设字段名:按名单查找
for field_name in target_fields:
extracted = self._extract_field(field_name, elements)
if extracted:
result.fields.append(extracted)
else:
result.fields.append(
ExtractedField(
field_name=field_name,
field_value="",
bbox=[],
confidence=0.0,
extraction_method="none",
)
)
else:
# 无预设字段名:自动发现文档中所有键值对
discovered = self._discover_fields(elements)
for field in discovered:
extracted = self._extract_field(field, elements)
if extracted:
result.fields.append(extracted)
else:
result.fields.append(
ExtractedField(
field_name=field,
field_value="",
bbox=[],
confidence=0.0,
extraction_method="none",
)
)
)
return result.to_dict()
@@ -396,6 +424,83 @@ class OcrExtractor:
# 阶段2: 字段精确提取
# ========================================================================
def _discover_fields(self, elements: list[OcrTextElement]) -> list[str]:
"""自动发现文档中的字段名(无需预设字段列表)。
策略
1. 单元素内"标签: 值"模式 从中提取标签
2. 同行相邻键值对 短文本(标签) + 长文本()
3. 表头行 首行/第二行的文本作为列字段名
"""
separators = [":", "", "=", ""]
discovered: set[str] = set()
elements_sorted = sorted(elements, key=lambda e: (e.y_min, e.x_min))
# 策略 1: 单元素内嵌键值对
for elem in elements:
text = elem.text
for sep in separators:
if sep in text:
parts = text.split(sep, 1)
label = parts[0].strip()
value = parts[1].strip()
if label and value and len(label) <= 20 and label != value:
discovered.add(label)
# 策略 2: 同行相邻键值对(标签在左,值在右)
# 按行分组
rows: dict[int, list[OcrTextElement]] = {}
for elem in elements_sorted:
row_key = int(elem.y_min)
for existing_key in list(rows.keys()):
if abs(int(elem.y_min) - existing_key) < 10:
row_key = existing_key
break
if row_key not in rows:
rows[row_key] = []
rows[row_key].append(elem)
for row_elems in rows.values():
row_elems.sort(key=lambda e: e.x_min)
for i in range(len(row_elems) - 1):
left = row_elems[i]
right = row_elems[i + 1]
# 左边是短文本(可能标签),右边是相邻的正常文本(可能值)
if (len(left.text) <= 15 and len(right.text) > 0
and abs(right.x_min - left.x_max) < left.width * 3):
# 左边不含仅数字/金额模式(这些更可能是值)
if not re.match(r'^[\d,.]+\s*%?$', left.text.strip()):
discovered.add(left.text.strip())
# 策略 3: 表头行 — 取前两行中较短的元素作为字段名候选
sorted_row_keys = sorted(rows.keys())
header_rows = sorted_row_keys[:min(3, len(sorted_row_keys))]
for row_key in header_rows:
for elem in rows.get(row_key, []):
text = elem.text.strip()
if text and len(text) <= 20 and not re.match(r'^[\d,.]+\s*%?$', text):
discovered.add(text)
# 去重合并:移除值文本中误识别为标签的条目
# 排除纯数字、日期、金额等明显是值的文本
value_patterns = [
r'^\d{1,2}[月/-]\d{1,2}[日/-]?\d{0,4}$',
r'^[\d,]+\.?\d*\s*%?$',
r'^[¥¥]\s*[\d,]+\.?\d*$',
r'^\d{3,}$',
]
filtered = set()
for name in discovered:
is_value = False
for pat in value_patterns:
if re.match(pat, name):
is_value = True
break
if not is_value:
filtered.add(name)
return sorted(filtered)
def _extract_field(
self,
field_name: str,
@@ -558,6 +663,7 @@ class OcrExtractor:
# -----------------------------------------------------------------------
PREDEFINED_PATTERNS: dict[str, str] = {
# 发票字段
"发票代码": r"[0-9A-Za-z]{10,12}",
"发票号码": r"\d{8}",
"合计金额": r"[\d,]+\.?\d*",
@@ -571,6 +677,15 @@ class OcrExtractor:
"数量": r"\d+\.?\d*",
"单价": r"[\d,]+\.?\d*",
"税率": r"\d+\.?\d*%?",
# 车历卡/维修结算单字段
"维修单号": r"[A-Za-z0-9\-]{6,20}",
"车牌号": r"[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-Z][·\-]?[A-Z0-9]{5,6}",
"联系电话": r"1[3-9]\d{9}",
"VIN码": r"[A-HJ-NPR-Z0-9]{17}",
"发动机号": r"[A-Z0-9]{6,12}",
# 采购单字段
"采购日期": r"\d{4}[年/\-]\d{1,2}[月/\-]\d{1,2}日?",
"订单号": r"[A-Z0-9\-]{6,20}",
}
def _regex_match(
+8 -2
View File
@@ -150,6 +150,12 @@ def _get_searcher() -> RAGSearcher:
return _searcher
def search_chunks(query: str, k: int = 5) -> str:
"""搜索 JRXML 知识库并返回拼接后的上下文文本(便捷函数)。"""
def search_chunks(query: str, k: int = 5, kb_id: str = "") -> str:
"""搜索知识库并返回拼接后的上下文文本
若指定 kb_id使用该 KB 专属 ChromaDB否则使用全局默认库
"""
if kb_id:
from backend.kb_searcher import search_kb
return search_kb(kb_id, query, k=k)
return _get_searcher().search_as_context(query, k=k)
+40 -9
View File
@@ -5,7 +5,9 @@
import json
import os
import re
import uuid
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
@@ -25,18 +27,27 @@ def _ensure_dir():
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
_VALID_SESSION_ID_RE = re.compile(r'^[a-fA-F0-9]{12,}$')
def validate_session_id(session_id: str) -> bool:
"""校验 session_id 仅含合法 hex 字符(防路径穿越)。"""
return bool(_VALID_SESSION_ID_RE.match(session_id))
def _session_path(session_id: str) -> Path:
if not validate_session_id(session_id):
raise ValueError(f"Invalid session_id: {session_id!r}")
return SESSIONS_DIR / f"{session_id}.json"
def generate_session_id() -> str:
return uuid.uuid4().hex[:12]
return uuid.uuid4().hex
def create_session(name: str = "", agent_state: Optional[dict] = None) -> dict:
"""创建新会话,返回会话元数据。"""
def create_session(name: str = "", agent_state: Optional[dict] = None,
session_id: Optional[str] = None) -> dict:
"""创建新会话,返回会话元数据。session_id 可选——传入时使用指定 ID。"""
_ensure_dir()
sid = generate_session_id()
sid = session_id or generate_session_id()
now = _now_iso()
agent_state = agent_state or {}
agent_state["session_id"] = sid
@@ -45,6 +56,7 @@ def create_session(name: str = "", agent_state: Optional[dict] = None) -> dict:
"session_name": name or f"新建报表 {now[:10]}",
"created_at": now,
"updated_at": now,
"kb_id": agent_state.get("kb_id", "") if agent_state else "",
"agent_state": agent_state,
}
with open(_session_path(sid), "w", encoding="utf-8") as f:
@@ -56,7 +68,10 @@ def create_session(name: str = "", agent_state: Optional[dict] = None) -> dict:
def load_session(session_id: str) -> Optional[dict]:
"""按 ID 加载会话数据。未找到则返回 None。"""
_ensure_dir()
fp = _session_path(session_id)
try:
fp = _session_path(session_id)
except ValueError:
return None
if not fp.exists():
return None
with open(fp, "r", encoding="utf-8") as f:
@@ -64,7 +79,7 @@ def load_session(session_id: str) -> Optional[dict]:
def save_session(session_id: str, agent_state: dict, session_name: str = ""):
"""将会话状态保存(更新)至磁盘"""
"""将会话状态原子保存至磁盘(temp file + rename,避免崩溃时截断)"""
_ensure_dir()
fp = _session_path(session_id)
data = {}
@@ -82,8 +97,21 @@ def save_session(session_id: str, agent_state: dict, session_name: str = ""):
data["created_at"] = data["updated_at"]
data["agent_state"] = agent_state
with open(fp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 原子写入:先写临时文件,再 replace,避免崩溃时截断 JSON
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False,
dir=SESSIONS_DIR, encoding="utf-8",
)
try:
json.dump(data, tmp, ensure_ascii=False, indent=2)
tmp.flush()
os.fsync(tmp.fileno())
tmp.close()
os.replace(tmp.name, str(fp))
except Exception:
tmp.close()
Path(tmp.name).unlink(missing_ok=True)
raise
def get_session_state(session_id: str) -> Optional[dict]:
@@ -117,7 +145,10 @@ def list_all_sessions() -> list[dict]:
def delete_session(session_id: str) -> bool:
"""按 ID 删除会话文件。"""
_ensure_dir()
fp = _session_path(session_id)
try:
fp = _session_path(session_id)
except ValueError:
return False
if fp.exists():
fp.unlink()
_session_log.info("删除会话", extra={"session_id": session_id})
+13 -3
View File
@@ -4,10 +4,11 @@ import os
import httpx
from dotenv import load_dotenv
from httpx import ConnectError, HTTPStatusError
from backend.logger import get_logger
load_dotenv()
load_dotenv(override=True)
_val_log = get_logger("validation")
@@ -31,10 +32,19 @@ def validate_jrxml(jrxml_text: str) -> dict:
},
)
return result
except httpx.ConnectError:
except ConnectError:
error_msg = f"无法连接到验证服务 ({VALIDATION_URL})。是否正在运行?"
_val_log.error("验证服务连接失败", extra={"error": error_msg, "url": VALIDATION_URL})
return {"valid": False, "error": error_msg}
return {"valid": False, "error": error_msg, "service_unavailable": True}
except HTTPStatusError as e:
status_code = e.response.status_code
error_msg = f"验证服务返回错误 ({status_code}): {str(e)}"
_val_log.error("验证请求异常", extra={"error": str(e), "url": VALIDATION_URL, "status_code": status_code})
return {
"valid": False,
"error": error_msg,
"service_unavailable": status_code >= 500,
}
except Exception as e:
error_msg = f"验证请求失败: {str(e)}"
_val_log.error("验证请求异常", extra={"error": str(e), "url": VALIDATION_URL})
+207
View File
@@ -0,0 +1,207 @@
# jaspersoft 五轮修正失败问题 — 现象文档
## 1. 问题概述
**触发场景**:用户上传一张车历卡(维修结算单)图片,系统通过 OCR 识别并生成 JRXML 报表模板。
**现象**:5 次自动修正循环全部失败,系统提示"[内容保真度不足] 得分 0.00/1.0",最终无法生成可用 JRXML。
**测试会话**`sessions/6d39a91e11c54f02bb70a62d856ea2d4.json`2026-05-24 15:14
---
## 2. 完整流程追踪
### 2.1 用户输入
- 上传文件:`e1113725c20fc4ec39bc9e4ab0caa6b2.jpg`(车历卡,1357×1920 RGB
- 用户输入:空(仅上传文件)
### 2.2 系统处理流程
```
上传图片
→ process_input 节点(OCR 字段提取 + 布局分析)
→ layout_analyzer34 行 × 1 列,A4 纵向)
→ ocr_extractor4 策略提取)
→ classify_intent= initial_generation
→ retrieve
→ route_after_retrieve(有 layout_schema,走 generate_skeleton
→ generate_skeleton(生成 ~34k 字符骨架 JRXML
→ refine_layoutBand 级窗口化精调)
→ map_fields(程序化字段替换 $F{field_N} → 真实字段名)
→ validatevalidate 节点)
→ XSD 验证:✅ 通过
→ OCR 保真度检查:❌ score=0.41 < 0.5 → 降级为 fail
→ error_msg = "[内容保真度不足] 得分 0.41/1.0。元素覆盖不足:JRXML 仅有 142 个文本元素,OCR 源有 173 个文本元素(覆盖率 82%)。JRXML 中未声明任何字段,但 OCR 提取了结构化字段数据"
→ 5 次 correct_jrxml 修正循环均失败
→ 状态:failMAX_RETRY=5 耗尽)
```
### 2.3 最终状态
```
status: fail
retry_count: 5
error_msg: "[内容保真度不足] 得分 0.00/1.0。JRXML 仅有 0 个文本元素,OCR 源有 173 个文本元素(覆盖率 0%)"
```
⚠️ **重要矛盾**`current_jrxml`24391 字符,142 个 textField)与 `error_msg`"0 个文本元素"score=0.00)存在矛盾。
验证服务审计(`validate-service-audit`)指出:"6d39a91e has 0 text elements causing score=0.00"——说明在**触发 fail 的那个时间点**,JRXML 确实只有 0 个文本元素。
但 session 文件最终保存的 `current_jrxml` 是 24391 字符版本。`jrxml_versions` 最后一条记录的 `jrxml` 才是触发失败的真实版本。
**结论**`current_jrxml` 在 5 次修正过程中被逐步侵蚀,最终版本是某个接近空壳的状态。`jrxml_versions[-1]` 中的 `jrxml` 才是真正的失败版本。
---
## 3. 关键数据对比
### 3.1 JRXML 实际内容
session `6d39a91e``current_jrxml`
- 长度:24391 字符
- `<textField` 标签数:284(含自闭合标签)
- 完整 textField 元素:142 个
- `<staticText>` 元素:0 个
- `<field>` 声明:63 个
- pageWidth/pageHeight595×842A4
- namespace:无 ns0: 前缀(✅ 已消除)
### 3.2 OCR 数据
layout_schema
- total_rows: 34
- total_columns: 1
- 总文本元素:173 个
ocr_extraction_result
- total_elements: 173
- fields 数组:18 个字段,全部是**发票模板字段**
- 不含税金额、价税合计、单价、发票代码、发票号码、合计金额、开票日期、总金额、数量、日期、校验码、税率、税额、规格型号、货物名称、购买方名称、金额、销售方名称
- 正确匹配率:3/1817%
### 3.3 评分数据
`_check_ocr_fidelity` 实际运行结果:
```python
# element_coverage 计算
textField_count = 142 # 完整元素(非标签数)
staticText_count = 0
total_jrxml_elements = 142
ocr_text_count = 173
element_coverage = min(142/173, 1.0) = 0.82
# field_coverage 计算
jrxml_fields = {"print_date", "repair_number", ..., "vehicle_plate", ...} # 63 个英文字段
ocr_field_names = {"发票代码", "发票号码", "合计金额", ...} # 18 个中文字段
matched = jrxml_fields ∩ ocr_field_names = ∅
field_coverage = 0/18 = 0.0
# score 计算
score = 0.0 * 0.5 + 0.82 * 0.5 = 0.41
```
**评分公式**`nodes.py:1251`):
```python
score = round(field_coverage * 0.5 + element_coverage * 0.5, 3)
```
**降级条件**`nodes.py:1294`):
```python
if fidelity["score"] < 0.5:
state["status"] = "fail"
```
---
## 4. 已确认的根因
### 根因 1 — 评分逻辑设计错误(P0)
**问题**`field_coverage` 将英文字段名和中文字段名做交集比对,在普通生成场景下永远为 0。
**原因**
1. LLM 生成英文字段名(`print_date``vehicle_plate`)是正确的设计选择
2. OCR 提取器硬套发票模板,提取中文字段名(`发票代码``合计金额`
3. 两者来自完全不同的命名体系,不可能匹配
4. `field_coverage=0` 是**预期行为**,而非错误
**修复方向**:评分公式改为只依赖 `element_coverage``field_coverage` 作为信息提示而非降级条件。
### 根因 2 — OCR 字段提取器无文档类型区分(P0)
**问题**`backend/ocr_extractor.py` 对所有单据使用同一套发票字段模板(14 个字段)。
**现象**
- 车历卡被当作发票处理
- 手机号 `13516727312` 被 6 个字段复用(发票代码/校验码/价税合计/单价/税率/不含税金额)
- 字段名错配:发票号码→"服务套餐"、总金额→"钣金"、数量→"零件等级"
### 根因 3 — namespace 修复指令是条件触发(P1)
**位置**`prompts/correction.md` 第 11 行
**问题**namespace 修复指令只在错误消息**包含 "namespace" 关键词**时才激活,是条件触发而非无条件指令。
**现象**
- ecd592 session 所有 5 次修正的实际错误类型是"字段未声明"(`字段 'u53d1_u7968...' 在表达式中使用但未声明`),**不包含 "namespace" 关键词**
- 因此 namespace 修复指令从未被激活
- ns0: 前缀在前两次修正中持续存在,直到第 3 次才被 LLM 自发消除
- 最终 `current_jrxml`ecd592)仍有 `<ns0:jasperReport>`
**修复方向**
- `prompts/correction.md`:改为无条件指令(检查 JRXML 是否包含 `ns0:`,而非依赖错误类型)
- `prompts/initial_generation.md` + `skeleton_generation.md`:添加删除 ns0: 前缀的无条件指令
### 根因 4 — 正则 `\w+` 不支持中文(低优先级)
**位置**`nodes.py:1211`
```python
jrxml_fields = set(re.findall(r'<field name="(\w+)"', jrxml))
```
**问题**`\w` 匹配 `[a-zA-Z0-9_]`,不匹配中文。如果 JRXML 使用中文字段名,正则返回 0 个匹配。
---
## 5. 验证服务的角色
**文件**`validation_service/main.py`
- XSD 验证:通过(✅)
- 结构检查:字段声明一致性、SQL SELECT 存在性、pageWidth/pageHeight
- **结论**XSD 验证通过,`correct_jrxml` 的 ns0: 消除也生效——真正导致 fail 的是 OCR 保真度评分
---
## 6. 现象描述的矛盾点(需进一步排查)
session `6d39a91e` 中存在数字矛盾:
| 指标 | session 中的值 | 矛盾说明 |
|---|---|---|
| 最终 `current_jrxml` | 24391 字符,142 个 textField | 这是最后一次修正后保存的最终版本 |
| `jrxml_versions[-1].jrxml` | 触发 fail 的真实版本 | 审计团队确认"6d39a91e has 0 text elements causing score=0.00" |
| `error_msg` score | "0.00/1.0" | 对应 `jrxml_versions[-1]`,而非 `current_jrxml` |
**核心矛盾已解决**`current_jrxml`(24391 字符)是**最终状态**(修正耗尽后最后一次保存的版本),而触发 5 次 fail 降级的是 `jrxml_versions` 中各版本的 JRXML——这些版本在修正循环中被逐步侵蚀,最终版本 `jrxml_versions[-1]` 只有 0 个文本元素(score=0.00)。
`fidelity-check-audit` 用 Python 分析的是最终保存的 `current_jrxml`score=0.5),而 `validate-service-audit` 分析的是 `jrxml_versions[-1]`score=0.00)。**两者分析的不是同一个时间点的 JRXML**。
---
## 7. 相关文件清单
| 文件 | 职责 | 备注 |
|---|---|---|
| `agent/nodes.py:1171-1257` | `_check_ocr_fidelity` | 评分逻辑(有 bug |
| `agent/nodes.py:1260-1350` | `validate` 节点 | 调用保真度检查 |
| `agent/nodes.py:1382-1461` | `correct_jrxml` | 修正循环 |
| `backend/ocr_extractor.py` | OCR 字段提取 | 无文档类型区分 |
| `prompts/correction.md` | 修正 prompt | namespace 触发受限 |
| `validation_service/main.py` | 验证服务 | XSD 通过 |
| `sessions/6d39a91e11c54f02bb70a62d856ea2d4.json` | 测试会话 | 主测试数据 |
| `sessions/ecd5921838004ab3bc4a1ef6ebd673d1.json` | 历史会话 | namespace 问题参考 |
+586
View File
@@ -0,0 +1,586 @@
# 对话场景遍历文档
> 从 `agent/graph.py` 状态图递归遍历生成,覆盖所有用户意图 → 节点路径 → 退出条件。
> 最后更新: 2026-05-24
---
## 状态图总览
```
┌──────────────────────────────────────────────────┐
│ 修正循环 (最多 MAX_RETRY=5 次) │
│ ┌─────────┐ ┌──────────────┐ ┌────────┐ │
│ │ validate │───→│ explain_error│───→│correct │ │
│ └────┬─────┘ └──────────────┘ │_jrxml │ │
│ │ pass └───┬────┘ │
│ ▼ │ │
│ ┌─────────┐ retry<5 │
│ │finalize │◄────────────────────────────────┘ │
│ └─────────┘ retry>=5 │
└──────────────────────────────────────────────────┘
load_session ──→ process_input ──→ manage_context ──→ save_state_snapshot
classify_intent
┌────────────┬──────────┬────────┬───────────┼───────────┬──────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
retrieve modify_jrxml save_ handle_ handle_ handle_ (兜底)
(新建报表) (修改报表) session consult undo reset
│ │ (预览) (咨询) (撤销) (重置)
┌────────┴────┐ │ │ │ │ │
▼ ▼ │ │ │ │ │
generate generate_ │ │ │ │ │
(1-shot) skeleton │ │ │ │ │
│ │ │ │ │ │ │
│ refine_ │ │ │ │ │
│ layout │ │ │ │ │
│ │ │ │ │ │ │
│ map_fields │ │ │ │ │
│ │ │ │ │ │ │
└──────┬──────┘ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
save_session ◄─────┴──────────┘ finalize ◄─── finalize ◄── finalize
│ ▲
│ (预览/导出跳过验证) │
├───────────────────────────────────────┘
│ (其他意图走验证)
validate ──→ explain_error ──→ correct_jrxml ──→ validate (循环)
│ pass │ retry>=MAX
▼ ▼
finalize ────────────────────────────────→ finalize
```
---
## 节点详细清单
每个节点标注了 **代码行号** (`agent/nodes.py``agent/graph.py`)、**前驱节点** (predecessors)、**后继节点** (successors)。
### 1. load_session — 加载会话
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:77` |
| 前驱 | (入口节点, graph entry_point) |
| 后继 | `process_input` (固定边 graph.py:198) |
| 功能 | 从 `sessions/{session_id}.json` 磁盘加载状态,注入 agent_state。不从磁盘覆盖 `session_id`。 |
| LLM | 否 |
### 2. process_input — 处理用户输入
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:98` |
| 前驱 | `load_session` (graph.py:198) |
| 后继 | `manage_context` (graph.py:199) |
| 功能 | 文件解析(PDF/DOCX/XLSX/图片/文本)→ OCR 字段提取 → 批注检测 → 模板 JRXML 解析。注入 `ocr_extraction_result``layout_schema``ocr_elements``uploaded_template_jrxml`。 |
| LLM | 否(OCR 用 PaddleOCR/EasyOCR |
### 3. manage_context — 上下文管理
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:143` |
| 前驱 | `process_input` (graph.py:199) |
| 后继 | `save_state_snapshot` (graph.py:200) |
| 功能 | Token 计数 → 对话压缩(超限时 LLM 压缩为摘要)→ `compressed_history`。 |
| LLM | 是(压缩时调 LLM |
### 4. save_state_snapshot — 状态快照
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:191` |
| 前驱 | `manage_context` (graph.py:200) |
| 后继 | `classify_intent` (graph.py:201) |
| 功能 | 深拷贝当前状态 → 推入 `history_states` 列表。最多保留 5 个快照。撤销时恢复到最新快照。 |
| LLM | 否 |
### 5. classify_intent — 意图分类
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:200` |
| 前驱 | `save_state_snapshot` (graph.py:201) |
| 后继 | 6 路条件分发 (graph.py:204-215) |
| 功能 | LLM 分类用户意图为 8 种之一。prompt: `prompts/intent_classify.md`。 |
| LLM | 是 |
| 路由函数 | `route_by_intent` (graph.py:67) |
**分类逻辑与路由目标**:
| 意图值 | 路由目标 | 说明 |
|--------|---------|------|
| `initial_generation` | → `retrieve` | 新建报表 |
| `modify_report` | → `modify_jrxml` | 修改现有报表 |
| `preview_report` | → `save_session` | 预览(跳过生成) |
| `export_pdf` | → `save_session` | 导出 PDF(跳过生成) |
| `export_jrxml` | → `save_session` | 下载 JRXML(跳过生成) |
| `consult_question` | → `handle_consult` | 咨询问答 |
| `undo_modification` | → `handle_undo` | 撤销 |
| `reset_session` | → `handle_reset` | 重置 |
| 未知/兜底 | 有 `current_jrxml``modify_jrxml`; 无 → `retrieve` | |
### 6. retrieve — RAG/知识库检索
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:442` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=initial_generation) |
| 后继 | 条件分发: `generate_skeleton``generate` (graph.py:218-224) |
| 功能 | ① ErrorKB 检索历史修正案例 → ② KB 模板检索 → ③ KB 字段定义检索。注入 `retrieved_context``kb_template_jrxml``kb_fields`。 |
| LLM | 否(向量搜索 + 字段匹配) |
| 路由函数 | `route_after_retrieve` (graph.py:94) |
**路由逻辑** (`route_after_retrieve`, graph.py:94-99):
- `layout_schema.total_rows > 0``generate_skeleton` (3 阶段)
- 否则 → `generate` (1-shot)
### 7. generate — 1-shot 生成
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:578` |
| 前驱 | `retrieve` (graph.py:218-224, 无 layout_schema 时) |
| 后继 | `save_session` (graph.py:227-231) |
| 功能 | LLM 一次生成完整 JRXML。注入 OCR 上下文 + 模板上下文。流式输出。截断时续写(最多 3 轮)。 |
| LLM | 是 |
| Prompt | `prompts/initial_generation.md` |
### 8. generate_skeleton — 骨架生成(3 阶段-1)
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:657` |
| 前驱 | `retrieve` (graph.py:218-224, 有 layout_schema 时) |
| 后继 | `refine_layout` (固定边 graph.py:233) |
| 功能 | 压缩布局 schema → LLM 生成骨架 JRXML。字段用 `$F{field_N}` 占位。流式输出 + 续写。 |
| LLM | 是 |
| Prompt | `prompts/skeleton_generation.md` |
### 9. refine_layout — 坐标精调(3 阶段-2)
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:879` |
| 前驱 | `generate_skeleton` (graph.py:233) |
| 后继 | `map_fields` (固定边 graph.py:234) |
| 功能 | ① `decompose_jrxml()` 拆解为 header + bands → ② 每个 band 窗口化(>4000 字符切分)→ ③ 逐窗口 LLM 精调坐标 → ④ `reassemble_jrxml()` 重组 → ⑤ `validate_element_count()` 校验(>10% 回退)。header 完全不发给 LLM。 |
| LLM | 是(N 次,N = band 窗口数) |
| Prompt | `prompts/refine_layout.md` |
### 10. map_fields — 字段映射(3 阶段-3)
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:978` |
| 前驱 | `refine_layout` (graph.py:234) |
| 后继 | `save_session` (graph.py:235-239) |
| 功能 | 纯程序化正则替换 `$F{field_N}` → OCR 真实字段名。`_sanitize_field_name()` 净化非 ASCII 字符。零 LLM 调用。 |
| LLM | 否 |
### 11. modify_jrxml — 修改报表
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1022` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=modify_report) |
| 后继 | `save_session` (graph.py:242-246) |
| 功能 | 基于现有 JRXML + 用户修改描述 + OCR 上下文 + 模板上下文 → LLM 修改。流式输出 + 续写。空响应守卫。 |
| LLM | 是 |
| Prompt | `prompts/modification.md` |
### 12. handle_consult — 咨询解答
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:261` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=consult_question) |
| 后继 | `finalize` (固定边 graph.py:280) |
| 功能 | LLM 回答 JasperReports 相关知识问题。回答写入 `conversation_history`。 |
| LLM | 是 |
| Prompt | `prompts/consult.md` |
### 13. handle_undo — 撤销
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:281` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=undo_modification) |
| 后继 | `save_session` (graph.py:249-253) |
| 功能 | 从 `history_states` 弹出最近快照,恢复 `current_jrxml``conversation_history``status`。无快照时提示"无可撤销状态"。 |
| LLM | 否 |
### 14. handle_reset — 重置
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:309` |
| 前驱 | `classify_intent` (graph.py:204-215, intent=reset_session) |
| 后继 | `finalize` (固定边 graph.py:281) |
| 功能 | 清空所有状态到 `create_initial_state()` 默认值(保留 `session_id``session_name`)。 |
| LLM | 否 |
### 15. save_session — 保存会话
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:325` |
| 前驱 | `generate``map_fields``modify_jrxml``handle_undo``classify_intent`(预览/导出) |
| 后继 | 条件分发: `validate``finalize` (graph.py:256-260) |
| 功能 | 原子持久化会话 JSON (`tempfile + os.replace`)。序列化 `agent_state``sessions/{session_id}.json`。 |
| LLM | 否 |
| 路由函数 | `route_after_save` (graph.py:118) |
**路由逻辑** (`route_after_save`, graph.py:118-123):
- `intent in (preview_report, export_pdf, export_jrxml)``finalize` (跳过验证)
- 其他 → `validate`
### 16. validate — 验证
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1235` |
| 前驱 | `save_session` (graph.py:256-260)、`correct_jrxml` (graph.py:273-277) |
| 后继 | 条件分发: `finalize``explain_error` (graph.py:263-267) |
| 功能 | ① 结构检查(字段引用一致性/SQL 存在/pageWidth/pageHeight/name)→ ② XSD 校验(可选)→ ③ 像素对比(有上传图片时 Java 渲染 JRXML→PNG + OpenCV SSIM)。 |
| LLM | 否 |
| 路由函数 | `route_after_validate` (graph.py:127) |
**路由逻辑** (`route_after_validate`, graph.py:127-131):
- `status == "pass"``finalize`
- `status == "fail"``explain_error`
### 17. explain_error — 错误解释
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1310` |
| 前驱 | `validate` (graph.py:263-267, status=fail) |
| 后继 | `correct_jrxml` (graph.py:268-272) |
| 功能 | LLM 将编译错误翻译为自然语言解释。注入 `natural_explanation`。 |
| LLM | 是 |
| Prompt | `prompts/explain_error.md` |
### 18. correct_jrxml — 自动修正
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1355` |
| 前驱 | `explain_error` (graph.py:268-272) |
| 后继 | 条件分发: `validate``finalize` (graph.py:273-277) |
| 功能 | 基于错误解释 + OCR 上下文 + 模板上下文 → LLM 修正 JRXML。注入 `last_error_case`。去重检测(输入输出相同则 `retry_count+=2`)。 |
| LLM | 是 |
| Prompt | `prompts/correction.md` |
| 路由函数 | `route_after_correct` (graph.py:139) |
**路由逻辑** (`route_after_correct`, graph.py:139-143):
- `retry_count >= MAX_RETRY` (默认5) → `finalize` (放弃修正)
- `retry_count < MAX_RETRY``validate` (重新验证)
### 19. finalize — 最终处理
| 属性 | 值 |
|------|-----|
| 代码位置 | `agent/nodes.py:1452` |
| 前驱 | `validate`(pass)、`correct_jrxml`(retry>=MAX)、`handle_consult``handle_reset``save_session`(预览/导出) |
| 后继 | `END` (graph.py:284) |
| 功能 | 记录 `jrxml_versions` 版本历史。验证通过时设置 `final_jrxml`。失败时记录 `pending_failure_context` 供下次输入自动注入。 |
| LLM | 否 |
---
## 路由函数索引
| # | 路由函数 | 代码位置 | 条件 | 分支 |
|---|---------|---------|------|------|
| R1 | `route_by_intent` | `graph.py:67` | `state.intent` | 6 路: retrieve / modify_jrxml / save_session / handle_consult / handle_undo / handle_reset |
| R2 | `route_after_retrieve` | `graph.py:94` | `layout_schema.total_rows > 0` | 2 路: generate_skeleton / generate |
| R3 | `route_after_generate` | `graph.py:103` | 无条件 | save_session |
| R4 | `route_after_modify` | `graph.py:108` | 无条件 | save_session |
| R5 | `route_after_undo` | `graph.py:113` | 无条件 | save_session |
| R6 | `route_after_save` | `graph.py:118` | `intent in (preview, export)` | 2 路: finalize / validate |
| R7 | `route_after_validate` | `graph.py:127` | `status == "pass"` | 2 路: finalize / explain_error |
| R8 | `route_after_explain` | `graph.py:133` | 无条件 | correct_jrxml |
| R9 | `route_after_correct` | `graph.py:139` | `retry_count >= MAX_RETRY` | 2 路: finalize / validate |
---
## 完整对话场景
### 场景 1: 新建报表 — 1-shot(无布局 schema
**触发**: `intent=initial_generation` + 无图片/无结构化布局
**用户示例**: "帮我生成一个销售报表"、"生成一个包含客户名和金额的表格"
```
load_session nodes.py:77
→ process_input nodes.py:98
→ manage_context nodes.py:143
→ save_state_snapshot nodes.py:191
→ classify_intent nodes.py:200 意图=initial_generation
└─ R1: route_by_intent graph.py:67 → retrieve
→ retrieve nodes.py:442
└─ R2: route_after_retrieve graph.py:94 layout_schema 为空 → generate
→ generate nodes.py:578 LLM 1-shot 生成完整 JRXML
└─ R3: route_after_generate graph.py:103 → save_session
→ save_session nodes.py:325 持久化到磁盘
└─ R6: route_after_save graph.py:118 intent=initial_generation → validate
→ validate nodes.py:1235 结构检查 + XSD + 像素对比
└─ R7: route_after_validate graph.py:127
├─ status=pass → finalize nodes.py:1452 → END ✓
└─ status=fail → explain_error nodes.py:1310
└─ R8 → correct_jrxml nodes.py:1355
└─ R9:
retry<5 → validate (循环)
retry>=5 → finalize → END ✗
```
**LLM 调用**: `classify_intent` + `generate` + 最多 5× (`explain_error` + `correct_jrxml`)
**退出好结局**: `final_jrxml` 有值, `status=pass`
**退出坏结局**: `pending_failure_context` 有值, `retry_count=5`
---
### 场景 2: 新建报表 — 3 阶段分层生成(有布局 schema)
**触发**: `intent=initial_generation` + 上传图片 + OCR 提取到 `layout_schema.total_rows > 0`
**用户示例**: 上传销售单图片 → "根据这个模板生成报表"
```
load_session nodes.py:77
→ process_input nodes.py:98 OCR提取 + 布局分析
→ manage_context nodes.py:143
→ save_state_snapshot nodes.py:191
→ classify_intent nodes.py:200 意图=initial_generation
└─ R1: route_by_intent graph.py:67 → retrieve
→ retrieve nodes.py:442 KB检索模板+字段
└─ R2: route_after_retrieve graph.py:94 layout_schema.total_rows>0 → generate_skeleton
→ generate_skeleton nodes.py:657 阶段1: 骨架JRXML ($F{field_N}占位)
→ refine_layout nodes.py:879 阶段2: Band级窗口化坐标精调
→ map_fields nodes.py:978 阶段3: 程序化字段映射
└─ R3: route_after_generate graph.py:103 → save_session
→ save_session nodes.py:325
└─ R6: route_after_save graph.py:118 → validate
→ validate nodes.py:1235
└─ R7 同场景1的验证循环
```
**内容保护**:
- `refine_layout`: header (field/param/queryString) 完全不发给 LLM
- `refine_layout`: 每窗口 ~4000 字符, LLM 无法重写整个报表
- `map_fields`: 纯正则替换, 零 LLM, 100% 确定性
- `validate_element_count()`: 每阶段后校验, >10% 变化回退
**LLM 调用**: `classify_intent` + `generate_skeleton` + N×`refine_layout`(N=band窗口数) + 可能的修正循环
---
### 场景 3: 修改已有报表
**触发**: `intent=modify_report`(已有 `current_jrxml`
**用户示例**: "把标题字体改大"、"在底部加合计行"、"删除第三列"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=modify_report
└─ R1: route_by_intent graph.py:67 → modify_jrxml
→ modify_jrxml nodes.py:1022 LLM修改现有JRXML
└─ R4: route_after_modify graph.py:108 → save_session
→ save_session nodes.py:325
└─ R6: route_after_save graph.py:118 → validate
→ (同场景1的验证循环)
```
**特殊逻辑**: `correct_jrxml` 去重检测: 输入输出相同 → `retry_count += 2`
---
### 场景 4: 预览 / 导出(跳过验证)
**触发**: `intent in (preview_report, export_pdf, export_jrxml)`
**用户示例**: "预览报表"、"导出 PDF"、"下载 JRXML"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=preview/export
└─ R1: route_by_intent graph.py:67 → save_session
→ save_session nodes.py:325
└─ R6: route_after_save graph.py:118 intent=preview/export → finalize
→ finalize nodes.py:1452 → END ✓
```
**LLM 调用**: 仅 `classify_intent` (1次)
**跳过**: generate / modify_jrxml / validate / correct_jrxml
---
### 场景 5: 咨询问答
**触发**: `intent=consult_question`
**用户示例**: "JasperReports 里 $F 和 $P 有什么区别?"、"怎么设置页脚?"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=consult_question
└─ R1: route_by_intent graph.py:67 → handle_consult
→ handle_consult nodes.py:261 LLM回答
→ finalize nodes.py:1452 → END ✓
```
**LLM 调用**: `classify_intent` + `handle_consult` (2次)
---
### 场景 6: 撤销
**触发**: `intent=undo_modification`
**用户示例**: "撤销"、"回退"、"恢复到修改前"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=undo_modification
└─ R1: route_by_intent graph.py:67 → handle_undo
→ handle_undo nodes.py:281 恢复history_states快照
└─ R5: route_after_undo graph.py:113 → save_session
→ save_session nodes.py:325
└─ R6 → validate → (验证循环)
```
**LLM 调用**: 仅 `classify_intent` (1次)
**特殊**: 无快照时提示"无可撤销状态",不改变当前状态
---
### 场景 7: 重置
**触发**: `intent=reset_session`
**用户示例**: "重置"、"重新开始"、"清空对话"
```
load_session → process_input → manage_context → save_state_snapshot
→ classify_intent nodes.py:200 意图=reset_session
└─ R1: route_by_intent graph.py:67 → handle_reset
→ handle_reset nodes.py:309 清空到初始状态
→ finalize nodes.py:1452 → END ✓
```
**LLM 调用**: 仅 `classify_intent` (1次)
---
### 场景 8: 兜底路由(未知意图)
**触发**: LLM 分类返回非标准意图
```
load_session → ... → classify_intent → [未知意图]
└─ R1 fallback (graph.py:87-90):
├─ state有current_jrxml → modify_jrxml (走修改路径, →场景3)
└─ state无current_jrxml → retrieve (走生成路径, →场景1/2)
```
---
## AgentState 字段速查
| 字段 | 类型 | 写节点 | 读节点 |
|------|------|--------|--------|
| `intent` | `str` | classify_intent | R1 route_by_intent, R6 route_after_save |
| `current_jrxml` | `str` | generate, generate_skeleton, refine_layout, map_fields, modify_jrxml, correct_jrxml, handle_undo | validate, save_session, finalize |
| `user_input` | `str` | process_input | classify_intent, manage_context |
| `user_modification_request` | `str` | process_input | modify_jrxml |
| `conversation_history` | `list` | process_input, finalize, handle_consult | manage_context, classify_intent, modify_jrxml |
| `full_conversation_history` | `list` | process_input | manage_context |
| `compressed_history` | `str` | manage_context | modify_jrxml, handle_consult |
| `retry_count` | `int` | correct_jrxml, validate | R7 route_after_correct |
| `status` | `str` | validate | R7 route_after_validate, finalize |
| `error_msg` | `str` | validate | explain_error, finalize |
| `natural_explanation` | `str` | explain_error | correct_jrxml |
| `final_jrxml` | `str` | finalize | (用户下载) |
| `jrxml_versions` | `list` | finalize | (前端展示) |
| `last_error_case` | `dict` | correct_jrxml | retrieve |
| `pending_failure_context` | `dict` | finalize | process_input (下次) |
| `layout_schema` | `dict` | process_input | R2 route_after_retrieve, generate_skeleton |
| `ocr_elements` | `list` | process_input | refine_layout, generate_skeleton |
| `ocr_extraction_result` | `dict` | process_input | map_fields, modify_jrxml, correct_jrxml |
| `history_states` | `list` | save_state_snapshot | handle_undo |
| `kb_id` | `str` | process_input | retrieve |
| `kb_fields` | `list` | retrieve | generate_skeleton |
| `uploaded_template_jrxml` | `str` | process_input | generate, generate_skeleton, modify_jrxml, correct_jrxml |
---
## LLM 调用统计
| 场景 | classify | 生成节点 | 窗口数 | 修正循环 | 总计(最小~最大) |
|------|----------|---------|--------|---------|----------------|
| 1-shot 生成 | 1 | generate=1 | - | 0~5×2 | 2 ~ 12 |
| 3 阶段生成 | 1 | skeleton+refine×N | N | 0~5×2 | 2+N ~ 12+N |
| 修改报表 | 1 | modify=1 | - | 0~5×2 | 2 ~ 12 |
| 预览/导出 | 1 | - | - | - | 1 |
| 咨询 | 1 | consult=1 | - | - | 2 |
| 撤销 | 1 | - | - | - | 1 |
| 重置 | 1 | - | - | - | 1 |
> N = band 窗口数。`销售单.jrxml` (73k 字符) 拆解后 N≈17。
---
## 修正循环流程
```
validate ──fail──→ explain_error ──→ correct_jrxml
▲ │
│ retry_count < MAX_RETRY(5) │
└──────────────────────────────────────┘
│ retry_count >= 5
finalize (放弃, 记录pending_failure_context)
```
**修正轮次推进**:
1. `validate` 失败 → `status="fail"`, `error_msg` 有值
2. `explain_error` → LLM 翻译错误 → `natural_explanation` 有值
3. `correct_jrxml` → LLM 修正 → `retry_count += 1`。去重检测:输入输出相同 → `retry_count += 2`
4. `route_after_correct` → retry<5 → 回到 `validate`; retry>=5 → `finalize`
**失败上下文** (`pending_failure_context`): 重试耗尽后记录 `{error_msg, bad_jrxml, retry_count, ts}`,下次用户消息时 `process_input` 自动注入到 prompt。
---
## 边定义索引(graph.py 全部边)
| 类型 | 源节点 | 目标节点 | 位置 |
|------|--------|---------|------|
| 固定边 | load_session | process_input | line 198 |
| 固定边 | process_input | manage_context | line 199 |
| 固定边 | manage_context | save_state_snapshot | line 200 |
| 固定边 | save_state_snapshot | classify_intent | line 201 |
| 条件边 | classify_intent | retrieve / modify_jrxml / save_session / handle_consult / handle_undo / handle_reset | lines 204-215 |
| 条件边 | retrieve | generate / generate_skeleton | lines 218-224 |
| 条件边 | generate | save_session | lines 227-231 |
| 固定边 | generate_skeleton | refine_layout | line 233 |
| 固定边 | refine_layout | map_fields | line 234 |
| 条件边 | map_fields | save_session | lines 235-239 |
| 条件边 | modify_jrxml | save_session | lines 242-246 |
| 条件边 | handle_undo | save_session | lines 249-253 |
| 条件边 | save_session | validate / finalize | lines 256-260 |
| 条件边 | validate | finalize / explain_error | lines 263-267 |
| 条件边 | explain_error | correct_jrxml | lines 268-272 |
| 条件边 | correct_jrxml | validate / finalize | lines 273-277 |
| 固定边 | handle_consult | finalize | line 280 |
| 固定边 | handle_reset | finalize | line 281 |
| 固定边 | finalize | END | line 284 |
+52 -3
View File
@@ -1,5 +1,54 @@
# Vue 3 + TypeScript + Vite
# JRXML Agent 前端
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Vue 3 + TypeScript + Vite + Pinia — JRXML 报表生成代理的 Web UI。
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
## 技术栈
- **Vue 3** (Composition API + `<script setup>`)
- **TypeScript** 6.x
- **Vite** 8.x
- **Pinia** 3.x (状态管理)
- **SSE** (Server-Sent Events) 流式响应
## 组件结构
```
src/
├── api/client.ts SSE 客户端 + fetch 封装
├── stores/
│ ├── chat.ts Pinia: 消息/流式/节点进度/文件
│ ├── session.ts Pinia: 会话 CRUD
│ └── kb.ts Pinia: 多租户知识库管理
├── components/
│ ├── Sidebar.vue 会话列表 + 下载 + 历史版本
│ ├── ChatMessages.vue 消息列表渲染
│ ├── ProcessSection.vue 处理过程折叠区(<details>/<summary>
│ ├── StreamingMessage.vue 流式消息显示
│ ├── NodeProgress.vue 节点进度指示器
│ ├── UnifiedInput.vue 统一输入框(文本+文件拖拽/粘贴)
│ ├── SummaryCard.vue 结果摘要卡片
│ ├── KbSelector.vue KB 下拉选择器
│ └── KbManager.vue KB 管理面板(创建/上传/构建/删除)
└── utils/format.ts 工具函数
```
## 开发
```bash
npm install
npm run dev # 启动开发服务器 (localhost:5173)
npm run build # 生产构建
npx playwright test # E2E 测试
```
## SSE 事件流
前端通过 `api.chat()` 发起 POST 请求,后端返回 `text/event-stream`
| 事件 | 说明 |
|------|------|
| `node_start` | 节点开始执行(含 node/label/step_index |
| `node_complete` | 节点执行完成(含 detail) |
| `stream_token` | LLM 逐字输出 |
| `agent_complete` | 全图执行完成(含 intent/status/jrxml_length/error 等) |
| `agent_error` | 执行异常 |
+87 -5
View File
@@ -12,6 +12,7 @@
"vue": "^3.5.34"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^24.12.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
@@ -66,6 +67,29 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -112,6 +136,22 @@
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
@@ -393,7 +433,6 @@
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1054,7 +1093,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -1083,6 +1121,53 @@
}
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
@@ -1212,7 +1297,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -1234,7 +1318,6 @@
"integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -1319,7 +1402,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz",
"integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.34",
"@vue/compiler-sfc": "3.5.34",
+3 -1
View File
@@ -6,13 +6,15 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test:e2e": "playwright test"
},
"dependencies": {
"pinia": "^3.0.4",
"vue": "^3.5.34"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^24.12.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
timeout: 60000,
expect: { timeout: 10000 },
retries: 0,
use: {
baseURL: "http://localhost:5173",
headless: true,
screenshot: "only-on-failure",
trace: "retain-on-failure",
},
webServer: {
command: "npm run dev",
url: "http://localhost:5173",
reuseExistingServer: true,
timeout: 30000,
},
});
+32 -9
View File
@@ -5,13 +5,22 @@ import { useSessionStore } from './stores/session'
import { api } from './api/client'
import Sidebar from './components/Sidebar.vue'
import ChatMessages from './components/ChatMessages.vue'
import StreamingMessage from './components/StreamingMessage.vue'
import NodeProgress from './components/NodeProgress.vue'
import ProcessSection from './components/ProcessSection.vue'
import SummaryCard from './components/SummaryCard.vue'
import UnifiedInput from './components/UnifiedInput.vue'
import KbSelector from './components/KbSelector.vue'
import KbManager from './components/KbManager.vue'
import { useKbStore } from './stores/kb'
const chat = useChatStore()
const session = useSessionStore()
const kb = useKbStore()
function handleKbChange(kbId: string) {
if (session.currentId) {
kb.bindKbToSession(session.currentId, kbId)
}
}
const chatContainer = ref<HTMLElement | null>(null)
@@ -55,7 +64,7 @@ async function handleSend(text: string, files: File[]) {
try {
await api.chat(session.currentId, text, remoteIds, {
onNodeStart(data) {
chat.addNode(data)
chat.addNode({ node: data.node, label: data.label, step_index: data.step_index })
},
onNodeComplete(data) {
chat.completeNode(data)
@@ -71,7 +80,9 @@ async function handleSend(text: string, files: File[]) {
jrxml_length: data.jrxml_length,
error_msg: data.error_msg,
natural_explanation: data.natural_explanation,
consult_answer: data.consult_answer,
retry_count: data.retry_count,
total_duration_ms: data.total_duration_ms,
ocr_extraction_result: data.ocr_extraction_result,
})
@@ -88,8 +99,12 @@ async function handleSend(text: string, files: File[]) {
type: 'error',
})
} else if (data.intent === 'consult_question') {
if (streamContent) {
chat.addMessage({ role: 'assistant', content: streamContent, type: 'consult' })
// streamContent consult_answer
const answerText = streamContent || data.consult_answer || ''
if (answerText) {
chat.addMessage({ role: 'assistant', content: answerText, type: 'consult' })
} else {
chat.addMessage({ role: 'assistant', content: '咨询已完成,但未获取到回答内容。', type: 'error' })
}
} else {
if (streamContent) {
@@ -98,29 +113,35 @@ async function handleSend(text: string, files: File[]) {
}
// Refresh session sidebar data after a short delay
setTimeout(() => session.refreshFromState({}), 500)
setTimeout(() => session.refreshFromApi(), 500)
},
onAgentError(data) {
chat.setError(data.error)
chat.addMessage({ role: 'assistant', content: `执行异常: ${data.error}`, type: 'error' })
setTimeout(() => session.refreshFromApi(), 500)
},
})
} catch (e: any) {
chat.setError(e.message || '网络请求失败')
chat.addMessage({ role: 'assistant', content: `请求失败: ${e.message}`, type: 'error' })
chat.finishStreaming({ status: '' })
} finally {
if (chat.streaming) {
chat.finishStreaming({ status: '' })
}
}
}
</script>
<template>
<div class="app-layout">
<Sidebar />
<Sidebar @quickAction="(text) => handleSend(text, [])" />
<main class="main-area">
<KbSelector @change="handleKbChange" />
<div class="chat-container" ref="chatContainer">
<ChatMessages />
<StreamingMessage />
<NodeProgress />
<ProcessSection />
<SummaryCard />
</div>
@@ -129,6 +150,8 @@ async function handleSend(text: string, files: File[]) {
@send="handleSend"
/>
</main>
<KbManager />
</div>
</template>
+3 -1
View File
@@ -27,12 +27,14 @@ export interface AgentCompleteData {
jrxml_length: number
error_msg: string
natural_explanation: string
consult_answer: string
retry_count: number
total_duration_ms: number
ocr_extraction_result: any
}
export interface SSECallbacks {
onNodeStart?: (data: { node: string; label: string }) => void
onNodeStart?: (data: { node: string; label: string; step_index: number }) => void
onNodeComplete?: (data: { node: string; label: string; detail: string }) => void
onStreamToken?: (data: { text: string; type: string }) => void
onAgentComplete?: (data: AgentCompleteData) => void
+160
View File
@@ -0,0 +1,160 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useKbStore } from '../stores/kb'
const kb = useKbStore()
const newKbName = ref('')
const newKbDesc = ref('')
const creating = ref(false)
const uploading = ref<string | null>(null)
const building = ref<string | null>(null)
const fileInput = ref<HTMLInputElement | null>(null)
onMounted(() => { kb.init() })
async function handleCreate() {
if (!newKbName.value.trim()) return
creating.value = true
await kb.createKb(newKbName.value.trim(), newKbDesc.value.trim())
newKbName.value = ''
newKbDesc.value = ''
creating.value = false
}
async function handleDelete(kbId: string) {
if (confirm('确定删除此知识库?所有文件和数据将被永久删除。')) {
await kb.deleteKb(kbId)
}
}
function triggerUpload(kbId: string) {
uploading.value = kbId
fileInput.value?.click()
}
async function handleFileSelect(e: Event, kbId: string) {
const input = e.target as HTMLInputElement
if (input.files && input.files.length > 0) {
for (const f of input.files) {
await kb.uploadFileToKb(kbId, f)
}
await kb.buildKb(kbId)
await kb.refreshKbs()
}
input.value = ''
uploading.value = null
}
async function handleBuild(kbId: string) {
building.value = kbId
await kb.buildKb(kbId)
building.value = null
}
</script>
<template>
<Teleport to="body">
<div v-if="kb.showManager" class="kb-manager-overlay" @click.self="kb.showManager = false">
<div class="kb-manager">
<h3>知识库管理</h3>
<div class="create-form">
<input v-model="newKbName" class="kb-input" placeholder="知识库名称" :disabled="creating" />
<input v-model="newKbDesc" class="kb-input" placeholder="描述(可选)" :disabled="creating" />
<button class="kb-btn primary" :disabled="creating || !newKbName.trim()" @click="handleCreate">
{{ creating ? '创建中...' : '创建' }}
</button>
</div>
<div v-if="kb.loading" class="empty">加载中...</div>
<div v-else-if="kb.kbs.length === 0" class="empty">暂无知识库</div>
<div v-for="k in kb.kbs" :key="k.kb_id" class="kb-card">
<div class="kb-card-header">
<strong>{{ k.name }}</strong>
<span class="kb-status" :class="k.parse_status">
{{ k.parse_status === 'ready' ? '就绪' : k.parse_status === 'partial' ? '部分' : '空' }}
</span>
</div>
<div class="kb-meta">
{{ k.field_count }}字段 &middot; {{ k.template_count }}模板 &middot;
{{ k.file_count }}文件 &middot; {{ k.chunk_count }}
</div>
<div class="kb-actions">
<button class="kb-btn" @click="triggerUpload(k.kb_id)" :disabled="uploading === k.kb_id">
{{ uploading === k.kb_id ? '上传中...' : '上传文件' }}
</button>
<button class="kb-btn" @click="handleBuild(k.kb_id)" :disabled="building === k.kb_id || k.file_count === 0">
{{ building === k.kb_id ? '构建中...' : '构建' }}
</button>
<button class="kb-btn danger" @click="handleDelete(k.kb_id)">删除</button>
</div>
</div>
<button class="kb-btn close-btn" @click="kb.showManager = false">关闭</button>
<input ref="fileInput" type="file" multiple
accept=".jrxml,.md,.xlsx,.xls,.docx,.doc,.pdf,.csv,.txt,.json,.zip,.tar,.gz"
style="display:none"
@change="(e: Event) => { const kbId = uploading; if (kbId) handleFileSelect(e, kbId); }" />
</div>
</div>
</Teleport>
</template>
<style scoped>
.kb-manager-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,0.6);
z-index: 100;
display: flex; align-items: center; justify-content: center;
}
.kb-manager {
background: #1e1e2e;
border: 1px solid #45475a;
border-radius: 12px;
padding: 24px;
width: 540px;
max-height: 80vh;
overflow-y: auto;
color: #cdd6f4;
}
h3 { margin: 0 0 16px; font-size: 18px; }
.create-form { display: flex; gap: 8px; margin-bottom: 16px; }
.kb-input {
flex: 1;
background: #181825;
border: 1px solid #45475a;
border-radius: 6px;
color: #cdd6f4;
padding: 6px 10px;
font-size: 13px;
outline: none;
}
.kb-input:focus { border-color: #cba6f7; }
.kb-btn {
background: #313244; border: none; border-radius: 6px;
color: #cdd6f4; padding: 6px 12px; font-size: 12px;
cursor: pointer; white-space: nowrap;
}
.kb-btn:hover:not(:disabled) { background: #45475a; }
.kb-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.kb-btn.primary { background: #cba6f7; color: #1e1e2e; }
.kb-btn.primary:hover:not(:disabled) { background: #b4befe; }
.kb-btn.danger { color: #f38ba8; }
.kb-btn.danger:hover:not(:disabled) { background: #f38ba8; color: #1e1e2e; }
.empty { text-align: center; color: #6c7086; padding: 24px 0; }
.kb-card {
background: #181825; border: 1px solid #313244;
border-radius: 8px; padding: 12px; margin-bottom: 8px;
}
.kb-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.kb-status { font-size: 11px; padding: 1px 6px; border-radius: 4px; }
.kb-status.ready { background: #a6e3a1; color: #1e1e2e; }
.kb-status.partial { background: #fab387; color: #1e1e2e; }
.kb-status.empty { background: #45475a; color: #a6adc8; }
.kb-meta { font-size: 11px; color: #6c7086; margin-bottom: 8px; }
.kb-actions { display: flex; gap: 6px; }
.close-btn { display: block; margin: 16px auto 0; }
</style>
+108
View File
@@ -0,0 +1,108 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useKbStore } from '../stores/kb'
const kb = useKbStore()
const emit = defineEmits<{
change: [kbId: string]
}>()
function handleChange(e: Event) {
const kbId = (e.target as HTMLSelectElement).value
kb.selectKb(kbId)
if (kbId) {
kb.fetchKbFields(kbId)
}
emit('change', kbId)
}
onMounted(() => {
kb.init()
})
</script>
<template>
<div class="kb-selector">
<label class="kb-label">知识库</label>
<select
class="kb-select"
:value="kb.currentKbId"
@change="handleChange"
>
<option value="">-- 不使用知识库 --</option>
<option
v-for="k in kb.kbs"
:key="k.kb_id"
:value="k.kb_id"
>
{{ k.name }} ({{ k.field_count }}字段, {{ k.template_count }}模板)
</option>
</select>
<button class="kb-manage-btn" @click="kb.showManager = !kb.showManager" title="管理知识库">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
</button>
<span v-if="kb.currentKbName" class="kb-badge">当前: {{ kb.currentKbName }}</span>
</div>
</template>
<style scoped>
.kb-selector {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #181825;
border-bottom: 1px solid #313244;
}
.kb-label {
font-size: 12px;
color: #6c7086;
white-space: nowrap;
}
.kb-select {
flex: 1;
background: #1e1e2e;
border: 1px solid #45475a;
border-radius: 6px;
color: #cdd6f4;
padding: 4px 8px;
font-size: 13px;
outline: none;
cursor: pointer;
}
.kb-select:focus {
border-color: #cba6f7;
}
.kb-manage-btn {
background: #313244;
border: none;
color: #a6adc8;
border-radius: 6px;
padding: 4px 6px;
cursor: pointer;
display: flex;
align-items: center;
}
.kb-manage-btn:hover {
background: #45475a;
color: #cdd6f4;
}
.kb-badge {
font-size: 11px;
color: #a6e3a1;
background: #1e1e2e;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
}
</style>
+231
View File
@@ -0,0 +1,231 @@
<script setup lang="ts">
import { useChatStore, type ProcessSection } from '../stores/chat'
const chat = useChatStore()
function sectionClass(s: ProcessSection): string {
if (s.status === 'running') return 'section-running'
if (s.content) return 'section-done'
return 'section-internal'
}
function isXmlLike(text: string): boolean {
return text.includes('<?xml') || text.includes('<jasperReport')
}
</script>
<template>
<div v-if="chat.sections.length > 0" class="process-sections">
<div class="sections-header">
<template v-if="chat.streaming">
<span class="pulse-dot"></span>
处理中 · {{ chat.formatDuration(chat.totalDurationMs) }}
</template>
<template v-else-if="chat.error">
<span class="error-icon"></span>
执行异常 · {{ chat.formatDuration(chat.totalDurationMs) }}
</template>
<template v-else>
<span class="done-icon"></span>
完成 · {{ chat.formatDuration(chat.totalDurationMs) }}
</template>
</div>
<details
v-for="s in chat.sections"
:key="s.node"
:open="s.expanded"
class="process-section"
:class="sectionClass(s)"
@toggle="(e: Event) => { const d = e.target as HTMLDetailsElement; s.expanded = d.open }"
>
<summary class="section-summary">
<span class="step-badge">{{ s.stepIndex }}</span>
<span class="step-label">{{ s.label }}</span>
<span v-if="s.status === 'running'" class="step-spinner">...</span>
<span v-else class="step-check">OK</span>
<span class="step-duration" v-if="s.durationMs > 0">
{{ chat.formatDuration(s.durationMs) }}
</span>
<span class="step-detail-short" v-if="s.status === 'done' && s.detail">
{{ s.detail }}
</span>
</summary>
<div class="section-content" v-if="s.content">
<pre v-if="isXmlLike(s.content)" class="xml-content">{{ s.content }}</pre>
<div v-else class="text-content">{{ s.content }}</div>
</div>
<div v-else-if="s.status === 'running'" class="section-waiting">
等待生成...
</div>
</details>
</div>
</template>
<style scoped>
.process-sections {
margin: 0 24px 8px;
padding: 0 12px;
}
.sections-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #89b4fa;
margin-bottom: 8px;
padding: 0 4px;
}
.pulse-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #89b4fa;
animation: pulse 1.5s infinite;
}
.error-icon {
color: #f38ba8;
font-weight: bold;
font-size: 12px;
}
.done-icon {
color: #a6e3a1;
font-weight: bold;
font-size: 12px;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.process-section {
margin-bottom: 4px;
border-radius: 8px;
border: 1px solid #313244;
background: #181825;
transition: background 0.2s;
}
.process-section.section-running {
border-color: #45475a;
background: #1e1e2e;
}
.process-section.section-internal {
opacity: 0.65;
font-size: 11px;
}
.section-summary {
padding: 6px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
list-style: none;
user-select: none;
}
.section-summary::-webkit-details-marker {
display: none;
}
.step-badge {
background: #313244;
color: #6c7086;
border-radius: 4px;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.section-running .step-badge {
background: #45475a;
color: #cba6f7;
}
.section-done .step-badge {
background: #313244;
color: #a6e3a1;
}
.step-label {
color: #cdd6f4;
font-weight: 500;
}
.section-internal .step-label {
color: #6c7086;
}
.step-spinner {
color: #f9e2af;
font-weight: bold;
animation: pulse-text 2s ease-in-out infinite;
}
@keyframes pulse-text {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.step-check {
color: #a6e3a1;
font-weight: bold;
font-size: 11px;
}
.step-duration {
margin-left: auto;
font-size: 11px;
color: #6c7086;
font-family: monospace;
}
.step-detail-short {
font-size: 11px;
color: #6c7086;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.section-content {
padding: 0 10px 10px 10px;
}
.xml-content {
background: #11111b;
padding: 10px;
border-radius: 6px;
font-size: 11px;
overflow-x: auto;
white-space: pre-wrap;
color: #a6e3a1;
max-height: 300px;
overflow-y: auto;
}
.text-content {
color: #bac2de;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
}
.section-waiting {
padding: 8px 10px 12px;
font-size: 12px;
color: #6c7086;
font-style: italic;
}
</style>
+130 -34
View File
@@ -6,6 +6,22 @@ import { useChatStore } from '../stores/chat'
const session = useSessionStore()
const chat = useChatStore()
const emit = defineEmits<{
quickAction: [text: string]
}>()
function handlePreview() {
emit('quickAction', '预览报表')
}
function handleUndo() {
emit('quickAction', '撤销上一步修改')
}
function handleReset() {
emit('quickAction', '重新来,清空当前报表')
}
onMounted(() => {
session.loadSessions()
})
@@ -63,15 +79,50 @@ async function handleDelete() {
</button>
</div>
<div class="sidebar-section" v-if="session.currentJrxml">
<div class="sidebar-section" v-if="session.currentId">
<div class="section-title">快捷操作</div>
<div class="quick-actions">
<button
class="btn-action btn-preview"
:disabled="!session.hasJrxml"
@click="handlePreview"
>预览</button>
<button
class="btn-action btn-undo"
:disabled="!session.hasHistory"
@click="handleUndo"
>撤销</button>
<button
class="btn-action btn-reset"
@click="handleReset"
>重置</button>
</div>
</div>
<div class="sidebar-section">
<div class="section-title">下载</div>
<a
v-if="session.currentJrxml"
:href="`/api/sessions/${session.currentId}/download/latest`"
class="btn-download"
download
>
下载最新 JRXML
</a>
<div v-else class="btn-download disabled">暂无下载文件</div>
<div v-if="session.versions.length > 1" class="version-list">
<div class="version-list-title">历史版本</div>
<a
v-for="(v, i) in session.versions"
:key="i"
:href="`/api/sessions/${session.currentId}/download/${i}`"
class="version-item"
download
>
<span class="version-label">{{ v.label || `版本 ${i + 1}` }}</span>
<span class="version-time">{{ v.ts?.slice(0, 16)?.replace('T', ' ') }}</span>
</a>
</div>
</div>
<div class="sidebar-footer">
@@ -131,85 +182,130 @@ async function handleDelete() {
cursor: pointer;
font-size: 16px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: #45475a;
}
.btn-icon:hover { background: #45475a; }
.session-list {
max-height: 300px;
overflow-y: auto;
padding: 0 8px;
}
.session-item {
padding: 8px 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
padding: 8px 8px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.session-item:hover {
background: #313244;
}
.session-item.active {
background: #45475a;
border-left: 3px solid #cba6f7;
padding-left: 13px;
}
.session-item:hover { background: #313244; }
.session-item.active { background: #45475a; }
.session-name {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 150px;
flex: 1;
}
.session-time {
font-size: 11px;
color: #6c7086;
margin-left: 8px;
flex-shrink: 0;
}
.btn-delete {
display: block;
width: calc(100% - 32px);
margin: 8px 16px 0;
padding: 6px 12px;
border: 1px solid #f38ba8;
background: transparent;
width: calc(100% - 16px);
margin: 8px 8px 0;
padding: 6px 0;
border: 1px solid #45475a;
background: none;
color: #f38ba8;
border-radius: 6px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
text-align: center;
}
.btn-delete:hover { background: #45475a; }
.quick-actions {
display: flex;
gap: 8px;
padding: 0 16px;
}
.btn-delete:hover {
background: #f38ba8;
color: #1e1e2e;
.btn-action {
flex: 1;
padding: 6px 0;
border: 1px solid #45475a;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
color: #cdd6f4;
background: none;
transition: background 0.15s;
}
.btn-action:hover:not(:disabled) { background: #45475a; }
.btn-action:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-preview { border-color: #a6e3a1; color: #a6e3a1; }
.btn-undo { border-color: #f9e2af; color: #f9e2af; }
.btn-reset { border-color: #f38ba8; color: #f38ba8; }
.btn-download {
display: block;
padding: 8px 16px;
color: #a6e3a1;
margin: 4px 16px;
padding: 8px 0;
background: #cba6f7;
color: #1e1e2e;
text-align: center;
border-radius: 4px;
text-decoration: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.btn-download:hover { background: #b4befe; }
.btn-download.disabled {
background: #313244;
color: #6c7086;
cursor: not-allowed;
font-weight: 400;
}
.btn-download:hover {
background: #313244;
.version-list {
margin-top: 8px;
padding: 0 16px;
}
.version-list-title {
font-size: 11px;
color: #6c7086;
margin-bottom: 4px;
}
.version-item {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 12px;
color: #a6adc8;
text-decoration: none;
}
.version-item:hover { color: #cba6f7; }
.version-time { font-size: 11px; color: #6c7086; }
.sidebar-footer {
margin-top: auto;
padding: 12px 16px;
font-size: 11px;
color: #6c7086;
color: #585b70;
border-top: 1px solid #313244;
}
</style>
+14 -1
View File
@@ -21,13 +21,21 @@ function downloadLatest() {
<div v-if="visible" class="summary-card">
<div v-if="chat.summary.status === 'pass'" class="card card-success">
<div class="card-title">JRXML 生成成功</div>
<div class="card-text">生成 {{ chat.summary.jrxml_length }} 字符</div>
<div class="card-text">
生成 {{ chat.summary.jrxml_length }} 字符
<span v-if="chat.lastDurationMs > 0" class="card-duration">
· {{ chat.formatDuration(chat.lastDurationMs) }}
</span>
</div>
<button class="card-btn" @click="downloadLatest">下载 JRXML</button>
</div>
<div v-else class="card card-error">
<div class="card-title">
经过 {{ chat.summary.retry_count }} 次重试后仍失败
<span v-if="chat.lastDurationMs > 0" class="card-duration">
· {{ chat.formatDuration(chat.lastDurationMs) }}
</span>
</div>
<div class="card-text">{{ chat.summary.error_msg }}</div>
<div v-if="chat.summary.natural_explanation" class="card-reason">
@@ -79,6 +87,11 @@ function downloadLatest() {
margin-bottom: 4px;
}
.card-duration {
color: #6c7086;
font-size: 12px;
}
.card-reason {
font-size: 12px;
color: #a6adc8;
+1 -1
View File
@@ -140,7 +140,7 @@ function handlePaste(e: ClipboardEvent) {
ref="fileInputRef"
type="file"
multiple
accept="image/*,.pdf,.docx,.doc,.xlsx,.xls,.txt,.csv"
accept="image/*,.pdf,.docx,.doc,.xlsx,.xls,.txt,.csv,.jrxml"
style="display:none"
@change="handleFileSelect"
/>
+105 -8
View File
@@ -1,7 +1,7 @@
/** Pinia store — chat messages + streaming state. */
/** Pinia store — chat messages + streaming state with per-section tracking. */
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, computed } from 'vue'
export interface Message {
id: string
@@ -18,6 +18,18 @@ export interface NodeProgress {
status: 'running' | 'done'
}
export interface ProcessSection {
node: string
label: string
stepIndex: number
detail: string
content: string
status: 'running' | 'done'
expanded: boolean
durationMs: number
startTime: number
}
export interface AgentSummary {
intent: string
status: string
@@ -27,18 +39,45 @@ export interface AgentSummary {
retry_count: number
}
export interface UploadedFile {
file_id: string
filename: string
content_type: string
size: number
preview?: string
}
export const useChatStore = defineStore('chat', () => {
const messages = ref<Message[]>([])
const streaming = ref(false)
const lastDurationMs = ref(0)
const streamText = ref('')
const nodes = ref<NodeProgress[]>([])
const sections = ref<ProcessSection[]>([])
const error = ref<string>('')
const ocrResult = ref<any>(null)
const uploadedFiles = ref<UploadedFile[]>([])
const summary = ref<AgentSummary>({
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
})
const totalDurationMs = computed(() => {
if (sections.value.length === 0) return 0
const last = sections.value[sections.value.length - 1]
return last.status === 'done'
? last.startTime + last.durationMs - sections.value[0].startTime
: Date.now() - sections.value[0].startTime
})
function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
const m = Math.floor(ms / 60000)
const s = Math.round((ms % 60000) / 1000)
return `${m}m${s}s`
}
function addMessage(msg: Omit<Message, 'id' | 'timestamp'>) {
messages.value.push({
...msg,
@@ -49,8 +88,10 @@ export const useChatStore = defineStore('chat', () => {
function startStreaming() {
streaming.value = true
lastDurationMs.value = 0
streamText.value = ''
nodes.value = []
sections.value = []
error.value = ''
summary.value = {
intent: '', status: '', jrxml_length: 0,
@@ -60,10 +101,31 @@ export const useChatStore = defineStore('chat', () => {
function appendStreamToken(text: string) {
streamText.value += text
const active = sections.value.find(s => s.status === 'running')
if (active) {
active.content += text
}
}
function addNode(node: { node: string; label: string }) {
nodes.value.push({ ...node, status: 'running' })
function addNode(node: { node: string; label: string; step_index?: number }) {
nodes.value.push({ node: node.node, label: node.label, status: 'running' })
const prev = sections.value.find(s => s.status === 'running')
if (prev) {
prev.status = 'done'
prev.durationMs = Date.now() - prev.startTime
prev.expanded = false
}
sections.value.push({
node: node.node,
label: node.label,
stepIndex: node.step_index || sections.value.length + 1,
detail: '',
content: '',
status: 'running',
expanded: true,
durationMs: 0,
startTime: Date.now(),
})
}
function completeNode(node: { node: string; label: string; detail: string }) {
@@ -72,22 +134,37 @@ export const useChatStore = defineStore('chat', () => {
existing.status = 'done'
existing.detail = node.detail
}
const sec = sections.value.find(s => s.node === node.node && s.status === 'running')
if (sec) {
sec.detail = node.detail
sec.status = 'done'
sec.durationMs = Date.now() - sec.startTime
}
}
function finishStreaming(data?: {
intent?: string; status?: string; jrxml_length?: number
error_msg?: string; natural_explanation?: string; retry_count?: number
ocr_extraction_result?: any
error_msg?: string; natural_explanation?: string; consult_answer?: string; retry_count?: number
total_duration_ms?: number; ocr_extraction_result?: any
}) {
streaming.value = false
nodes.value.forEach(n => { n.status = 'done' })
sections.value.forEach(s => {
if (s.status === 'running') {
s.status = 'done'
s.durationMs = Date.now() - s.startTime
}
s.expanded = false
})
if (data) {
lastDurationMs.value = data.total_duration_ms || 0
summary.value = {
intent: data.intent || '',
status: data.status || '',
jrxml_length: data.jrxml_length || 0,
error_msg: data.error_msg || '',
natural_explanation: data.natural_explanation || '',
consult_answer: data.consult_answer || '',
retry_count: data.retry_count || 0,
}
if (data.ocr_extraction_result) {
@@ -99,15 +176,33 @@ export const useChatStore = defineStore('chat', () => {
function setError(err: string) {
error.value = err
streaming.value = false
sections.value.forEach(s => { s.status = 'done'; s.expanded = false })
}
function toggleSection(node: string) {
const sec = sections.value.find(s => s.node === node)
if (sec) {
sec.expanded = !sec.expanded
}
}
function addUploadedFile(file: UploadedFile) {
uploadedFiles.value.push(file)
}
function removeUploadedFile(fileId: string) {
uploadedFiles.value = uploadedFiles.value.filter(f => f.file_id !== fileId)
}
function reset() {
messages.value = []
streamText.value = ''
nodes.value = []
sections.value = []
error.value = ''
streaming.value = false
ocrResult.value = null
uploadedFiles.value = []
summary.value = {
intent: '', status: '', jrxml_length: 0,
error_msg: '', natural_explanation: '', retry_count: 0,
@@ -115,8 +210,10 @@ export const useChatStore = defineStore('chat', () => {
}
return {
messages, streaming, streamText, nodes, error, ocrResult, summary,
messages, streaming, lastDurationMs, streamText, nodes, sections, error, ocrResult,
uploadedFiles, summary, totalDurationMs,
addMessage, startStreaming, appendStreamToken, addNode, completeNode,
finishStreaming, setError, reset,
finishStreaming, setError, toggleSection, reset, formatDuration,
addUploadedFile, removeUploadedFile,
}
})
+152
View File
@@ -0,0 +1,152 @@
/** Pinia store — multi-tenant KB management. */
import { ref } from 'vue'
import { defineStore } from 'pinia'
export interface KbSummary {
kb_id: string
name: string
description: string
created_at: string
updated_at: string
field_count: number
template_count: number
file_count: number
chunk_count: number
parse_status: string
}
export interface UserInfo {
user_id: string
name: string
created_at: string
}
export interface KbTemplate {
name: string
file: string
}
export interface KbField {
name: string
description: string
type: string
required: boolean
}
export const useKbStore = defineStore('kb', () => {
const users = ref<UserInfo[]>([])
const currentUserId = ref('')
const kbs = ref<KbSummary[]>([])
const currentKbId = ref('')
const currentKbName = ref('')
const currentKbFields = ref<KbField[]>([])
const currentKbTemplates = ref<KbTemplate[]>([])
const loading = ref(false)
const showManager = ref(false)
function setKbs(list: KbSummary[]) { kbs.value = list }
function selectKb(kbId: string) {
currentKbId.value = kbId
const kb = kbs.value.find(k => k.kb_id === kbId)
if (kb) currentKbName.value = kb.name
}
async function refreshUsers() {
try {
const r = await fetch('/api/users')
const data = await r.json()
users.value = data.users || []
if (users.value.length > 0 && !currentUserId.value) {
currentUserId.value = users.value[0].user_id
}
} catch (e) { console.error('获取用户列表失败:', e) }
}
async function refreshKbs(userId?: string) {
const uid = userId || currentUserId.value
if (!uid) return
loading.value = true
try {
const r = await fetch(`/api/users/${uid}/kbs`)
const data = await r.json()
kbs.value = data.kbs || []
} catch (e) { console.error('获取知识库列表失败:', e) }
finally { loading.value = false }
}
async function createKb(name: string, description = ''): Promise<KbSummary | null> {
if (!currentUserId.value) return null
try {
const r = await fetch(`/api/users/${currentUserId.value}/kbs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description }),
})
if (!r.ok) throw new Error('创建失败')
const kb = await r.json()
await refreshKbs()
return kb
} catch (e) { console.error('创建知识库失败:', e); return null }
}
async function deleteKb(kbId: string): Promise<boolean> {
try {
const r = await fetch(`/api/kbs/${kbId}`, { method: 'DELETE' })
if (!r.ok) throw new Error('删除失败')
if (currentKbId.value === kbId) { currentKbId.value = ''; currentKbName.value = '' }
await refreshKbs()
return true
} catch (e) { console.error('删除知识库失败:', e); return false }
}
async function uploadFileToKb(kbId: string, file: File): Promise<boolean> {
try {
const form = new FormData()
form.append('file', file)
const r = await fetch(`/api/kbs/${kbId}/upload`, { method: 'POST', body: form })
return r.ok
} catch (e) { console.error('KB文件上传失败:', e); return false }
}
async function buildKb(kbId: string): Promise<boolean> {
try {
const r = await fetch(`/api/kbs/${kbId}/build`, { method: 'POST' })
if (!r.ok) throw new Error('构建失败')
await refreshKbs()
return true
} catch (e) { console.error('KB构建失败:', e); return false }
}
async function fetchKbFields(kbId: string) {
try {
const r = await fetch(`/api/kbs/${kbId}/fields`)
const data = await r.json()
currentKbFields.value = data.fields || []
currentKbTemplates.value = data.templates || []
} catch (e) { console.error('获取KB字段失败:', e) }
}
async function bindKbToSession(sessionId: string, kbId: string) {
try {
await fetch(`/api/sessions/${sessionId}/kb`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kb_id: kbId }),
})
} catch (e) { console.error('绑定KB失败:', e) }
}
async function init() {
await refreshUsers()
if (currentUserId.value) await refreshKbs()
}
return {
users, currentUserId, kbs, currentKbId, currentKbName,
currentKbFields, currentKbTemplates, loading, showManager,
setKbs, selectKb, refreshUsers, refreshKbs, createKb, deleteKb,
uploadFileToKb, buildKb, fetchKbFields, bindKbToSession, init,
}
})
+22 -3
View File
@@ -9,8 +9,12 @@ export const useSessionStore = defineStore('session', () => {
const currentId = ref<string>('')
const currentName = ref<string>('')
const versions = ref<any[]>([])
const historyStates = ref<any[]>([])
const currentJrxml = ref<string>('')
const hasJrxml = computed(() => !!currentJrxml.value)
const hasHistory = computed(() => historyStates.value.length > 0)
const sortedSessions = computed(() =>
[...sessions.value].sort((a, b) =>
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
@@ -43,6 +47,7 @@ export const useSessionStore = defineStore('session', () => {
const state = data.agent_state
currentJrxml.value = state.current_jrxml || ''
versions.value = state.jrxml_versions || []
historyStates.value = state.history_states || []
} catch (e) {
console.error('加载会话失败:', e)
}
@@ -58,14 +63,28 @@ export const useSessionStore = defineStore('session', () => {
versions.value = []
}
async function refreshFromApi() {
if (!currentId.value) return
try {
const data = await api.getSession(currentId.value)
const state = data.agent_state
currentJrxml.value = state.current_jrxml || ''
versions.value = state.jrxml_versions || []
historyStates.value = state.history_states || []
} catch (e) {
console.error('刷新会话状态失败:', e)
}
}
function refreshFromState(agentState: Record<string, any>) {
currentJrxml.value = agentState.current_jrxml || currentJrxml.value
versions.value = agentState.jrxml_versions || versions.value
historyStates.value = agentState.history_states || historyStates.value
}
return {
sessions, currentId, currentName, versions, currentJrxml,
sortedSessions, currentSession,
loadSessions, createSession, switchSession, deleteCurrent, refreshFromState,
sessions, currentId, currentName, versions, historyStates, currentJrxml,
hasJrxml, hasHistory, sortedSessions, currentSession,
loadSessions, createSession, switchSession, deleteCurrent, refreshFromState, refreshFromApi,
}
})
+320
View File
@@ -0,0 +1,320 @@
/**
* E2E tests: key user flows for the JRXML Agent frontend.
*
* Pre-requisites: npm run dev (reuseExistingServer in playwright.config).
* API calls are intercepted by page.route() no real backend needed.
*/
import { test, expect } from "@playwright/test";
// ── helpers ────────────────────────────────────────────────────
function mockApi(page: any) {
page.route("**/api/health", (route: any) =>
route.fulfill({ json: { status: "ok", version: "5.0" } })
);
page.route("**/api/sessions", (route: any) => {
if (route.request().method() === "POST") {
return route.fulfill({
json: {
session_id: "test12345678",
session_name: "新建报表 2026-05-22",
created_at: "2026-05-22T10:00:00.000Z",
updated_at: "2026-05-22T10:00:00.000Z",
},
});
}
return route.fulfill({ json: { sessions: [] } });
});
page.route("**/api/sessions/*/chat", (route: any) => {
const sseBody = [
"event: node_start",
'data: {"node":"classify_intent","label":"识别意图","step_index":1}',
"",
"event: node_complete",
'data: {"node":"classify_intent","label":"识别意图","detail":"意图: 新建报表"}',
"",
"event: agent_complete",
'data: {"reason":"done","intent":"initial_generation","status":"pass","jrxml_length":42,"versions":1,"total_duration_ms":1200}',
"",
"",
].join("\n");
return route.fulfill({
status: 200,
headers: { "content-type": "text/event-stream" },
body: sseBody,
});
});
page.route("**/api/upload", (route: any) =>
route.fulfill({
json: { file_id: "f001122334455", filename: "test.png", size: 1024 },
})
);
// Catch-all for GET/DELETE /api/sessions/:id (must fallback for POST to let chat route match)
page.route("**/api/sessions/**", (route: any) => {
if (route.request().method() === "DELETE") {
return route.fulfill({ json: { status: "deleted" } });
}
if (route.request().method() === "GET") {
return route.fulfill({
json: {
session_id: "test12345678",
session_name: "测试会话",
agent_state: { current_jrxml: "<jasperReport/>" },
},
});
}
return route.fallback();
});
}
// ── tests ──────────────────────────────────────────────────────
test.describe("Page load", () => {
test("renders sidebar and input area", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await expect(page.locator("aside.sidebar")).toBeVisible();
await expect(page.locator("h2")).toContainText("JRXML");
await expect(page.locator(".unified-input")).toBeVisible();
});
test("sidebar shows session list header and new button", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await expect(page.getByText("会话列表")).toBeVisible();
await expect(page.locator('button[title="新建会话"]')).toBeVisible();
});
});
test.describe("Session management", () => {
test("creates new session on button click", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await page.locator('button[title="新建会话"]').click();
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
await expect(page.locator(".session-item.active")).toBeVisible();
});
test("can delete current session", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await page.locator('button[title="新建会话"]').click();
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
page.on("dialog", (dialog) => dialog.accept());
await page.locator(".btn-delete").click();
await expect(page.locator(".session-item")).toHaveCount(0, { timeout: 5000 });
});
});
test.describe("Chat flow", () => {
test("sends text and displays user message + process section", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await page.locator('button[title="新建会话"]').click();
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
const textarea = page.locator(".unified-input textarea");
await textarea.fill("生成一个员工名册报表");
await page.locator(".send-btn").click();
await expect(
page.locator(".chat-messages .message.msg-user").filter({ hasText: "员工名册" })
).toBeVisible({ timeout: 10000 });
await expect(page.locator(".process-section")).toBeVisible({ timeout: 10000 });
});
test("summary card appears after stream complete", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await page.locator('button[title="新建会话"]').click();
await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 });
await page.locator(".unified-input textarea").fill("生成报表");
await page.locator(".send-btn").click();
await expect(page.locator(".summary-card")).toBeVisible({ timeout: 15000 });
});
});
test.describe("Input UX", () => {
test("send button disabled when input empty", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await expect(page.locator(".send-btn")).toBeDisabled();
});
test("send button enabled when text entered", async ({ page }) => {
await mockApi(page);
await page.goto("/");
await page.locator(".unified-input textarea").fill("Hi");
await expect(page.locator(".send-btn")).toBeEnabled();
});
});
// ── KB (Knowledge Base) API mocks ───────────────────────────────
function mockKbApi(page: any) {
mockApi(page);
page.route("**/api/users", (route: any) => {
if (route.request().method() === "POST") {
return route.fulfill({
json: { user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" },
});
}
return route.fulfill({
json: { users: [{ user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" }] },
});
});
page.route("**/api/users/*/kbs", (route: any) => {
if (route.request().method() === "POST") {
return route.fulfill({
json: {
kb_id: "kb_e2e_001", user_id: "u_e2e_test_001",
name: "E2E测试库", description: "",
created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z",
fields: [], templates: [], file_count: 0, chunk_count: 0, parse_status: "empty",
},
});
}
return route.fulfill({
json: {
kbs: [{
kb_id: "kb_e2e_001", name: "E2E测试库", description: "",
created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z",
field_count: 10, template_count: 3, file_count: 2, chunk_count: 50, parse_status: "ready",
}],
},
});
});
page.route("**/api/kbs/*/status", (route: any) =>
route.fulfill({ json: { parse_status: "ready", file_count: 2, chunk_count: 50 } })
);
page.route("**/api/kbs/*/fields", (route: any) =>
route.fulfill({ json: {
fields: [{ name: "billNo", description: "工单号", type: "String" }],
templates: [{ name: "结算单", file: "结算单.jrxml" }],
}})
);
page.route("**/api/kbs/*", (route: any) => {
if (route.request().method() === "DELETE") {
return route.fulfill({ json: { status: "deleted" } });
}
return route.fulfill({
json: {
kb_id: "kb_e2e_001", user_id: "u_e2e_test_001",
name: "E2E测试库", description: "",
fields: [], templates: [],
file_count: 2, chunk_count: 50, parse_status: "ready",
},
});
});
page.route("**/api/kbs/*/upload", (route: any) =>
route.fulfill({ json: { filename: "test.jrxml", type: "jrxml", error: null } })
);
page.route("**/api/sessions/*/kb", (route: any) => {
if (route.request().method() === "PUT") {
return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } });
}
return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } });
});
}
// ── KB feature tests ────────────────────────────────────────────
test.describe("KB selector", () => {
test("KB selector renders in chat interface", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await expect(page.locator(".kb-selector")).toBeVisible({ timeout: 5000 });
await expect(page.locator(".kb-label")).toContainText("知识库");
await expect(page.locator(".kb-select")).toBeVisible();
await expect(page.locator(".kb-manage-btn")).toBeVisible();
});
test("can select a KB from dropdown", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
const select = page.locator(".kb-select");
await expect(select).toBeVisible({ timeout: 5000 });
await select.selectOption({ label: "E2E测试库 (10字段, 3模板)" });
await expect(page.locator(".kb-badge")).toContainText("E2E测试库");
});
});
test.describe("KB manager", () => {
test("opens KB manager overlay", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await expect(page.locator(".kb-manager h3")).toContainText("知识库管理");
});
test("can close KB manager", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await page.locator(".close-btn").click();
await expect(page.locator(".kb-manager")).not.toBeVisible({ timeout: 3000 });
});
test("create form has name input and create button", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await expect(page.locator('.kb-manager .create-form .kb-input').first()).toBeVisible();
await expect(page.locator('.kb-manager .create-form button.primary')).toBeVisible();
});
test("KB cards show name, status, and actions", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
await page.locator(".kb-manage-btn").click();
await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 });
await expect(page.locator(".kb-card")).toBeVisible({ timeout: 3000 });
await expect(page.locator(".kb-card strong")).toContainText("E2E测试库");
await expect(page.locator(".kb-status.ready")).toContainText("就绪");
await expect(page.locator(".kb-actions button")).toHaveCount(3);
});
});
test.describe("JRXML upload in chat", () => {
test("file input accepts .jrxml extension", async ({ page }) => {
await mockKbApi(page);
await page.goto("/");
const input = page.locator('.unified-input input[type="file"]');
await expect(input).toHaveAttribute("accept", /\.jrxml/);
});
});
@@ -0,0 +1,13 @@
{
"kb_id": "49b972ec9e424f04aec34899c978f087",
"user_id": "2db10c2ebbf6434aab28035026e196c3",
"name": "smoke_kb",
"description": "",
"created_at": "2026-05-23T12:21:32.409028+00:00",
"updated_at": "2026-05-23T12:21:32.409028+00:00",
"fields": [],
"templates": [],
"file_count": 0,
"chunk_count": 0,
"parse_status": "empty"
}
@@ -0,0 +1,5 @@
{
"user_id": "2db10c2ebbf6434aab28035026e196c3",
"name": "SmokeTest",
"created_at": "2026-05-23T12:21:32.399217+00:00"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
# 保险单接口文档
# 保险单接口文档
打印平台模版分类:保险单
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| companyTitle | 打印title | | String |
| customerName | 客户姓名 | | String |
| carNo | 车牌号 | | String |
| vin | vin码 | | String |
| carModel | 车型 | | String |
| cellPhone | 手机号 | | String |
| insureDate | 起保日期 | | String yyyy-MM-dd |
| receivable | 应收金额 | | BigDecimal |
| preferentialAmount | 优惠金额 | | BigDecimal |
| receiveAmount | 实收金额 | | BigDecimal |
| oweAmount | 未收金额 | | BigDecimal |
| commissionAmountTotal | 手续费 | | BigDecimal |
| companyRefundAmount | 保险公司返点 | | BigDecimal |
| customerRefundAmount | 客户返点 | | BigDecimal |
| insuranceCompanyName | 承保公司 | | String |
| memo | 备注 | | String |
| employeeName | 服务顾问 | | String |
| contacts | 保险公司联系人 | | String |
| contactMobile | 保险公司联系人手机号 | | String |
| channelName | 来店途径 | | String |
| startDate | 开始日期 | | String yyyy-MM-dd |
| endDate | 结束日期 | | String yyyy-MM-dd |
| renewal | 是否续保 0否/1是 | | Integer |
| tsInsuranceDetailList | 保险单明细 | | List<TsInsuranceDetailPrintVo> |
| | | | |
| <br/><br/> | | | |
TsInsuranceDetailPrintVo
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| policyNo | 保单号 | | String |
| insuranceType | 保险类型(0交强险/1商业险) | | Integer |
| name | 险种名称 | | String |
| amount | 保额 | | BigDecimal |
| receivable | 应收金额(元) | | BigDecimal |
| discount | 折扣 | | BigDecimal |
| concessionary | 优惠金额(元) | | BigDecimal |
| commissionRate | 手续费率 | | BigDecimal |
| commissionAmount | 手续费 | | BigDecimal |
| paid | 实收金额(元) | | BigDecimal |
| memo | 备注 | | String |
| companyRefundAmount | 保险公司返点 | | BigDecimal |
| customerRefundAmount | 客户返点 | | BigDecimal |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
| <br/><br/> | | | |
> 更新: 2023-09-20 11:32:29 原文: <https://xcz.yuque.com/ombipo/rpc7ms/fpzmr5qph5mloy1x>
@@ -0,0 +1,420 @@
# 出/入库单据打印
# 现状梳理
| **场景** | | 入口 | 打印效果 | **底层模版** | 接口 |
| --- | --- | --- | --- | --- | --- |
| 库存 | 出入库单据-出库单 | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739783099448-84de3a8e-2473-4d88-a83d-638122d695ac.png) | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739782451805-24599a5e-df6e-4d6b-bbfb-cbbc1816614f.png) | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739782672560-9e9927d9-bdbd-4ad1-9199-f6588fc11e9d.png) | REST/stock/stockInAndOutBill/stockOutPrint?idStock=XXX&isNew=true<br>底层接口:com.f6car.stock.service.impl.print.PrintServiceImpl#getStockOutPrintUrl<br>日志关键字:+"出库单打印参数:" (有 apollo 开关 log.stockInout.print.switch 默认true |
| | 领料出库(工单领料)-出库单 | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739782990039-cc493301-06f6-48f0-bd0d-76487b30d04e.png)![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739783038197-1924e595-433a-48b7-8ea2-291492a7caae.png) | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739783054922-543a7d3c-f0eb-4814-874d-34bb82aed7cd.png) |
| | 出入库单据-入库单 | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/f5893832-29ca-4164-9071-78371d724dbd.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/09773831-4ef6-4c46-b04a-0b00ea6376b7.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/f4b0a5b6-b259-4b41-91ec-67ed895c9c61.png) | REST/stock/stockInAndOutBill/stockInPrint?idStock=XXX&isNew=true<br>底层接口:com.f6car.stock.service.impl.print.PrintServiceImpl#getStockInPrintUrl<br>日志关键字:+"入库单打印参数:" (有 apollo 开关 log.stockInout.print.switch 默认true |
| | 领料出库(工单领料)-退料单 | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/b3aaf9d0-5231-4dd2-bbb4-a3f02afbfb89.png)<br>![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/afe3844b-a253-4493-ad83-f1a6371aaa51.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/17875029-3462-449d-9f24-3aad72d3737b.png) |
| | 手工出入库-出库单 | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739783184569-1df2391a-c404-48d3-be4e-b39f3f50e9ef.png) | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739783196368-a62151d6-2ff3-48ce-ae99-2742f296e1be.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/de1c2e35-c7db-43e1-b03d-9a617b876df3.png) | REST/stock/manual/print?pkId=XXX&billType=0&isNew=true type=0 表示入库单 type=1 表示出库单)<br>底层接口:com.f6car.stock.service.print.PrintService#getManualStorageStockInPrintUrl<br>日志关键字:+"手工出入库单据打印入参是:" |
| | 手工出入库-入库单 | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/21383c0d-4fbd-490f-95a4-b55a9c1aa473.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/4e269988-1029-48cd-97e5-c71e3ec6b275.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/5e6f982e-0369-48eb-a09b-3d247608db0f.png) |
| | 领料详情-打印领料单 | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/9c2c7fdb-360f-43e0-b07f-4ee0f8a43510.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/974ef939-6d36-433d-a3fe-594cb6cb270c.png) | ![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/8oLl952E8m3Relap/img/f4baaf53-7915-4b45-9ee5-aff164922a00.png) | REST/stock/maintain/print?idSourceBill=XXX&hasPreview=true<br>底层接口:com.f6car.stock.service.impl.print.PrintServiceImpl#getMaintainPrintUrl<br>日志关键字:+"领料单打印参数:" (有 apollo 开关 log.stock.maintain.print.switch 默认false |
# 出入库单据-出库单 && 领料出库-出库单打印模版参数说明
出库单据定制类需求模版分类(newStockOutMaintainCustomPrint--20250925新增
打印模版参数
HashMap<String, Object> resultMap
| **字段** | **说明** | 备注 |
| --- | --- | --- |
| title | 门店名称+ "出库单" | |
| billNo | 出库单号 | |
| sourceBillNo | 来源单号 | |
| showSourceBillNo | 显示来源单号 boolean | |
| billStatus | 单据状态(制单、完成) | |
| inOutDate | 出库日期 | |
| showInOutDate | 是否显示出库日期(boolean | |
| objectName | 出入库对象 | |
| objectNameGD | 出入库对象工单<br/>工单出库单:客户姓名+车牌号整体+车辆VIN码+车辆品牌车系车型全称 (拼接后取前 80 个字符)<br/>非工单出库单:"" | |
| creatorName | 制单人 | |
| billDate | 制单日期 yyyy-MM-dd | |
| sumNumber | 材料总数量 | |
| sumAmount | 总金额 <br>\--脱敏场景显示 _\*_\*_\*\*_ | |
| chineseAmount | 总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
| nowDateTime | 打印当前时间 yyyy-MM-dd HH:mm | |
| idOwnOrg | 门店ID | |
| remark | 备注信息 | |
| showRemark | 是否显示备注(boolean<br>\-- 备注不为空时是 true | |
| isGdType | 是否是工单类型单据出库(boolean<br>\-- 工单出库单是 true | |
| saName | 服务顾问姓名<br>\-- 工单出库单场景 | |
| printCount | 打印次数 | |
| showCustomCode | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739785277433-3b96ef23-7a4c-4df1-bf12-49cd4547de9e.png)配置出入库打印参数-是否显示材料编码(boolean) | |
| showBusinessLabel | 配置出入库打印参数-是否显示材料业务分类(boolean) | |
| showApplyModel | 配置出入库打印参数-是否显示材料适用车型(boolean) | |
| showStorageName | 配置出入库打印参数-是否显示出库仓库(boolean) | |
| showDefSeat | 配置出入库打印参数-是否显示出库货位(boolean) | |
| showChineseAmount | 配置出入库打印参数-是否显示大写金额(boolean) | |
| showChineseSubtotal | 配置出入库打印参数-是否显示大写行合计(boolean) | |
| columnCount | 显示几列 | |
| batchPrintConfig | 配置出入库打印参数-查询批次成本展示设置 0:都不展示,1:总成本(将批次成本合并),2和3:展示批次成本 | |
| memo | 车主描述 | \--20250925 新增 |
| partInfoDetailMapList | | |
| sortNumber | 序号<br>**通用模版追加一行合计行,显示 合计** | |
| partShowName | 材料组合名称 | |
| partName | 材料名称 | |
| partBrand | 材料品牌 | \-- 2025.02.27 新增 |
| supplierCode | 零件号 | |
| labelName | 业务分类 | |
| applyModel | 适用车型 | |
| customCode | 材料编码 | |
| storageName | 仓库名称 | |
| defSeat | 货位 | |
| number | 数量<br>**\-- 通用模版追加一行合计行,显示 材料出库总数** | |
| unit | 单位 | |
| price | 单价<br>\--脱敏场景显示 _\*_\*\*\* | |
| subtotal | 金额<br>\--脱敏场景显示 _\*_\*\*\* | |
| employeeName | 出库人 | |
| salesEmployeeNameList | 材料行销售人员 | \--20250925 新增 |
| orderBatchList<br>\--List<Map<String, String>> | 材料行成本相关 | |
| orderNo | 批次号<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示 ""** | |
| count | 批次出库几个<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示总数** | |
| price | 批次单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 "" | |
| priceNoTax | 批次单位除税成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 "" | 2025.08.14 新增 |
| totalPrice | 批次总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本 | |
| totalPriceNoTax | 批次除税总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价除税总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本 | 2025.08.14 新增 |
样列:
![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739844284185-900277fc-1fd5-4da3-a36f-224e8fcc9994.png)
```plaintext
{
"saName":"xuetting",
"objectNameGD":"18551638685【苏ABF358】 LFV2A21K6A3092399 大众 速腾 1.4T 双离合变速器(DSG) 2011 速腾",
"creatorName":"cltest",
"showStorageName":true,
"remark":"",
"title":"流程配置ES出库单",
"showSourceBillNo":true,
"sumAmount":"860.0",
"showChineseAmount":true,
"billStatus":"完成",
"sumNumber":"1.0",
"isGdType":true,
"printCount":"3",
"billNo":"CKD20250116001",
"showDefSeat":true,
"idOwnOrg":"15870306745529549109",
"showApplyModel":true,
"memo":"",
"partInfoDetailMapList":[
{
"employeeName":"cltest",
"sortNumber":"1",
"partShowName":" 3M 燃油宝1号 PN6868 3M (PN6868)",
"defSeat":"A",
"orderBatchList":[
{
"orderNo":"20211213000001",
"totalPrice":"0.0",
"price":"0.0",
"count":"1.0"
}
],
"supplierCode":"PN6868",
"partName":"3M 燃油宝1号 PN6868",
"applyModel":"",
"customCode":"CL0000015",
"storageName":"主仓库",
"number":"1.0",
"unit":"瓶",
"price":"860.0",
"subtotal":"860.0",
"labelName":"保养",
"salesEmployeeNameList": "B2C一店新员工,B2C一店采购员"
},
{
"number":"1.0",
"subtotal":"860.0",
"sortNumber":"合计",
"orderBatchList":[
{
"orderNo":"",
"totalPrice":"0.0",
"price":"",
"count":"1.0"
}
]
}],
"sourceBillNo":"WXD20250103001",
"billDate":"2025-01-16",
"chineseAmount":"捌佰陆拾元整",
"columnCount":"5",
"showCustomCode":true,
"showBusinessLabel":true,
"batchPrintConfig":"3",
"nowDateTime":"2025-02-18 10:04",
"showInOutDate":true,
"showChineseSubtotal":true,
"objectName":"18551638685【苏ABF358】大众 速腾",
"inOutDate":"2025-01-16",
"showRemark":false
}
```
# 出入库单据-入库单 && 退料入库-入库单打印模版参数说明
入库单据定制类需求模版分类(newStockInMaintainCustomPrint--20250925新增
打印模版参数
HashMap<String, Object> resultMap
| **字段** | **说明** | 备注 |
| --- | --- | --- |
| title | 门店名称+ "入库单" | |
| billNo | 入库单号 | |
| sourceBillNo | 来源单号 | |
| showSourceBillNo | 显示来源单号 boolean<br>默认:true | |
| billStatus | 单据状态(制单、完成) | |
| inOutDate | 入库日期<br>\-- yyyy-MM-dd | |
| showInOutDate | 是否显示入库日期(boolean<br>\-制单是 false<br>\-完成是 true | |
| objectName | 出入库对象 | |
| creatorName | 制单人 | |
| billDate | 制单日期 yyyy-MM-dd | |
| sumNumber | 材料总数量 | |
| sumAmount | 总金额 <br>\--脱敏场景显示 _\*_\*_\*\*_ | |
| noTaxSumAmount | 除税总金额 <br>\--脱敏场景显示 _\*_\*_\*\*_ | |
| chineseAmount | 总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
| chineseNoTaxSumAmount | 除税总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
| nowDateTime | 打印当前时间 yyyy-MM-dd HH:mm | |
| idOwnOrg | 门店ID | |
| remark | 备注信息 | |
| showRemark | 是否显示备注(boolean<br>\-- 备注不为空时是 true | |
| isGdType | 是否是工单类型单据出库(boolean<br>\-- 工单出库单是 true | |
| saName | 服务顾问姓名<br>\-- 工单出库单场景 | |
| printCount | 打印次数 | |
| showCustomCode | 配置出入库打印参数-是否显示材料编码(boolean) | |
| showBusinessLabel | 配置出入库打印参数-是否显示材料业务分类(boolean) | |
| showApplyModel | 配置出入库打印参数-是否显示材料适用车型(boolean) | |
| showStorageName | 配置出入库打印参数-是否显示出库仓库(boolean) | |
| showDefSeat | 配置出入库打印参数-是否显示出库货位(boolean) | |
| showChineseAmount | 配置出入库打印参数-是否显示大写金额(boolean) | |
| showChineseSubtotal | 配置出入库打印参数-是否显示大写行合计(boolean) | |
| sumSubtotal | 入库总金额<br>\-- 脱敏场景显示 \*\*\*\*<br>\-- 工单退-才有 | |
| chineseSubtotal | 大写入库总金额<br>\-- 脱敏场景显示 \*\*\*\*<br>\-- 工单退-才有 | |
| showReturnIn | 显示退料入库一行<br>\-- 工单退-true | |
| showSign | 【仓管签字】显示的位置<br>工单退-2;其它场景1 | |
| stockInType | 退料入库<br>\-- 工单退-才有 | |
| columnCount | 显示几列 | |
| memo | 车主描述 | \--20250925 新增 |
| partInfoDetailMapList | | |
| sortNumber | 序号 | |
| partShowName | 材料组合名称 | |
| partName | 材料名称 | |
| partBrand | 材料品牌 | |
| labelName | 业务分类 | |
| applyModel | 适用车型 | |
| customCode | 材料编码 | |
| storageName | 仓库名称 | |
| defSeat | 货位 | |
| number | 数量<br>**\-- 通用模版追加一行合计行,显示 材料出库总数** | |
| unit | 单位 | |
| price | 单价<br>\--脱敏场景显示 _\*_\*\*\* | |
| noTaxPrice | 除税单价<br>\--脱敏场景显示 _\*_\*\*\* | |
| subtotal | 金额<br>\--脱敏场景显示 _\*_\*\*\* | |
| noTaxSubtotal | 除税金额<br>\--脱敏场景显示 _\*_\*\*\* | |
| employeeName | 入库人 | |
| salesEmployeeNameList | 材料行销售人员 | \--20250925 新增 |
# 手工出入库-出/入库单
打印模版参数
HashMap<String, Object> resultMap
| **字段** | **说明** | **备注** |
| --- | --- | --- |
| title | 出库单:门店名称+ "出库单"<br>入库单:门店名称+ "入库单" | |
| billNo | 出库单号/入库单号 | |
| sourceBillNo | 来源单号 空 | |
| showSourceBillNo | 显示来源单号 booleanfalse | |
| billStatus | 单据状态(制单、完成) | |
| inOutDate | 出库日期 (yyyy-MM-dd) | |
| showInOutDate | 是否显示出库日期(boolean | |
| objectName | 出入库对象 | |
| objectNameGD | 出入库对象 | |
| creatorName | 制单人 | |
| billDate | 制单日期 yyyy-MM-dd | |
| sumNumber | 材料总数量 | |
| sumAmount | 总金额 <br>\--脱敏场景显示 _\*_\*\*\* | |
| chineseAmount | 总金额(中文大写)<br>\--脱敏场景显示 _\*_\*\*\* | |
| nowDateTime | 打印当前时间 yyyy-MM-dd HH:mm | |
| idOwnOrg | 门店ID | |
| remark | 备注信息 | |
| showRemark | 是否显示备注(boolean<br/>-- 备注不为空时是 true | |
| printCount | 打印次数 | |
| showCustomCode | ![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739785277433-3b96ef23-7a4c-4df1-bf12-49cd4547de9e.png)<br/>配置出入库打印参数-是否显示材料编码(boolean) | |
| showBusinessLabel | 配置出入库打印参数-是否显示材料业务分类(boolean) | |
| showApplyModel | 配置出入库打印参数-是否显示材料适用车型(boolean) | |
| showStorageName | 配置出入库打印参数-是否显示出库仓库(boolean) | |
| showDefSeat | 配置出入库打印参数-是否显示出库货位(boolean) | |
| showChineseAmount | 配置出入库打印参数-是否显示大写金额(boolean) | |
| showChineseSubtotal | 配置出入库打印参数-是否显示大写行合计(boolean) | |
| columnCount | 显示几列 | |
| batchPrintConfig | 配置出入库打印参数-查询批次成本展示设置 0:都不展示,1:总成本(将批次成本合并),2和3:展示批次成本 | |
| partInfoDetailMapList | | |
| sortNumber | 序号<br>**\-- 通用模版追加一行合计行,显示 合计** | |
| partShowName | 材料组合名称 (材料名称 规格型号 材料品牌 零件号) | |
| partName | 材料名称 | |
| partBrand | 材料品牌 -- 2025.02.27 新增 | |
| supplierCode | 零件号 | |
| labelName | 业务分类 | |
| applyModel | 适用车型 | |
| customCode | 材料编码 | |
| storageName | 仓库名称 | |
| defSeat | 货位 | |
| number | 数量<br>**\-- 通用模版追加一行合计行,显示 材料出库总数** | |
| unit | 单位 | |
| price | 单价<br>\--脱敏场景显示 _\*_\*\*\* | |
| subtotal | 金额<br>\--脱敏场景显示 _\*_\*\*\* | |
| employeeName | 出库人 | |
| taxRate | 税率 | |
| orderBatchList<br>\--List<Map<String, String>> | 材料行成本相关 | |
| orderNo | 批次号<br>\- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示 ""** | |
| count | 批次出库几个<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示出库/入库个数<br>**\-- 通用模版追加一行合计,追加行,显示总数** | |
| price | 批次单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | |
| priceNoTax | 批次除税单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | 2025.08.14 追加 |
| totalPrice | 批次总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | |
| totalPriceNoTax | 批次除税总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | 2025.08.14 追加 |
样列:
![image](https://ddoc.f6yc.com/yuque/0/2025/png/227465/1739842418604-54075443-f0a5-465f-ba7b-487a96b7728e.png)
日志关键字:+"手工出入库单据打印入参是"
```plaintext
{
"objectNameGD":"",
"creatorName":"王◇龙",
"showStorageName":true,
"remark":"",
"title":"ISC总店出库单",
"showSourceBillNo":false,
"sumAmount":"410.0",
"showChineseAmount":true,
"billStatus":"完成",
"sumNumber":"2.0",
"printCount":"5",
"billNo":"SGC20240515001",
"showDefSeat":true,
"idOwnOrg":"4060685614490690260",
"showApplyModel":true,
"partInfoDetailMapList":[
{
"employeeName":"XN",
"sortNumber":"1",
"partShowName":" 材料09080112 123456 米其林 (1240)",
"defSeat":"A-14-02",
"orderBatchList":[
{
"orderNo":"20210909000128",
"totalPrice":"400.0",
"price":"400.0",
"count":"1.0"
}
],
"supplierCode":"1240",
"partName":"材料09080112",
"applyModel":"大众 途安",
"customCode":"CL090800112",
"storageName":"主仓库",
"number":"1.0",
"unit":"条",
"price":"400.0",
"subtotal":"400.0",
"labelName":"轮胎"
},
{
"employeeName":"XN",
"sortNumber":"2",
"partShowName":" fnst=>1 AC德科 (FNST>=1)",
"defSeat":"",
"orderBatchList":[
{
"orderNo":"20201104000002",
"totalPrice":"4.0",
"price":"4.0",
"count":"1.0"
}
],
"supplierCode":"FNST>=1",
"partName":"fnst=>1",
"applyModel":"江淮瑞风S52....",
"customCode":"fnst>=1",
"storageName":"总2仓",
"number":"1.0",
"unit":"个",
"price":"10.0",
"subtotal":"10.0",
"labelName":"保养"
},
{
"number":"2.0",
"subtotal":"410.0",
"sortNumber":"合计",
"orderBatchList":[
{
"orderNo":"",
"totalPrice":"404.0",
"price":"",
"count":"2.0"
}
]
}
],
"sourceBillNo":"",
"billDate":"2024-05-15",
"chineseAmount":"肆佰壹拾元整",
"columnCount":"5",
"showCustomCode":true,
"showBusinessLabel":true,
"batchPrintConfig":"3",
"nowDateTime":"2025-02-18 09:33",
"showInOutDate":true,
"showChineseSubtotal":true,
"objectName":"",
"inOutDate":"2024-05-15",
"showRemark":false
}
```
# 领料详情-打印领料单
打印模版参数
HashMap<String, Object> resultMap
| **字段** | **说明** | **备注** |
| --- | --- | --- |
| idOwnOrg | 门店ID | |
| title | 门店名称+ "领料单" | |
| billNo | 工单号 | |
| nowDateTime | 打印当前时间 yyyy-MM-dd HH:mm | |
| employeeName | 服务顾问 | |
| carModel | 车型 | |
| carNoWhole | 车牌号 | |
| memo | 车主描述 | 2025.08.14 新增 |
| printTimes | 打印次数 | |
| columnCount | 显示几列 | |
| batchPrintConfig | 配置出入库打印参数-查询批次成本展示设置 0:都不展示,1:总成本(将批次成本合并),2和3:展示批次成本 | |
| stuffDetailVOList | | |
| index | 序号<br>**\-- 通用模版追加一行合计行,显示 合计** | |
| partName | 材料组合名称 (材料名称 规格型号 材料品牌 零件号) | |
| unit | 单位 | |
| defSeatList | 货位 | |
| salesEmployeeNameList | 销售人员<br>List<String> | 8.14新增 |
| orderBatchList<br>\--List<Map<String, String>> | 材料行成本相关 | |
| orderNo | 批次号<br>\- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示 ""<br>**\-- 通用模版追加一行合计,追加行,显示 ""** | |
| count | 批次出库几个<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 均价模式下,显示出库/入库个数<br>**\-- 通用模版追加一行合计,追加行,显示总数** | |
| price | 批次单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | |
| priceNoTax | 批次除税单位成本<br>\-- batchPrintConfig = 1 场景下,显示 ""<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价单位成本**<br>\-- 通用模版追加一行合计,追加行,显示 ""\*\* | 2025.08.14 追加 |
| totalPrice | 批次总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | |
| totalPriceNoTax | 批次除税总成本<br>\-- batchPrintConfig = 1 场景下,显示合计总成本<br>\-- 脱敏场景显示 **\*\*\*\***<br>**\-- 均价模式下,显示均价总成本**<br>\-- 通用模版追加一行合计,追加行,显示总成本\*\*\*\* | 2025.08.14 追加 |
@@ -0,0 +1,682 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3fa22123-efc4-4f3f-a186-6a8f692d17e6">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<subDataset name="List1" uuid="7366c5be-288c-41c7-b295-b8d023ec81ae">
<queryString>
<![CDATA[]]>
</queryString>
<field name="index" class="java.lang.String"/>
<field name="serviceName" class="java.lang.String"/>
<field name="cooperationServiceName" class="java.lang.String"/>
<field name="cooperationOrgName" class="java.lang.String"/>
<field name="auditStatus" class="java.lang.String"/>
<field name="cooperationCost" class="java.math.BigDecimal"/>
</subDataset>
<parameter name="title" class="java.lang.String"/>
<parameter name="billNo" class="java.lang.String"/>
<parameter name="creatorName" class="java.lang.String"/>
<parameter name="printTime" class="java.lang.String"/>
<parameter name="naEmployee" class="java.lang.String"/>
<parameter name="billDate" class="java.lang.String"/>
<parameter name="deliveryTime" class="java.lang.String"/>
<parameter name="naCustomer" class="java.lang.String"/>
<parameter name="carModel" class="java.lang.String"/>
<parameter name="cellPhone" class="java.lang.String"/>
<parameter name="carNoWhole" class="java.lang.String"/>
<parameter name="vin" class="java.lang.String"/>
<parameter name="serviceDetailVOList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="amountAll" class="java.math.BigDecimal"/>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="115" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="555" height="40" uuid="7c3a0deb-dcb0-406e-ba9c-9f279e1518b0">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="16"/>
</textElement>
<textFieldExpression><![CDATA[$P{title}!=null?$P{title}+"协作项目确认单":""]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="240" y="60" width="120" height="18" uuid="6849ccdb-a3a0-4d32-a556-7a8fd5a19cca">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{creatorName}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="420" y="60" width="135" height="18" uuid="b347e5b3-390c-44c3-b746-481aa6dc808e">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="60" y="60" width="120" height="18" uuid="92646184-6d70-4b6e-9f0e-23b4a7b15fb5">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement key="" x="360" y="60" width="50" height="18" isRemoveLineWhenBlank="true" uuid="86735eee-0435-4238-9b93-c08d86ed318f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[打印时间:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="60" width="60" height="18" uuid="fef0343d-31f4-4b1c-a521-1a1b736aa485">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[工单号:]]></text>
</staticText>
<staticText>
<reportElement x="180" y="60" width="60" height="18" uuid="627d90fe-c235-45d8-9f4a-92f8d7ec0de7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[制单人:]]></text>
</staticText>
<line>
<reportElement x="0" y="78" width="555" height="1" uuid="329fa736-2c18-4d9d-8fab-f54846315633">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<staticText>
<reportElement x="180" y="96" width="60" height="18" uuid="ef411f00-dff2-400c-9e82-bbc2450553c0">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.4" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[车型]]></text>
</staticText>
<staticText>
<reportElement x="0" y="78" width="60" height="18" uuid="74602da5-9195-47dd-9416-9cb229aeccad">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[服务顾问]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="240" y="96" width="315" height="18" uuid="985f2bdc-723d-477e-9786-263a1d3ce58c">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.4"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{carModel}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="240" y="78" width="120" height="18" uuid="49312902-8cda-4fe3-a92f-d6ecbf9cb893">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{billDate}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="180" y="78" width="60" height="18" uuid="65483940-e0c7-4713-9e9a-6a5a7294d248">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[进厂时间]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="60" y="78" width="120" height="18" uuid="e9c8328d-22ea-4e66-a23f-5015ffbd8248">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{naEmployee}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="96" width="555" height="1" uuid="f6716829-054d-4fed-a12c-2f4d227724c7">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<line>
<reportElement x="0" y="114" width="555" height="1" uuid="404f9e68-011f-4c55-88f6-f1231a22908b">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<line>
<reportElement x="0" y="113" width="555" height="1" uuid="09d822fb-3735-4773-b954-9c83f3eefdef">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="420" y="78" width="135" height="18" uuid="09e44308-af9c-4bf2-b88d-b6c213fa2c43">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{vin}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="360" y="78" width="60" height="18" uuid="180c43d9-ea5c-4074-9b9e-b1ffaa905250">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[VIN码]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="60" y="96" width="120" height="18" uuid="85a6bfa2-352f-4f7e-b690-625639120c96">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{carNoWhole}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="96" width="60" height="18" uuid="fb4b2bbf-ccd0-4201-9e4b-d98ad63364ca">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[车牌号]]></text>
</staticText>
</band>
<band height="56">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<componentElement>
<reportElement x="0" y="20" width="555" height="36" uuid="dab34898-b275-4c88-8129-eefa0454fc8a">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table 2_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table 2_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table 2_TD"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
<datasetRun subDataset="List1" uuid="c97f7286-0f00-496d-af86-5689f8ef21e5">
<dataSourceExpression><![CDATA[$P{serviceDetailVOList}]]></dataSourceExpression>
</datasetRun>
<jr:column width="20" uuid="2a312910-46b6-42d0-a6f9-5b10690854b7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<jr:columnHeader height="18" rowSpan="1">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
</jr:columnHeader>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="20" height="18" uuid="c20bd145-2589-4e34-acd8-c52ecb01f627"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$V{COLUMN_COUNT}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="150" uuid="e296f490-d79c-4dc7-b064-4df5beea7f6a">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<jr:columnHeader height="18" rowSpan="1">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<staticText>
<reportElement x="0" y="0" width="150" height="18" uuid="2cf4018d-7073-47cd-ba69-6239dea208d5"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[项目名称]]></text>
</staticText>
</jr:columnHeader>
<jr:detailCell height="18">
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="150" height="18" uuid="6d5dabce-2317-4d34-b286-b035b0319c86"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{serviceName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="125" uuid="901500f5-9ff9-42de-985e-5d31c114a41c">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<jr:columnHeader height="18" rowSpan="1">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<staticText>
<reportElement x="0" y="0" width="125" height="18" uuid="fda635b8-de40-4302-a86e-27d94d5977cf"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[协作项目名称]]></text>
</staticText>
</jr:columnHeader>
<jr:detailCell height="18">
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="125" height="18" uuid="776c62d8-14e6-4bd5-9548-ecc3f933440a"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{cooperationServiceName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="120" uuid="cf002016-6b32-41b9-ad57-b813114477bf">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column4"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:columnHeader height="18" rowSpan="1">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<staticText>
<reportElement x="0" y="0" width="120" height="18" uuid="04d9aca5-e6c0-4688-b7f4-93dcf012c025"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[协作门店]]></text>
</staticText>
</jr:columnHeader>
<jr:detailCell height="18">
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="120" height="18" uuid="b7b12dc9-c060-47b9-8252-ad264faed596"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$F{cooperationOrgName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="70" uuid="275bd3e2-9eb4-4274-8693-06306f525ed9">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column5"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:columnHeader height="18" rowSpan="1">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<staticText>
<reportElement x="0" y="0" width="70" height="18" uuid="a8222aea-c4e9-47c4-9c88-4043e14b06bc"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="0" rightIndent="0"/>
</textElement>
<text><![CDATA[审核状态]]></text>
</staticText>
</jr:columnHeader>
<jr:detailCell height="18">
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="70" height="18" uuid="afca6b83-a1d0-4752-8fdb-b977d5c29aad"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$F{auditStatus}.equals("0")?"未审核":"已审核"]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="70" uuid="efa6775a-c855-4f28-9c4b-f2f940ca48ed">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column6"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:columnHeader height="18" rowSpan="1">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<pen lineWidth="0.4"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.4" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<staticText>
<reportElement x="0" y="0" width="70" height="18" uuid="bc6f3a7c-ae6f-48a8-abea-958935d8ee5c"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="0" rightIndent="0"/>
</textElement>
<text><![CDATA[协作成本]]></text>
</staticText>
</jr:columnHeader>
<jr:detailCell height="18">
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="70" height="18" uuid="e473f232-b64b-4d97-a953-f8a62d80e2f0"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$F{cooperationCost}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="19">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="20" y="1" width="60" height="18" uuid="fc27cb3d-0e5d-4143-b83d-daf432fa56c8">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[小计]]></text>
</staticText>
<line>
<reportElement x="0" y="0" width="555" height="1" uuid="98b923f9-1e14-4cc5-b508-12141cefb96e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="485" y="1" width="70" height="18" uuid="bac7389a-d2c8-4df5-9e2b-bd0fba391750">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
</band>
<band height="50">
<staticText>
<reportElement x="185" y="25" width="70" height="18" uuid="c9a9b702-c7eb-4be8-b07e-365064dee61f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph firstLineIndent="5"/>
</textElement>
<text><![CDATA[技师签名:]]></text>
</staticText>
<staticText>
<reportElement x="370" y="25" width="70" height="18" uuid="a35c01ab-c3e4-46ca-a47f-e24e82171c0e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph firstLineIndent="5"/>
</textElement>
<text><![CDATA[审核人签名:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="25" width="70" height="18" uuid="fa883b7a-2acb-44e9-a646-e9e7c5f2aef8">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph firstLineIndent="5"/>
</textElement>
<text><![CDATA[顾问签名:]]></text>
</staticText>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,32 @@
# 协作单接口文档
# 协作单接口文档
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| title | 打印抬头(门店简称) | | String |
| billNo | 工单号 | | String |
| creatorName | 制单人 | | String |
| printTime | 打印时间 | | String |
| naEmployee | 服务顾问 | | String |
| billDate | 进厂日期 | | String |
| deliveryTime | 交车时间(出厂时间) | | String |
| naCustomer | 车主姓名 | | String |
| cellPhone | 车主电话 | | String |
| carModel | 车型 | | String |
| carNoWhole | 车牌号 | | String |
| vin | 车辆VIN码 | | String |
| amountAll | 小计 | | BigDecimal |
| serviceDetailVOList | 协作项目集合 | | List<CooperationServicePrintAttribute> |
## CooperationServicePrintAttribute
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| serviceName | 工单项目名称 | | String |
| cooperationServiceName | 协作项目名称 | | String |
| cooperationOrgName | 协作门店 | | String |
| auditStatus | 审核状态 | | String |
| cooperationCost | 协作成本 | | BigDecimal |
> 更新: 2023-08-28 16:06:07 原文: <https://xcz.yuque.com/ombipo/rpc7ms/awq306g9g8fg78or>
@@ -0,0 +1,72 @@
# 增项单接口文档
# 增项单接口文档
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| title | 打印抬头(门店名称+"新增项目确认单" | | String |
| sourceBillNo | 关联工单号 | | String |
| printTime | 打印时间 | | String |
| naEmployee | 服务顾问 | | String |
| arrivalTime | 进厂日期 | | String |
| deliveryTime | 交车时间 | | String |
| naCustomer | 车主姓名 | | String |
| cellPhone | 车主电话 | | String |
| repairPerson | 送修人 | | String |
| repairPersonContact | 送修人联系方式 | | String |
| carModel | 车型 | | String |
| carNoWhole | 车牌号 | | String |
| carColor | 车身颜色 | | String |
| engineCode | 发动机号 | | String |
| vin | 车辆VIN码 | | String |
| mileage | 进厂里程 | | String |
| oilCapacity | 进厂油量 | | String |
| merchantAddress | 商家联系地址 | | String |
| merchantTel | 商家联系方式(固定电话) | | String |
| merchantPhone | 商家联系方式(手机) | | String |
| workHourPriceSubtotal | 工时费(小计) | | String |
| amountSubtotal | 材料费(小计) | | String |
| attachedServiceVoList | 增项服务项目集合 | | List<ServicePrintAttribute> |
| attachedStuffVoList | 增项配件材料集合 | | List<PartPrintAttribute> |
**ServicePrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| serviceName | 工单项目名称 | | String |
| labelName | 业务分类名称 | | String |
| customCode | 项目编码 | | String |
| price | 工时单价 | | Double |
| workHour | 工时 | | Double |
| number | 项目数量 | | Integer |
| discount | 折扣 | | Double |
| discountedSubtotal | 折后金额 | | Double |
| subtotal | 金额 | | Double |
| serviceMemo | 项目备注 | | String |
| isMember | 当前项目是否使用会员 | | Integer |
| nameMember | 会员项目的来源名(如套餐代码) | | String |
| empNameStr | 服务项目明细对应修理工名称组装字符串 | | String |
If you get gainsplease give a like
**PartPrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| partName | 材料名称 | | String |
| labelName | 业务分类名称 | | String |
| partBrand | 配件品牌 | | String |
| customCode | 材料编码 | | String |
| price | 单价 | | Double |
| number | 材料数量 | | Integer |
| unit | 单位 | | String |
| discount | 折扣 | | Double |
| discountedSubtotal | 折后金额 | | Double |
| subtotal | 金额 | | Double |
| partMemo | 材料备注 | | String |
| isMember | 当前项目是否使用会员 | | Integer |
| nameMember | 会员项目的来源名(如套餐代码) | | String |
| employeeName | 员工名称 | | String |
| empNameStr | 材料明细对应修理工名称组装字符串 | | String |
> 更新: 2023-09-05 10:34:22 原文: <https://xcz.yuque.com/ombipo/rpc7ms/gpobrxn8mn5lzthk>
@@ -0,0 +1,53 @@
# 委托单接口文档
# 委托单接口文档
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| printOrgName | 打印抬头 | | String |
| orgName | 维修厂名称 | | String |
| billNo | 工单号 | | String |
| naEmployee | 服务顾问 | | String |
| employeePhone | 服务顾问手机号 | | String |
| naCustomer | 单位名称(客户姓名) | | String |
| carNoWhole | 车牌号整体 = carPrefix + carNo | | String |
| cellPhone | 联系电话(客户) | | String |
| repairPerson | 送修人 | | String |
| repairPersonContact | 送修人联系方式 | | String |
| billDate | 进厂日期 | | String |
| deliveryTime | 交车时间(出厂时间) | | String |
| mileage | 出厂里程(进厂里程) | | BigDecimal |
| oilCapacity | 当前油量 | | String |
| vin | 车辆VIN码 | | String |
| carModelShort | 车型简称 | | String |
| signaturePhotoUrl | 签名图片 | | String |
| orgContactNumber | 联系电话(维修厂) | | String |
| orgDetailAddress | 联系地址(维修厂) | | String |
| orgContactMobile | 联系电话(维修厂) | | String |
| printContentEntrust | 委托单免责条款 | | String |
| serviceSubtotalVip | 服务项目明细小计(会员项目) | | BigDecimal |
| stuffSubtotalVip | 材料收入小计(会员项目) | | BigDecimal |
| serviceSubtotalAll | 工时费小计 | | BigDecimal |
| stuffSubtotalAll | 材料费小计 | | BigDecimal |
| serviceList | 工单对应项目集合 | | List<ServicePrintAttribute> |
| | | | |
| | | | |
| | | | |
| | | | |
| | | | |
## ServicePrintAttribute
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| serviceName | 项目名称 | | String |
| price | 工时单价 | | BigInteger |
| workHour | 工时 | | BigInteger |
| subtotal | 金额 | | BigInteger |
| serviceMemo | 附加信息备注 | | String |
| isMember | 当前项目是否使用会员 | | Integer |
| empNameStr | 服务项目明细对应修理工名称组装字符串 | | String |
| | | | |
| | | | |
> 更新: 2022-11-30 15:56:54 原文: <https://xcz.yuque.com/ombipo/rpc7ms/eppwl9lml80qq2bi>
@@ -0,0 +1,121 @@
# 定金单打印接口文档
# 定金单打印接口文档
# 接口出参
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| title | 标题(门店名称) | String |
| abbreviation | 门店简称 | String |
| billNO | 定金单号 | String |
| printTime | 打印时间 | String |
| customerName | 客户姓名 | String |
| cellPhone | 手机号码 | String |
| carNo | 适用车辆(顿号、隔开) | String |
| orgName | 适用门店(顿号、隔开) | String |
| balanceStatus | 结算状态 | String |
| receivedAmount | 已收金额 | Double |
| amount | 收款交易金额 | Double |
| amountAll | 定金单收款小计 | Double |
| preRefundBalance | 定金单退款前余额 | Double |
| advancesReceivedBalance | 定金单收款后剩余面额 | Double |
| memo | 定金备注 | String |
| settlePerson | 结算人 | String |
| employeeName | 收款人 | String |
| businessDate | 收款时间 | String |
| billDate | 交易时间 | String |
| transactionDate | 交易时间 | String |
| detailAddress | 联系地址 | String |
| contactMobile | 联系方式(手机+固定电话) | String |
| naServicePerson | 服务顾问 | String |
| gatheringList | 收款方式 | List |
| L paymentType | 支付方式 | String |
| L amount | 金额 | BigDecimal |
| relationServices | 适用项目列表 | List |
| L infoId | 项目id | BigInteger |
| L infoName | 项目名称 | String |
| L labelName | 业务分类 | String |
| relationParts | 适用材料列表 | List |
| L infoId | 材料id | BigInteger |
| L infoName | 材料名称 | String |
| L labelName | 业务分类 | String |
| relationCars | 适用车辆列表 | List |
| L idCar | 车辆信息id | BigInteger |
| L carNo | 车牌号 | String |
| L vin | vin码 | String |
# 范例
```plaintext
收款收款{
"data": {
"preRefundBalance": 20000,
"memo": "我是备注。",
"title": "演示主店",
"carNo": "藏AVB2131",
"naServicePerson": "唐铭远",
"contactMobile": "15051779785",
"employeeName": "刘思杰",
"amount": 10000,
"orgName": "演示主店测试、第一分店",
"advancesReceivedBalance": 10000,
"amountAll": 10000,
"balanceStatus": "7100",
"billDate": "2024-07-11 16:27:49",
"businessDate": "2024-07-24 14:49:01",
"receivedAmount": 10000,
"abbreviation": "演示主店测试",
"transactionDate": "2024-07-11 16:27:49",
"relationServices": [
{
"infoName": "龙膜全车贴膜(不含撕膜)",
"infoId": "10545055918005551735",
"infoType": 1,
"id": "239",
"labelName": "其他",
"idSubscription": "11159"
}
],
"customerName": "牛洋",
"gatheringList": [
{
"amount": 4000,
"paymentType": "支付宝"
},
{
"amount": 3000,
"paymentType": "现金"
},
{
"amount": 3000,
"paymentType": "挂账"
}
],
"detailAddress": "西藏自治区那曲市班戈县西藏自治区那曲市班戈县北拉镇邮政所",
"billNO": "DJD20240711001",
"cellPhone": "17625046227",
"printTime": "2024-07-11 17:28:08",
"relationParts": [
{
"infoName": "奔腾CI-4 15W40 4*4L 胜牌 (706650)",
"infoId": "10545055918005692400",
"infoType": 2,
"id": "240",
"labelName": "业务分类测试",
"idSubscription": "11159"
}
],
"relationCars": [
{
"carNo": "AV616E",
"vin": "LSVAA49J132047371",
"idCar": 15809106713748983890
}
],
},
"storeId": 4060685614487994527,
"tempId": 41
}
```
> 更新: 2024-07-25 20:54:41 原文: <https://xcz.yuque.com/ombipo/rpc7ms/io3q0kkop242geg6>
@@ -0,0 +1,942 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="dingjin" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="ecb495fd-dade-4203-bf0d-1beb50748cbb">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<subDataset name="Bean" uuid="c4b2280f-9d97-43a6-af41-df8e4c3837f1">
<queryString>
<![CDATA[]]>
</queryString>
<field name="index" class="java.lang.String"/>
<field name="paymentType" class="java.lang.String"/>
<field name="amount" class="java.math.BigDecimal"/>
</subDataset>
<subDataset name="SubscriptionInfoRelationBean" uuid="63a5d0a7-7ac9-4e14-b3f3-d03138dc8910">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="infoName" class="java.lang.String"/>
<field name="labelName" class="java.lang.String"/>
</subDataset>
<parameter name="title" class="java.lang.String"/>
<parameter name="billNO" class="java.lang.String"/>
<parameter name="printTime" class="java.lang.String"/>
<parameter name="customerName" class="java.lang.String"/>
<parameter name="cellPhone" class="java.lang.String"/>
<parameter name="carNo" class="java.lang.String"/>
<parameter name="orgName" class="java.lang.String"/>
<parameter name="amountAll" class="java.math.BigDecimal"/>
<parameter name="memo" class="java.lang.String"/>
<parameter name="settlePerson" class="java.lang.String"/>
<parameter name="employeeName" class="java.lang.String"/>
<parameter name="billDate" class="java.lang.String"/>
<parameter name="detailAddress" class="java.lang.String"/>
<parameter name="contactMobile" class="java.lang.String"/>
<parameter name="gatheringList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="transactionDate" class="java.lang.String"/>
<parameter name="naServicePerson" class="java.lang.String"/>
<parameter name="relationServices" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="relationParts" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="balanceStatus" class="java.lang.String">
<parameterDescription><![CDATA[]]></parameterDescription>
</parameter>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="103" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="1" width="555" height="30" uuid="6b893528-b46e-4a7d-b95d-62163a92812c">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="16"/>
</textElement>
<textFieldExpression><![CDATA[$P{title}!=null?$P{title} + "定金收款单":""]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="40" width="50" height="18" uuid="df2d5fff-f94e-48ce-8327-d04b0ed8a8eb">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[定金单号:]]></text>
</staticText>
<staticText>
<reportElement x="405" y="40" width="50" height="18" uuid="6100aced-372a-4449-91cf-1b49465ddce1">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[打印时间:]]></text>
</staticText>
<line>
<reportElement x="0" y="68" width="555" height="1" uuid="6820699a-64e5-4110-aab9-059fcfa04956">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
<staticText>
<reportElement x="0" y="68" width="50" height="15" uuid="2d6a570f-5cff-411e-a42d-5f3833a49674">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户姓名]]></text>
</staticText>
<staticText>
<reportElement x="150" y="68" width="50" height="15" uuid="e25f93dc-e5d5-4f43-bd78-20bbd337df95">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[手机号码]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="50" y="68" width="100" height="15" uuid="ae838d08-7ea2-4a9f-9bcb-ad039eea0139">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="200" y="68" width="100" height="15" uuid="f1280a32-7f86-45b7-b15e-e16b88db6f69">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{cellPhone}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="50" y="40" width="100" height="18" uuid="f86834e1-12c3-4f52-ab27-921980da0a86">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{billNO}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="455" y="40" width="100" height="18" uuid="f01ca83f-cc02-4c16-8edf-1ef24a479592">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="300" y="69" width="150" height="15" uuid="28fd8ca7-cd61-45c9-a5de-ecf081f3623c">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{naServicePerson} != null ? "服务顾问 " + $P{naServicePerson} : null]]></textFieldExpression>
</textField>
</band>
<band height="15">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="0" width="505" height="15" isPrintWhenDetailOverflows="true" uuid="bc87056d-ec39-4fa8-bc1b-cf2475f0b712">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<box bottomPadding="1"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{carNo}!=null?$P{carNo}:""]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="50" height="15" uuid="be24a675-674a-4e69-904f-462adddebdf2">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[限定车辆]]></text>
</staticText>
<line>
<reportElement x="0" y="0" width="555" height="1" uuid="e0b7e65f-b9cd-4f1d-9896-f7b35076c20f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="15">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="0" width="50" height="15" uuid="c951994c-2dc6-4a82-835e-9c132409edd0">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[适用门店]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="0" width="505" height="15" isPrintWhenDetailOverflows="true" uuid="6450a724-3257-45c0-b5ac-d970e2608683">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<box bottomPadding="1"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgName}!=null?$P{orgName}:""]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="0" width="555" height="1" uuid="95190ceb-a559-4b6b-99ed-4d92568db2ce">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="15">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isBlankWhenNull="true">
<reportElement x="50" y="0" width="505" height="15" uuid="73423a33-419c-424a-bf28-1f278c95f6de">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="50" height="15" uuid="bbfe6557-435b-4a06-89e7-68f5fa5f866e">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[定金金额]]></text>
</staticText>
<line>
<reportElement x="0" y="0" width="555" height="1" uuid="34772527-56ed-496e-83e8-638d8f87a7d4">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="18">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement stretchType="ContainerBottom" x="0" y="0" width="50" height="15" isRemoveLineWhenBlank="true" uuid="31b57e69-4dea-4593-84c4-5f6e5d7c5cad">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[定金备注]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement stretchType="ContainerBottom" x="50" y="0" width="505" height="15" isRemoveLineWhenBlank="true" isPrintWhenDetailOverflows="true" uuid="d8c6dc34-aedb-46ab-ab17-1b75d37cee18">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="0" width="555" height="1" uuid="024b26b4-5ca0-4d1a-a713-2b1a04e4facb">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="6">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
<line>
<reportElement x="0" y="5" width="555" height="1" uuid="3286a86a-5ca6-42c8-8302-9412dd51255b">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
<line>
<reportElement x="0" y="2" width="555" height="1" uuid="6e4f8003-b171-43ba-b3d1-1e863feaa14f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="62">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="555" height="18" uuid="186a3a0f-9415-48af-8fc5-f0b91f01c8dd">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[限定项目条件]]></text>
</staticText>
<line>
<reportElement x="0" y="18" width="555" height="1" uuid="784988f1-bab7-4893-b7b0-45b8c7d36113">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
<staticText>
<reportElement x="10" y="21" width="40" height="18" uuid="3581da6d-7736-4324-8d00-46a8101328b8">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[序号]]></text>
</staticText>
<staticText>
<reportElement x="60" y="21" width="260" height="18" uuid="729944a0-c591-4d3c-bd82-10852a27ddb5">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[项目名称]]></text>
</staticText>
<staticText>
<reportElement x="340" y="21" width="215" height="18" uuid="91d16827-3716-49bf-9589-fef01692e051">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[业务分类]]></text>
</staticText>
<componentElement>
<reportElement x="10" y="43" width="530" height="15" uuid="70e14cdb-d3db-4143-a95b-06c79be594e9">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="net.sf.jasperreports.export.headertoolbar.table.name" value=""/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="65597dd8-2b18-4b8f-af59-9a14a8c656f9">
<dataSourceExpression><![CDATA[$P{relationServices}]]></dataSourceExpression>
</datasetRun>
<jr:column width="20" uuid="9f5cb566-9a81-471e-83b7-eed0ae9a15cc">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="20" height="15" uuid="5b2a2317-f511-4c5b-8146-7422aa7bb656"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="299" uuid="50b00464-f90b-4b48-8dc9-3d06ac503250">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="299" height="15" uuid="4d201729-3ee9-42c2-b659-49966309bb6a">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph lineSpacing="Single" leftIndent="33"/>
</textElement>
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="211" uuid="9589926e-9fb0-432d-a202-7701e2654ee5">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="211" height="15" uuid="41413b43-582e-4a5e-9025-bbe735caf9f7">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph rightIndent="170"/>
</textElement>
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="6">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
<line>
<reportElement x="0" y="4" width="555" height="1" uuid="2f6ac67a-48f3-4ead-a04d-dc0a5ee9ec77">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="59">
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="555" height="18" uuid="0cf72aee-23b8-46aa-a29f-cf3f3b5f60fa">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[限定材料条件]]></text>
</staticText>
<line>
<reportElement x="0" y="18" width="555" height="1" uuid="55b86547-d6ca-42e0-854e-2e08893a2b5f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
<staticText>
<reportElement x="10" y="20" width="40" height="18" uuid="91997de4-2512-47f0-ab50-a9611a851818">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[序号]]></text>
</staticText>
<staticText>
<reportElement x="60" y="20" width="260" height="18" uuid="821e871f-4d45-4c6e-a153-bdb11222edfd">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[材料名称]]></text>
</staticText>
<staticText>
<reportElement x="340" y="20" width="215" height="18" uuid="cb6c5a3e-3605-4451-b6e6-57bbd3b20aaa">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[业务分类]]></text>
</staticText>
<componentElement>
<reportElement x="10" y="40" width="530" height="15" uuid="d9989d91-ae8e-494b-99ae-e466e8480b93">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="2250db78-4f45-464b-afac-5c1fbe1c526a">
<dataSourceExpression><![CDATA[$P{relationParts}]]></dataSourceExpression>
</datasetRun>
<jr:column width="20" uuid="a3a9ba5c-d15d-45a0-9529-905dd13d1bb3">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="20" height="15" uuid="80a4d1e9-c964-49e0-9165-e83d7f53c955"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="299" uuid="5b5b73ee-6e45-440c-abcc-c47295a8f445">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="299" height="15" uuid="f344b148-d0c4-4f7e-9b21-1a350e385f04">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph lineSpacing="Single" leftIndent="33"/>
</textElement>
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="211" uuid="bc6a0624-08f4-4831-b786-d96ecbad358f">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="211" height="15" uuid="43282bdc-4735-4173-b9be-1e34a4c7ecf1">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph rightIndent="170"/>
</textElement>
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="6">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<line>
<reportElement x="0" y="4" width="555" height="1" uuid="6020511d-2f28-4dc2-80f1-9176d218b3be">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="19">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<staticText>
<reportElement x="20" y="0" width="50" height="18" uuid="3581da6d-7736-4324-8d00-46a8101328b8">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收款方式]]></text>
</staticText>
<line>
<reportElement x="0" y="18" width="555" height="1" uuid="784988f1-bab7-4893-b7b0-45b8c7d36113">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
<staticText>
<reportElement x="455" y="0" width="75" height="18" uuid="e5017d0f-3946-4dd0-8cdd-cc01396607c0">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
<paragraph leftIndent="0" rightIndent="3"/>
</textElement>
<text><![CDATA[金额]]></text>
</staticText>
</band>
<band height="15">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<componentElement>
<reportElement x="0" y="0" width="530" height="15" uuid="e508f855-fa78-4d64-a6e4-a8a8a46aca77">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
<datasetRun subDataset="Bean" uuid="b65bc3e9-f219-4cb4-9e37-06389e44b27a">
<dataSourceExpression><![CDATA[$P{gatheringList}]]></dataSourceExpression>
</datasetRun>
<jr:column width="20" uuid="ee067303-224b-4228-9945-59b00d0f5cab">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="20" height="15" uuid="69411085-ae93-4494-a5be-5f1c08c669e6"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="435" uuid="c73b03e1-b451-4ba7-aa66-506770b0d16b">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="435" height="15" uuid="513d0744-5c1a-4cbc-9ab0-a494b491c01f"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{paymentType}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="75" uuid="e1f4c79b-33e8-46e3-8d01-2405584f90dc">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="15">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="75" height="15" uuid="41c011f0-5a98-40a5-96c5-27cad152cd7b"/>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph rightIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{amount}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="15">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<line>
<reportElement x="0" y="0" width="555" height="1" uuid="125ba998-bf5b-433a-9954-6043f153ff42">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
<staticText>
<reportElement x="20" y="0" width="50" height="15" uuid="f6849ec7-9e15-47e5-b71e-dcd53f0b84e0">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[小计]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="455" y="0" width="75" height="15" uuid="6fc715c5-3a85-4a39-8b51-ff75731aba06">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="0" rightIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
</band>
<band height="25">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<staticText>
<reportElement x="0" y="10" width="50" height="15" uuid="97da61eb-2f1d-46a4-8ed4-c3172d371408">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[结算人:]]></text>
</staticText>
<staticText>
<reportElement x="200" y="10" width="50" height="15" uuid="319ada1b-ff86-4b3c-82d6-15bc9981b1d9">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收款人:]]></text>
</staticText>
<staticText>
<reportElement x="405" y="10" width="50" height="15" uuid="01e213a0-948f-4a51-9b39-f3da64729c60">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收款时间:]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="50" y="10" width="100" height="15" uuid="92b95e36-c1a5-4311-94db-f615b29958f1">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="250" y="10" width="100" height="15" uuid="389f723a-1618-48f7-8e25-890a4902b19b">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="455" y="10" width="100" height="15" uuid="d8abcebc-b5b3-4c86-89b2-b45c218f353e">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{billDate}]]></textFieldExpression>
</textField>
</band>
<band height="109">
<staticText>
<reportElement x="0" y="25" width="50" height="15" uuid="55ff22be-c6bb-4bbb-bb41-8cefc91675e0">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[办理人签名]]></text>
</staticText>
<staticText>
<reportElement x="300" y="25" width="50" height="15" uuid="ff44db31-a42f-477e-9e52-378b94c34217">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户签名]]></text>
</staticText>
<staticText>
<reportElement x="0" y="55" width="50" height="15" uuid="9afa1f35-7bf2-4b9b-aa4d-9989db266bd5">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[联系地址:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="70" width="50" height="15" uuid="a439d4d9-7268-40cb-8f44-41c75c328179">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[联系方式:]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="50" y="55" width="505" height="15" uuid="0fd29c3e-c04b-4552-85b0-55b2720d57df">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{detailAddress}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="50" y="70" width="505" height="15" uuid="421732bc-d6d4-4f97-8480-565eaa109ffc">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{contactMobile}.split(" ")[1].length() == 5 ? $P{contactMobile}.split(" ")[0] : $P{contactMobile}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,641 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="223" pageHeight="842" whenNoDataType="NoPages" columnWidth="223" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="5195ed21-e8c1-4daf-ab55-5f5dc9c07b0a">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="mm"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="mm"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="mm"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="mm"/>
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="mm"/>
<property name="ireport.zoom" value="1.0"/>
<property name="ireport.x" value="0"/>
<property name="ireport.y" value="0"/>
<subDataset name="Dataset1" uuid="7e6cf9c7-d927-4f84-a0f6-e84863998e11">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="paymentType" class="java.lang.String"/>
<field name="amount" class="java.math.BigDecimal"/>
</subDataset>
<subDataset name="SubscriptionInfoRelationBean" uuid="63b98521-dd7d-419f-bae4-f6a776630dd5">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="infoName" class="java.lang.String"/>
<field name="labelName" class="java.lang.String"/>
</subDataset>
<parameter name="title" class="java.lang.String"/>
<parameter name="abbreviation" class="java.lang.String"/>
<parameter name="billNO" class="java.lang.String"/>
<parameter name="printTime" class="java.lang.String"/>
<parameter name="customerName" class="java.lang.String"/>
<parameter name="cellPhone" class="java.lang.String"/>
<parameter name="carNo" class="java.lang.String"/>
<parameter name="orgName" class="java.lang.String"/>
<parameter name="amountAll" class="java.math.BigDecimal"/>
<parameter name="memo" class="java.lang.String"/>
<parameter name="settlePerson" class="java.lang.String"/>
<parameter name="employeeName" class="java.lang.String"/>
<parameter name="billDate" class="java.lang.String"/>
<parameter name="detailAddress" class="java.lang.String"/>
<parameter name="contactMobile" class="java.lang.String"/>
<parameter name="gatheringList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="transactionDate" class="java.lang.String"/>
<parameter name="naServicePerson" class="java.lang.String"/>
<parameter name="relationServices" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="relationParts" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="balanceStatus" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="20" splitType="Stretch">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="222" height="20" isPrintWhenDetailOverflows="true" uuid="fdd7c75d-7f0c-42a3-afa1-9927522a4dd1">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{title}]]></textFieldExpression>
</textField>
</band>
<band height="22">
<staticText>
<reportElement x="0" y="1" width="222" height="15" uuid="2f71d4ac-e294-4763-a1ad-8e24fcb09055">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体"/>
</textElement>
<text><![CDATA[定金单]]></text>
</staticText>
<line>
<reportElement x="15" y="16" width="195" height="1" uuid="1fac1b83-9a99-4059-94f8-705f03630c06">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="12">
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="44" height="12" uuid="8210a1bf-fb5f-465d-8e13-15ee265db665">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收款时间]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="44" y="0" width="177" height="12" uuid="e79b3ef6-3727-46d0-8de1-035b71f358f4">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{transactionDate}]]></textFieldExpression>
</textField>
</band>
<band height="12">
<staticText>
<reportElement x="0" y="0" width="44" height="12" uuid="924e0a29-acb7-4430-98fd-0dd08bf26d01">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[单号]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="44" y="0" width="177" height="12" uuid="35315081-9a78-4f78-8df5-5ad786dd56b9">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{billNO}]]></textFieldExpression>
</textField>
</band>
<band height="12">
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="44" height="12" uuid="af0870ec-d979-4c13-8743-c7a0c3621815">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收款人]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="44" y="0" width="177" height="12" uuid="3b66f5f5-f6d9-4d05-90e7-af9561373bb6">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
</textField>
</band>
<band height="12">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{naServicePerson}!=null]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="44" height="12" uuid="ccdb57d5-cdaf-4189-930f-fc1165c936ee">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[服务顾问]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="44" y="-1" width="177" height="12" uuid="62146321-e5d6-4c36-b91c-db8c0a5653de">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{naServicePerson}]]></textFieldExpression>
</textField>
</band>
<band height="24">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="-1" width="44" height="12" uuid="53391abc-4be2-40a8-8c2d-a770e6c13f18">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[车主姓名]]></text>
</staticText>
<staticText>
<reportElement x="0" y="11" width="44" height="12" uuid="e3b541a2-001d-4a3c-b67d-c687f923f78e">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[车主电话]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="44" y="0" width="177" height="12" uuid="7f7dfe97-543c-4dae-8af6-d7113d7a438e">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="44" y="11" width="177" height="12" isPrintWhenDetailOverflows="true" uuid="b8c5fc03-800c-4e05-bfe1-e47ec4dfbcb8">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{cellPhone}]]></textFieldExpression>
</textField>
</band>
<band height="12">
<staticText>
<reportElement x="0" y="0" width="44" height="12" uuid="92f7d079-701e-433b-80f4-54b2bffec66c">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[适用门店]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="44" y="0" width="177" height="12" isPrintWhenDetailOverflows="true" uuid="0e37d554-d28f-405d-8f0a-ea70b390ee9d">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgName}!=null?$P{orgName}:""]]></textFieldExpression>
</textField>
</band>
<band height="12">
<staticText>
<reportElement x="0" y="0" width="44" height="12" uuid="a58d2826-ecce-4305-a937-880861b60320">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[限定车辆]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="44" y="0" width="177" height="12" isPrintWhenDetailOverflows="true" uuid="fb93e076-c62e-40f1-ad63-412181861bbb">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="15"/>
</textElement>
<textFieldExpression><![CDATA[$P{carNo}!=null?$P{carNo}:""]]></textFieldExpression>
</textField>
</band>
<band height="12">
<staticText>
<reportElement x="0" y="0" width="33" height="12" uuid="3776a7e6-1f34-4e6e-b685-28ea1beb953f">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[备注:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="33" y="0" width="188" height="12" isPrintWhenDetailOverflows="true" uuid="49649536-e37f-4a5c-8fbf-35e71314c888"/>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
</textField>
</band>
<band height="6">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
<line>
<reportElement x="5" y="4" width="202" height="1" uuid="d13f3282-835d-4a3f-bfaf-de1bc0f02c6f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="42">
<printWhenExpression><![CDATA[$P{relationServices} != null]]></printWhenExpression>
<staticText>
<reportElement x="5" y="6" width="64" height="12" uuid="b46dad1a-e4e2-4cbb-8078-8fadef5c2421">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[限定项目条件]]></text>
</staticText>
<componentElement>
<reportElement x="12" y="21" width="200" height="12" uuid="7544bbec-c542-4d41-ae3c-a434b41bfc67">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="net.sf.jasperreports.export.headertoolbar.table.name" value=""/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="0c7af790-dee9-440a-9406-7ca725234db3">
<dataSourceExpression><![CDATA[$P{relationServices}]]></dataSourceExpression>
</datasetRun>
<jr:column width="130" uuid="854655c8-8e29-4621-8d50-301a091b089c">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<jr:detailCell height="12">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="130" height="12" uuid="17589590-fe21-4b4b-ac94-6c89446150af"/>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="70" uuid="4a3d6479-0f51-4e1b-8499-f5e408b62814">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<jr:detailCell height="12">
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="70" height="12" uuid="948b7821-5590-4c82-a0ba-540f0c14ea99"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体"/>
<paragraph leftIndent="23"/>
</textElement>
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="10">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
<line>
<reportElement x="5" y="4" width="202" height="1" uuid="8e4022d7-288e-46d5-9e8b-322f809389b5">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="42">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{relationParts} != null]]></printWhenExpression>
<staticText>
<reportElement x="5" y="6" width="64" height="12" uuid="3eb5fc85-6d1a-4e66-a6bb-90ffab0419bf">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[限定材料条件]]></text>
</staticText>
<componentElement>
<reportElement x="12" y="21" width="200" height="12" uuid="0e8c9514-c83d-46bb-a398-df11b50450b3">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
<datasetRun subDataset="SubscriptionInfoRelationBean" uuid="a7ec5475-0753-496e-ba0c-821200ca3b09">
<dataSourceExpression><![CDATA[$P{relationParts}]]></dataSourceExpression>
</datasetRun>
<jr:column width="130" uuid="521fa5b3-9477-4345-a4f1-22b89cd526b4">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<jr:detailCell height="12">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement stretchType="RelativeToBandHeight" x="0" y="0" width="130" height="12" uuid="00aa7b2c-66f5-4b2a-86ef-e353042a169e"/>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{infoName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="70" uuid="7d0f5ffc-76f9-4f63-9218-1af7cb5a5fbd">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<jr:detailCell height="12">
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="70" height="12" uuid="006c2c0f-2b11-4379-964b-6b7769307ee7"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体"/>
<paragraph leftIndent="23"/>
</textElement>
<textFieldExpression><![CDATA[$F{labelName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="6">
<line>
<reportElement x="5" y="4" width="202" height="1" uuid="394e187e-5449-424f-807c-bb0c62e2d18d">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="29">
<textField isBlankWhenNull="true">
<reportElement x="130" y="5" width="70" height="12" uuid="3d050094-7d49-4e3f-bca4-47d99321ed82">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{amountAll}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="5" width="130" height="12" uuid="4e9ec6ff-a558-48bb-a529-e94936a53b12">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[金额]]></text>
</staticText>
<line>
<reportElement x="15" y="23" width="195" height="1" uuid="267b9040-1d0d-4050-8906-04ebcdfba7b0">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="18">
<printWhenExpression><![CDATA["7100".equals($P{balanceStatus})]]></printWhenExpression>
<componentElement>
<reportElement x="0" y="5" width="200" height="12" uuid="9ba8d55a-8e43-4aee-af93-ffdb8f2ec5ce">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
<datasetRun subDataset="Dataset1" uuid="a4a70f4b-c3cb-4482-95ff-149fb66ea7f6">
<dataSourceExpression><![CDATA[$P{gatheringList}]]></dataSourceExpression>
</datasetRun>
<jr:column width="130" uuid="3b60f38e-2a7c-43b7-b976-071e57aeadf1">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<jr:detailCell height="12">
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="130" height="12" uuid="faf9cca2-d266-463d-91ee-cc7cc3575f24"/>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{paymentType}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="70" uuid="56829dd0-7286-4bbf-825a-48d016b353bd">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<jr:detailCell height="12">
<textField isBlankWhenNull="true">
<reportElement x="0" y="0" width="70" height="12" uuid="f8450d4c-7ad6-4237-ba69-56862a2d9a6d"/>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{amount}.setScale( 2, BigDecimal.ROUND_DOWN )]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="44">
<line>
<reportElement x="15" y="5" width="195" height="1" uuid="425e895d-8b95-436e-bd82-55a6c4d4cbf8">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
<staticText>
<reportElement x="0" y="11" width="44" height="12" uuid="c3e05a21-6ba4-4708-991a-825a994a1b2f">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户签字]]></text>
</staticText>
<line>
<reportElement x="15" y="28" width="195" height="1" uuid="b6bb8778-b57c-4fda-9e09-1a7eac4ca4e7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
<staticText>
<reportElement x="0" y="32" width="222" height="12" uuid="8a83c68d-8555-4889-a063-2f78dce9af4a">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[请妥善保管购物凭证,谢谢惠顾!]]></text>
</staticText>
</band>
<band height="12">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="53" y="0" width="169" height="12" isPrintWhenDetailOverflows="true" uuid="429e5e34-e7cc-4b2e-a7ec-f9bdc72268ce">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{detailAddress}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="53" height="12" uuid="91cf76df-ccf1-417b-8ad1-73b7e1be2306"/>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[门店地址:]]></text>
</staticText>
</band>
<band height="24">
<textField isBlankWhenNull="true">
<reportElement x="53" y="12" width="169" height="12" uuid="bee0af12-b474-4e3a-8218-141663bc62f2">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="33" y="0" width="189" height="12" uuid="0f0d0893-f0ea-4221-b89f-b7a8ab6461be">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{contactMobile}.split(" ")[1].length() == 5 ? $P{contactMobile}.split(" ")[0] : $P{contactMobile}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="33" height="12" uuid="d007cd43-b97e-4837-badf-79a6f4fad415">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[电话:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="12" width="53" height="12" uuid="3bd289b6-ad7a-4e24-b8b0-cbf579a979f2">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[打印时间:]]></text>
</staticText>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,510 @@
# 工单结算单接口文档
# 打印单最新接口参数
#### maintain接口
**接口:/print/dispatchPrint/genUrl**
**方法:post**
支持场景:
各类结算单
不支持:上海结算单
**入参:**
```plaintext
{
"pkId": "14581820313319918809",
"rowCode": "costSettlePrint",
"rowId": "12"
}
```
```plaintext
{
"code": 200,
"data": {
"url": "http://s1.f6yc.com/printserver/test/printFile/201912/191213154046701.pdf"
},
"message": "SUCCESS"
}
```
#### erp接口
**/print/getPrintPDFPath.do**
templateId=56&templateType=newSettlePrint&idSourceBill=10546443563897503197
**rest  get**
**支持各种新版打印类型**
```plaintext
{
"code": 200,
"data": "http://s1.f6yc.com/printserver/test/printFile/201912/191213154046701.pdf",
"message": "SUCCESS"
}
```
#### jasper取参对照
```plaintext
{
"data": {
"cellPhone": "15421562365", //联系电话
"naCustomer": "0322新", //单位名称
"repairPerson": "", //送修人
"carOwnerName":"", // 车辆所有人
"accountNumber": "", //账号
"billNo": "GD20190517001", //工单号
"carNoWhole": "苏1542", //车牌号
""
"carColor":"" //车身颜色
"orgDetailAddress": "江苏省盐城市阜宁县豆豆",//联系地址
"vin": "11111111111111111", //车辆VIN码
"naEmployee": "员工1(旧1)", //服务顾问
"billDate": "2019-05-17 11:49",//进厂日期
"businessTypeName": "维修", //维修类别
"deliveryTime": "2019-05-17 12:49",//交车时间(出厂时间)
"email": "126544@qq.com", //组织邮件
"maintainType": "GD", //工单类型
"billStatus":"6300", //单据状态
"orgContactMobile": "15315256232", //联系电话
"memo": "", //工单备注
"orgMemo":"", // 门店备注
"carMemo":"", // 车辆备注
"printCount": "1", //打印次数
"orgContactNumber": "", //联系电话-承修方信息
"carSeriesName": "商用车", //车系名称
"carBrandName": "商用车", //品牌名称
"balanceStatus": "7000", //结算状态
"printTime": "2019-07-26 11:43:48",//打印时间
"firstSettlementTime":"2019-07-26 11:43:48",//结算时间(第一次收款时间)
"orgName": "新公司测试", //单位名称-承修方信息
"abbreviation": "门店简称", //门店简称
"engineNumber": "123", //发动机号
"transmissionNo": "123", //变速箱号
"creationtime": "2019-05-17 11:50:46.0",//创建时间
"creatorName": "员工1(旧1)", //创建人名称
"employeePhone":"18734033191", //服务顾问手机号
"paymentTypeDetails":"记账", //支付方式(记账公司)汇总
"bankAccount": "", //开户银行
"naInsurer":"", //理赔公司名称
"insurancepolicyNo":"", //理赔单理赔保险单号
"mergePackageContent": "1", //套餐合并标识
"totalStuffNum": 1.0, //材料数量合计
"selfTotalStuffNum": 0.0, //自带材料数量合计
"serviceNum": "1", //维修项目小计
"spreadRate": 0.0, //进销差价率
"managementCost": 0.0, //进销差价合计
"amountAll": 160.0, //应收总计
"serviceDisCountSubTotal": 100.0, //项目折后金额合计
"stuffSubtotalAll": 60.0, //材料费小计
"serviceSubtotalAll": 100.0, //工时费小计
"receiptAmount": 160.0, //实收金额
"chineseAmount": "壹佰陆拾元整", //实收金额(大写)
"oweAmount": 160.0, //未收金额
"remainAmount":1.0, //结算金额tsf
"receivedAmount":1.0, //收款金额tsf
"settleOweAmout":1.0, //结算单中用的待付金额(未收)
"settleOweAmoutChinese":"壹", //待付金额大写
"settleReceivedAmout":1.0, //实付金额
"settleReceivedAmoutChinese":1.0, //实付金额大写
"totalWorkHour": 1.0, //项目工时合计
"serviceFavourableTotal": 52.0, //项目优惠金额合计
"serviceFavourableCommonTotal": 52.0,//普通项目优惠金额合计
"stuffSubtotalAll": 532.0, //材料费小计
"partFavourableTotal": 102.0, //材料优惠金额合计
"partFavourableCommonTotal": 92.0, //普通材料优惠金额合计
"stuffDisCountTotal": 60.0, //材料折后金额合计
"extraCostTotal": 0.0, //附加费小计
"allOtherCost": 0.0, //附加费合计应收
"packageFavourable": 0.0, //套餐优惠
"czkExpense": 0.0, //储值卡消费金额
"vipExpense": 0.0, //会员卡消费金额
"czkExpenseFavourable": 0.0, //储值卡优惠金额
"czkDiscountFavourable": 0.0, //储值卡办卡优惠金额
"vipExpenseFavourable": 0.0, //计次卡/套餐卡优惠金额
"partinfoDiscountFavourable": 0.0, //材料折扣优惠
"partinfoFavourable": 0.0, //材料项目(非会员项目)客户等级优惠
"couponFavourable": 0.0, //优惠券优惠
"pointFavourable": 0.0, //积分优惠
"discountFavourable": 0.0, //结算时设置的结清优惠
"gatheringFavourable": 0.0, //收银时设置的收银优惠
"customerLevelFavourable": 0.0, //客户级别优惠金额
"serviceFavourable": 0.0, //服务项目(非会员项目)客户等级优惠
"disCountAll": 0.0, //总优惠合计
"disCountAllBak":0.0, //总优惠合计bak
"printContent": "", //免责条款
"printContentEntrust":"", //委托单免责条款
"mainCostList": [ //维修结算费用集合
{
"subtotal": 60.0, //价格
"sortNumber": "1", //序号
"costName": "材料费" //名称
},
{
"subtotal": 100.0,
"sortNumber": "2",
"costName": "工时费"
},
{
"subtotal": 160.0,
"sortNumber": "3",
"costName": "合计"
}
],
"partList": [ //工单对应配件材料集合
{
"unit": "个", //单位
"isBring": 0, //是否自带,1表示自带,0表示非自带
"discountedSubtotal": 60.0, //折后金额
"price": 60.0, //价格
"partName": "分全", //材料名称
"number": 1.0, //数量
"subtotal": 60.0, //金额
"taxRateOutput": 0.13, //销项税率
"singleFavourable":0.0, //优惠金额
"partBrand":"", //配件品牌
"spec":"", //规格型号
"supplierCode":"", //供应商编码(零件号)
"customCode":"", //材料编码
"isMember":0, //是否是会员卡材料 1是
"discount":0.5, //折扣
"partMemo":"", //备注
"employeeName":"", //员工名称(维修技师)
"cargoSpace":"", //货位
"outStockEmployeeName":"", //领料人
"sortNumber": "1" //序号
}
],
"serviceList": [ //工单对应项目集合
{
"discountedSubtotal": 100.0,//折后金额
"price": 100.0, //工时单价
"workHour": 1.0, //工时
"subtotal": 100.0, //金额
"taxRateOutput": 0.13, //销项税率
"sortNumber": "1", //序号
"discount":1, //折扣
"singleFavourable":0.0, //优惠金额
"isMember":1, //是否是会员卡项目 1是 0否
"empNameStr":"", //修理工
"unusedNumber":1, //会员卡项目-未使用次数
"number":2, //会员卡项目-总次数
"infiniteFlag":1, //是否无限,0:否,1:是
"serviceName": "0322新" //项目名称
}
],
"cardList": [ //会员卡列表
{
"favourable": 20, //优惠
"amount": 2019799, //余额
"consumeAmount":10, //本次消费金额
"memberCardNo": "123232rg",//卡号
"name": "测试洗车卡项目2" //卡名称
}
],
"czkList": [ //储值卡列表
{
"favourable": 20, //优惠
"amount": 2019799, //余额
"consumeAmount":10, //本次消费金额
"memberCardNo": "123232rg",//卡号
"name": "测试洗车卡项目2" //卡名称
}
],
"extraPrintVo": { //附加项目
"processItemName": "加工", //加工条目名称
"checkCustomName": "检测费", //检测费名称
"diagnosisCustomName": "诊断费", //诊断费名称
"diagnosisItemName": "维修诊断", //维修诊断项目
"diagnosisMemo": "", //诊断费备注
"diagnosisCost": 0.0, //诊断费
"commissionCustomName": "代办费", //代办费名称
"managementCustomName": "管理费", //管理费名称
"commissionMemo": "", //代办费备注
"processMemo": "", //加工费
"commissionCost": 0.0, //代办费
"checkCost": 0.0, //检测费
"managementCost": 0.0, //管理费
"processCustomName": "加工费", //加工费名称
"processCost": 0.0, //加工费
"managementMemo": "", //管理费备注
"checkMemo": "" //检测费备注
}
},
"storeId": 25965086392720693,
"tempId": 123
}
```
#### 参数说明(完整)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| \------基础信息 | | |
| **billNo** | String | _工单号_ |
| **maintainType** | String | _工单类型_ |
| **balanceStatus** | String | _结算状态_<br/>_"7000" -- 未结算(即存在待付金额)_<br/>_"7100" -- 已结算_<br/>_"7200"  -- 部分结算_ |
| **creatorName** | String | _创建人名称_ |
| **naEmployee** | String | _服务顾问_ |
| **spreadRate** | Double | _进销差价率_ |
| carCategoryName | String | _客户车辆分类名称_ |
| **creationtime** | String | _创建时间_ |
| **memo** | String | _工单备注_ |
| **printCount** | String | _打印次数_ |
| **printTime** | String | _打印时间_ |
| **deliveryTime** | String | _交车时间(出厂时间),收款取收款时间,未收款完工的取完工时间,未完工的取预计交车时间_ |
| **mergePackageContent** | String | _套餐合并标识_ |
| **printContent** | String | _免责条款_ |
| **engineNumber** | String | _发动机号_ |
| **transmissionNo** | String | 变速箱号 |
| **printMaintainGuaZi** | String | _guazi标识_ |
| **amountAll** | Double | _应收总计_ |
| **disCountAll** | Double | _总优惠合计_ |
| **oweAmount** | Double | _未收金额_ |
| **vipExpense** | Double | _会员卡消费金额_ |
| **vipExpenseFavourable** | Double | _计次卡/套餐卡优惠金额_ |
| **czkExpense** | Double | _储值卡消费金额_ |
| **czkExpenseFavourable** | Double | _储值卡优惠金额_ |
| **czkSettleFavourable** | Double | _储值卡结算优惠金额(仅未收款返回)_ |
| **accountAmount** | Double | 记账金额 |
| **totalOweAmount** | Double | _未付金额_ |
| **serviceFavourable** | Double | _服务项目(非会员项目)客户等级优惠_ |
| **partinfoFavourable** | Double | _材料项目(非会员项目)客户等级优惠_ |
| **partinfoDiscountFavourable** | Double | _材料折扣优惠_ |
| **pointFavourable** | Double | _积分优惠_ |
| **packageFavourable** | Double | _套餐优惠_ |
| **discountFavourable** | Double | _结算时设置的结清优惠_ |
| **gatheringFavourable** | Double | _收银时设置的收银优惠_ |
| **couponFavourable** | Double | _优惠券优惠_ |
| **customerLevelFavourable** | Double | _客户级别优惠金额_ |
| **customerLevelName** | String | _客户级别_ |
| **customerDetailAddress** | String | 客户详细地址 |
| **channelName** | String | 来店途径名称 |
| **receiptMemo** | String | 收银备注 |
| **customerSourceName** | String | _客户来源名称_ |
| **carSourceName** | String | _车辆来源名称_ |
| \-----托修方信息 | | |
| **naCustomer** | String | _单位名称/托修方_ |
| **repairPerson** | String | _送修人_ |
| **carNoWhole** | String | _车牌号整体_ |
| **carBrandName** | String | _品牌名称_ |
| **carSeriesName** | String | _车系名称_ |
| carMemo | String | 车辆备注 |
| **businessTypeName** | String | _维修类别_ |
| **vin** | String | _车辆VIN码_ |
| carFuelTypeNameOriginal | carFuelTypeNameOriginal | 燃料类型 |
| **billDate** | String | _进厂日期_ |
| **mileage** | java.math.BigDecimal | _出厂里程_ |
| **contractNumber** | | _合同编号(空)_ |
| **certificateNumber** | | _合格证号(空)_ |
| **cellPhone** | String | _联系电话_ |
| **email** | String | _组织邮件_ |
| \------承修方信息 | | |
| **orgName** | String | _单位名称_ |
| **abbreviation** | String | 门店简称 |
| **orgContactNumber** | String | _联系电话_ |
| **orgDetailAddress** | String | _联系地址_ |
| **orgContactMobile** | String | _联系电话_ |
| **bankAccount** | String | _开户银行_ |
| **accountNumber** | String | _账号_ |
| **businessLicenseCode** | String | 营业执照编码 |
| \------项目信息 | | |
| serviceList | array | 项目条目 |
| #### orderNumber | String | 序号(验证可用) |
| sortNumber | String | _序号_ |
| customCode | String | 项目编码 |
| serviceName | String | _项目名称_ |
| **labelName** | String | _业务分类名称_ |
| **nameMember** | String | _会员项目的来源名_ |
| **labelName** | String | 业务分类 |
| price | Double | _工时单价_ |
| workHour | Double | _工时_ |
| subtotal | Double | _金额_ |
| ```plaintext<br> taxRateOutput <br>``` | BigDecimal | _销项税率_ |
| ```plaintext<br> singleFavourable <br>``` | Double | 优惠金额 |
| discountedSubtotal | Double | _折后金额_ |
| **serviceMemo** | String | _单据服务项目备注_ |
| discount | Double | 折扣 |
| unusedNumber | Integer | 会员卡项目-未使用次数 |
| number | Integer | 会员卡项目-总次数 |
| ```plaintext<br>favourableVoList <br>``` | List<FavourableDetailPrintVo> | 优惠明细 |
| ```plaintext<br>discountType <br>``` | Integer | 优惠类型(编码) |
| ```plaintext<br>discountTypeName <br>``` | String | 优惠类型名称 |
| amount | Double | 优惠金额 |
| ```plaintext<br>sourceId <br>``` | String | 优惠项目的主键,如:如果优惠项是优惠券,那么该字段为优惠券的id |
| **memo** | String | 项目说明 |
| **qualityCheckEmployeeName** | String | 质检人姓名 |
| **qualityCheckEmployeeCode** | String | 质检人工号 |
| **cooperationMemo** | String | 协作备注 |
| **totalWorkHour** | Double | _项目工时合计_ |
| **serviceSubtotalAll** | Double | _工时费小计_ |
| **serviceNum** | Double | _维修项目小计_ |
| **serviceDisCountSubTotal** | Double | _项目折后金额合计_ |
| \-------材料信息 | | |
| partList | array | 材料条目 |
| #### orderNumber | String | 序号(验证可用) |
| sortNumber | String | _序号_ |
| customCode | String | 材料编码 |
| partName | String | _材料名称_ |
| **partBrand** | String | _配件品牌_ |
| spec | String | 规格型号 |
| standard | String | 规格型号(旧) |
| **partShowName** | String | _配件名称规格型号品牌_ |
| **supplierCode** | String | _供应商编码(零件号)_ |
| unit | String | _单位_ |
| number | Double | _数量_ |
| **nameMember** | String | _会员项目的来源名_ |
| price | Double | _价格_ |
| cost | Double | _材料成本_ |
| subtotal | Double | _退货金额_ |
| taxRateOutput | BigDecimal | _销项税率_ |
| singleFavourable | Double | 优惠金额 |
| discountedSubtotal | Double | _折后金额_ |
| **partMemo** | String | _单据服务材料备注_ |
| discount | Double | 折扣 |
| **applyModel** | String | 适用车型 |
| ```plaintext<br>cargoSpace <br>``` | String | 材料货位 |
| **defSeats** | List<String> | 材料货位列表 |
| ```plaintext<br>favourableVoList <br>``` | List<FavourableDetailPrintVo> | 优惠明细 |
| ```plaintext<br>discountType <br>``` | Integer | 优惠类型(编码) |
| ```plaintext<br>discountTypeName <br>``` | String | 优惠类型名称 |
| ```plaintext<br>amount <br>``` | Double | 优惠金额 |
| ```plaintext<br>sourceId <br>``` | String | 优惠项目的主键,如:如果优惠项是优惠券,那么该字段为优惠券的id |
| \-------自带材料(新版维修/贴膜)信息 | | |
| **bringPartList** | array | 自带材料条目 |
| **partShowName** | String | 自带材料名称(文本) |
| **photoList** | List<String> | 自带图片路径(url |
| empNameStr | String | 技师 |
| outStockEmployeeName | String | 领料人 |
| **isBring** | String | _是否自带_ |
| **selfPartList** | array | _工单对应配件自带材料集合(内容同上面材料)_ |
| **totalStuffNum** | String | _材料数量合计_ |
| **selfTotalStuffNum** | String | _自带材料数量合计_ |
| **stuffSubtotalAll** | Double | _材料费小计_ |
| **partFavourableTotal** | Double | _材料优惠金额合计_ |
| **partFavourableCommonTotal** | Double | _普通材料优惠金额合计_ |
| **stuffDisCountTotal** | Double | _材料折后金额合计_ |
| **managementCost** | Double | _进销差价合计_ |
| \------附加费用信息 | | |
| **extraChargeList** | | |
| **sortNumber** | String | _序号_ |
| **extraName** | String | _附加费名称_ |
| **subtotal** | Double | _金额_ |
| **memo** | String | _备注_ |
| \-----维修结算费用集合(江苏结算单) | | |
| **mainCostList** | array | |
| **sortNumber** | String | _序号_ |
| **costName** | String | _名称_ |
| **memo** | String | _备注_ |
| **subtotal** | Double | _金额_ |
| \----_其他结算费用集合(江苏结算单)_ | | |
| **otherCostList** | array | 内容同上 维修结算费用集合 |
| **extraCostTotal** | Double | _附加费小计_ |
| **allOtherCost** | Double | _附加费合计应收_ |
| **favourableExtraCost** | Double | _附加费优惠金额_ |
| **receiptAmount** | Double | _实收金额(已收金额-储值卡金额,如未收款,则还加入了欠款金额+客户等级优惠-积分优惠-结清优惠)_ |
| **receiptAmountChinese** | String | _实收金额大写(逻辑同上)_ |
| **amountReal** | Double | 工单收款后真正的实收 |
| **chineseAmount** | String | _实收金额(大写)(逻辑同上)_ |
| **payItemTogetherChinese** | String | _付款方式总额大写_ |
| **payItemTogetherExcludeAccountAmountChinese** | String | 付款总额(排除记账金额)大写 |
| **settleOweAmout** | Double | 结算单中用的待付金额(未收),使用后台逻辑算好 |
| **settleOweAmoutChinese** | String | 待付金额大写 |
| \-----结算付款方式及优惠保存信息 | array | |
| **settlementPayItemList** | | |
| **payWay** | String | _付款方式_ |
| **payAmount** | Double | _付款金额_ |
| **chinesePayAmount** | String | _大写付款方式_ |
| **accountAgreementName** | String | _记账客户名称_ |
| \-----付款方式信息 | array | |
| **payItemList** | | |
| **payWay** | String | _付款方式_ |
| **payAmount** | Double | _付款金额_ |
| **chinesePayAmount** | String | _大写付款方式_ |
| \-----附加项目(江苏结算单) | | |
| **extraPrintVo** | obj | |
| **commissionCustomName** | String | _代办费自定义名称_ |
| **commissionCost** | Double | _代办费成本_ |
| **commissionMemo** | String | _代办费备注_ |
| **diagnosisCustomName** | String | _诊断费自定义名称_ |
| **diagnosisCost** | Double | _诊断费成本_ |
| **diagnosisItemName** | String | _诊断详细名称_ |
| **diagnosisMemo** | String | _诊断费备注_ |
| **checkCustomName** | String | _检查费自定义名称_ |
| **checkCost** | Double | _检查费成本_ |
| **checkMemo** | String | _检查费备注_ |
| **processCustomName** | String | _加工费自定义名称_ |
| **processCost** | Double | _加工费成本_ |
| **processMemo** | String | _加工费备注_ |
| **processItemName** | String | _加工详细名称_ |
| **managementCustomName** | String | _管理费自定义名称_ |
| **managementCost** | Double | _管理费成本_ |
| **managementMemo** | String | _管理费备注_ |
| \-----二期新增字段 | | |
| **oilCapacity** | String | 油量 |
| **nextMileage** | Double | _下次保养里程(工单数据源,目前维保、洗车单读取)_ |
| **nextMaintainDate** | String | _下次保养日期(工单数据源,目前维保、洗车单读取)_ |
| **nextMileageRemind** | Double | _下次服务里程(服务提醒数据源,目前维修、贴膜单读取)_ |
| **nextMaintainDateRemind** | Long | _下次服务时间(服务提醒数据源,目前维修、贴膜单读取)_ |
| **repairPersonContact** | String | _送修人联系方式_ |
| **memberCardNo** | String | _会员号_ |
| **points** | String | _积分_ |
| **czkList** | array | 储值卡列表 |
| **name** | String | 名称 |
| **memberCardNo** | String | 卡号 |
| ```plaintext<br> **cardOwner** <br>``` | String | 持卡人 |
| **amount** | Double | 金额 |
| **cardList** | array | 套餐卡列表 |
| **name** | String | 名称 |
| **memberCardNo** | String | 卡号 |
| ```plaintext<br> **cardOwner** <br>``` | String | 持卡人 |
| **amount** | Double | 金额 |
| **combineServiceAndPartList** | array | 项目材料组合列表 |
| **servicePrintVo** | 参见serviceList | |
| **partPrintVo** | 参见partList | |
# 案例记录:
#### 1.结算前 待付 结算后实付(跟进结算状态判断,7100 为已结算)
```plaintext
$P{balanceStatus}.equals("7100")?($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).toString()+($P{payItemTogether}==null?"":"("+$P{payItemTogether}+")")):($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN ))
```
* 对应的汉字
```plaintext
$P{balanceStatus}.equals("7100")?$P{payItemTogetherChinese}:$P{settleOweAmoutChinese}
```
# 工具类jar包附件下载:
[请至钉钉文档查看附件《print-core-1.0.7.jar》](https://alidocs.dingtalk.com/i/nodes/vy20BglGWOexYpophlEGoZvGJA7depqY?iframeQuery=anchorId%3DX02mjljl3qzo6fk6o7712b)
### 数字金额转中文方法调用示例:
**数字金额**$P{amount}==null?BigDecimal.ZERO:$P{amount}
**转中文****com.f6car.printserver.core.CharacterUtil.chinese(**$P{amount}==null?BigDecimal.ZERO:$P{amount})
### 日期时间戳转日期示例:
**日期格式选择:**java.lang.Long
**日期时间戳**$P{nextMaintainDateRemind} **转为目标格式**$P{nextMaintainDateRemind} == null ? "" : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date($P{nextMaintainDateRemind}))
其中,"yyyy-MM-dd HH:mm:ss" 根据实际需求指定,比如到日则选择 "yyyy-MM-dd"
### 洗车单
洗车单小票不支持定制,定制洗车单的模板名称必须包含“洗车单”三个字  ,否则无法显示对应模板
> 更新: 2025-02-24 17:08:50 原文: <https://xcz.yuque.com/ombipo/rpc7ms/ro5fs1>
@@ -0,0 +1,262 @@
# 打印单定制介绍
# 打印单定制介绍
| **最常用的基础操作** |
| --- |
| [1、新增静态文本](#jwTdk) |
| [2、新增动态字段(例如想要展示结算单中的工单号)](#fKwpz) |
| [3、列表中增减字段(例如结算单中的项目列表和材料列表)](#IUyOs) |
| [4、新增边框和样式设置](#RKqIf) |
| [6、保存+输出](#M31IF) |
| [7、打印单后台配置](#gr6xz) |
## 常用链接地址:
###### 打印单后台地址
[http://print.f6yc.com/print-server/ui/index.html#/template/classification](http://print.f6yc.com/print-server/ui/index.html#/template/classification)
###### 打印单模板样式
[https://xcz.yuque.com/ombipo/rpc7ms/fbd6ay?singleDoc#](https://xcz.yuque.com/ombipo/rpc7ms/fbd6ay?singleDoc#) 《打印单各类模板样式》
###### 打印单参数表
[https://xcz.yuque.com/ombipo/rpc7ms/ro5fs1?singleDoc#](https://xcz.yuque.com/ombipo/rpc7ms/ro5fs1?singleDoc#) 《打印单最新接口参数》
###### 打印单工具简易开发教程(附带案例)
[《打印单定制简易开发教程》](https://alidocs.dingtalk.com/i/nodes/dQPGYqjpJYgZGbvbCdEKGDGZWakx1Z5N?utm_scene=team_space)
## 工具下载
jdk1.8 使用 jaspersoft6.8版本
[请至钉钉文档查看附件《Jaspersoft Studio-6.8.0.zip》](https://alidocs.dingtalk.com/i/nodes/20eMKjyp81R0ndXdsdYe4BaDWxAZB1Gv?corpId=&iframeQuery=anchorId%3DX02mgkgykvfbfbiqcbc8b4)
WIN
[请至钉钉文档查看附件《Jaspersoft Studio-6.3.1.final.rar》](https://alidocs.dingtalk.com/i/nodes/20eMKjyp81R0ndXdsdYe4BaDWxAZB1Gv?corpId=&iframeQuery=anchorId%3DX02mki20wvdslzwftp0ao)
MAC
[请至钉钉文档查看附件《TIBCOJaspersoftStudio-6.3.1.final-mac-x86\_64.zip》](https://alidocs.dingtalk.com/i/nodes/20eMKjyp81R0ndXdsdYe4BaDWxAZB1Gv?corpId=&iframeQuery=anchorId%3DX02mki26xyvtjfkmi00ki)
## 打印单模板修改流程
#### 1、下载需要的模板
###### 通过模板名称,直接到模板管理中通过模板名称查询并下载
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715248827418-8d2597e8-0ded-4cb8-9825-202074613dcc.png)
#### 2、打开编辑工具 TIBCO Jaspersoft Studio
###### 打开文件夹,双击Jaspersoft Studio.exe 运行工具
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715304945610-6a9086af-c9f5-4b88-8c6b-1ef6bc11e8a0.png)
###### 点击File-->Open File-->选择下载的模板文件
###### ![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715305042360-e716b275-c696-4231-b0c8-99c0566fe7c5.png)进入编辑模板的页面
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715305302215-a5f09700-28cc-4e80-a78c-e919b831e857.png)
#### 3、常见编辑操作
##### 1.新增静态文本
###### 新增组件到模板中
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715305970916-23aa4144-a215-4656-8281-0c3385aba707.png)
###### 双击组件编辑显示文本
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306012526-86e4bc9d-73dc-4120-abbf-fbedcf99c47f.png)
###### 调整组件大小和位置参数
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306133042-c7c17a7c-138c-4b4c-b587-11782185e910.png)
##### 2.新增动态字段(例如想要展示结算单中的工单号)
###### 在参数表中搜索想要的参数名称和类型:名称是:billNo  类型是文本信息=java.lang.String
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306460522-232a3499-d104-470d-be1a-0b1fb0957331.png)
###### 拖拽一个 Text Field 组件到模板中
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306292127-d6629324-8ef8-4ed8-a35e-a77d543bac2d.png)
###### 双击组件写入公式固定写法$P{参数名称}
###### ![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306617239-b7ce26f3-c520-4266-83c2-8b18196e2ea9.png)
###### 遇到提示:The current expression is not valid. Please verify it!;表明这个参数在模板中没有预先创建,需要手动创建参数信息
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306763081-4495ab86-c432-455f-b06b-7b15fb0b8a3e.png)
###### 模板中预设参数信息:Outline-->Parameters(右键单击)-->Create Parameter
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715306875194-8d8a509a-23a8-4563-b6b7-ba6c673151e1.png)
###### 编辑参数信息:Name(填写参数名称);Class(数字就选择:java.math.BigDecimal   文本就选择java.lang.String
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715307033769-86f5efa1-d6e2-4d9a-a322-849079503f21.png)
##### 3.列表中增减字段(例如结算单中的项目列表和材料列表)
###### 例如查找材料名称,可以发现参数名是partName,是在一个名字叫partList 的列表里面的,在材料信息的列表中能使用到的参数就只有partList下的这个参数,其他参数无法在列表中直接使用(例如工单号在材料列表中展示不了)
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715307395294-6a1cf006-260b-4f9e-950f-b0f34fa9c3c9.png)
###### 双击需要编辑的列表,进入列表编辑页面
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715308008053-5abeeebb-84ad-4fb3-994b-e0a360ea96d8.png)
###### 编辑方式与新增动态字段相同,但是固定写法从$P{参数名称} 改为 $F{参数名称}
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715308039626-e11f409f-014a-4146-a578-38d2cab4d1e0.png)
###### 提示The current expression is not valid. Please verify it!  参数没有预设时,在列表编辑页面中新增,逻辑与上面相同![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715313291292-8fe511dd-64f4-4ee6-971e-f8d1f0a7dfb0.png)
##### 4.新增边框和样式设置
###### 选择需要编辑的组件,选择Boeders 进行编辑
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715308327002-0c996de2-b92b-4477-9f5d-5e68a9be188b.png)
#### 4、常见语法介绍
###### 文本拼接参数:$P{参数名称}+"特定文本内容"  —— 例如打印单标题:$P{printOrgName}+"结算单"
###### 小数保留2位小数或多位:$P{参数名称}.setScale( 保留几位小数, BigDecimal.ROUND\_DOWN ) ——例如折后金额小计,保留2位小数:$P{stuffSubtotalAll}.setScale( 2, BigDecimal.ROUND\_DOWN )
###### 字符串截取:$P{参数名称}.substring(起始位置,截取长度)——例如进厂日期,保留前10位:$P{billDate}.substring(0,10)
###### 三元运算-IF判断:(关系表达式) ? 表达式1 : 表达式2 ——例如打印单标题:($P{printOrgName}==null?$P{orgName}:($P{printOrgName}.isEmpty()?$P{orgName}:$P{printOrgName}))+"结算单"
###### 常见运算:
是否相等:”==“  或者 $P{参数名称}.equals("文本内容")
加:$P{参数名称1}.add($P{参数名称2})
减:$P{参数名称1}.subtract($P{参数名称2})
乘:$P{参数名称1}.multiply($P{参数名称2})              $P{参数名称1}.multiply(new BigDecimal(1.13))
除:$P{参数名称1}.divide($P{参数名称2}, 2, BigDecimal.ROUND_HALF_UP)
#### 4.1、高级语法介绍
###### jar包导入:例如金额转大写,研发通过编写一个jar工具包实现特定功能,下面是导入jar包步骤
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/oJGq75kaYJrZ2lAK/img/882b929c-310e-4525-a463-15def4e3fac3.png)
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/oJGq75kaYJrZ2lAK/img/3839a386-06ab-442a-b0e3-997afd7e0a94.png)
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/oJGq75kaYJrZ2lAK/img/76ef341c-a67b-4f30-816d-73497eb8d703.png)
启用成功后按照研发语法实现具体功能,比如![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/oJGq75kaYJrZ2lAK/img/0d61f87d-719a-4af6-bc34-c06a2fabf5b8.png)
###### 从list中取特定的值写入外层表格中:该公式使用jdk1.8语法,jaspersoft6.8可用
下面表达式的意思是,从支付方式列表(payItemList) 中找到
支付方式(payWay) 
等于“记账”的
第一个支付金额(payAmount
```java
$P{payItemList}.getData().stream()
.filter(map -> "记账".equals(map.get("payWay")))
.map(map -> {
Object amt = map.get("payAmount");
return amt == null ? BigDecimal.ZERO : new BigDecimal(amt.toString());
})
.findFirst()
.orElse(BigDecimal.ZERO)
```
#### 5、格式预览
###### 工具中只能预览模板的样式,涉及到参数判断的需要将模板上传到门店后在F6系统工单中打印预览
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311314096-6d38326f-8c42-4e22-b8c4-59f23f1ad075.png)
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311442290-12854551-e373-40ea-b19a-c0d9fe59393b.png)
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311456159-163f97ae-fb32-46b3-8548-42fd39bc66e4.png)
#### 6、保存+输出
###### 保存
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311668513-2cf8626b-3d4f-4a8d-8a0e-71b2e1300189.png)
###### 选择.jrxml的文件,右键选择Compile Report 进行编译
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311723006-c971ca0b-90d1-4568-90f4-78861c437d62.png)
###### 选择.jasper的文件,右键选择Export Files to...
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311841522-716ed408-0069-449d-a460-4a483c724ca5.png)
###### 另存到桌面
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715311906058-9ae58e93-f4d9-487d-96fa-e87450747f8c.png)
#### 7、打印单后台配置
###### 新增/编辑模板
注意:不要随便删除模板,删除一定要再三确认清楚,避免出现误操作的情况(删除不可恢复)  预计5.16号后 对删除的功能二次确认进行优化。
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715312153740-d570281c-2e63-432a-885f-2d4d703a810d.png)
###### 模板名称命名规范
* **简单调整**\*\***\*\*模板名称:基础表+特殊修改需求**
**模板编码:修改人姓名首字母英文大写+模板分类+日期**
**模板备注:模板各修改点**
![image](https://ddoc.f6yc.com/yuque/0/2024/png/245629/1715334288403-7a693418-2995-4707-a36c-87ee83a0e7f5.png)
* **定制调整**\*\***\*\*模板名称:门店名称+定制**
**模板编码:修改人姓名首字母英文大写+模板分类+日期**
**模板备注:模板各修改点**
![image](https://ddoc.f6yc.com/yuque/0/2024/png/245629/1715334472806-b911e666-83b7-4512-bdc0-d08996377303.png)
###### 给指定门店配置打印单
![image](https://ddoc.f6yc.com/yuque/0/2024/png/247998/1715312294997-84f80cc6-8058-4dbc-988d-3681bd2ece7e.png)
#### 8、常见打印分类及对应的通用模板
| 常见打印单分类 | 对应系统上的打印模块 | 通用模板名称 | 模板编码 |
| --- | --- | --- | --- |
| 新结算单打印 | 维保单 | F6标准结算单(壹) | newSettleFirst |
| 结算单-新 | 除维保单的其他单据(维修单、贴膜单......) | 无 | 无 |
| 附表-新 | 新附表 | 无 | 无 |
| 销售单 | 销售单 | 销售单(日期版) | xiaoshodanriqiban |
| 洗车单 | 洗车单 | 洗车单 | wash01 |
| 报价单打印 | 报价单 | 报价单打印 | quotationPrint |
| 新库存入库单打印 | 入库单 | 新库存入库单打印 | 9001 |
| 新库存出库单打印 | 出库单 | 新库存出库单打印 | 9002 |
| | | | |
| **注:采购单和采购退货单走打印平台定制,先向赵亚妮提供门店编码、门店名称,开通后再上传配置门店生效** | | | |
**注意:结算单要在收款后页面打印,请确保模板名称中包含“结算单”三个字**
> 更新: 2025-05-16 13:54:24 原文: <https://xcz.yuque.com/ombipo/obbigo/kg2qc1sbszwk48bf>
@@ -0,0 +1,60 @@
# 报价单接口参数
# 报价单接口参数
# 参数说明
## 主单信息
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| partDetailVoList | 材料列表 | | List<QuotationServiceDetailPrintVo> |
| serviceDetailVoList | 项目列表 | | List<QuotationPartDetailPrintVo> |
| amountChinese | 商品金额中文大写 | | |
| realAmountWithoutCardChinese | 待付金额中文大写 | | |
## QuotationPartDetailPrintVo
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| pkId | 无意义主键 | 是 | BigInteger |
| idOwnOrg | 门店id | 是 | BigInteger |
| idQuotation | 报价单id | 是 | BigInteger |
| idPart | 材料id | 否 | BigInteger |
| partName | 材料名称(非组合,对应材料名称字段) | 是 | String |
| partShowName | 材料在界面上显示的名称(组合供应商编码等信息) | 是 | String |
| idMdmPart | 云材料ID | 否 | String |
| labelId | 业务分类id | 否 | BigInteger |
| labelName | 荣誉的业务分类名称 | 否 | String |
| number | 数量 | 是 | BigDecimal |
| price | 单价 | 是 | BigDecimal |
| subtotal | 总价 | 是 | BigDecimal |
| idEmployee | 服务员工id | 否 | String |
| employeeName | 服务员工姓名 | 否 | String |
| isMember | 套餐2,套餐卡1,普通0 | 是 | byte |
| memo | 备注 | 否 | String |
| discount | 折扣 | 是 | BigDecimal |
| realSubtotal | 折后总计 | 是 | BigDecimal |
| stockNumber | 门店库存数量 | 否 | BigDecimal |
| groupId | 公司id | 是 | BigInteger |
| unit | 单位 | 否 | String |
| spec | 规格型号 | 否 | String |
| brand | 品牌名称 | 否 | String |
| brandId | 品牌id | 否 | String |
| supplierCode | 零件号 | 否 | String |
## QuotationServiceDetailPrintVo
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| pkId | 无意义主键 | 是 | BigInteger |
| customCode | 项目编码 | 否 | String |
| serviceName | 项目名称 | 是 | String |
| workHour | 工时 | 否 | BigDecimal |
| price | 单价 | 是 | BigDecimal |
| subtotal | 工时费 | 是 | BigDecimal |
| discount | 折扣 | 是 | BigDecimal |
| realSubtotal | 折后金额 | 是 | BigDecimal |
| memo | 备注 | 否 | String |
> 更新: 2025-05-26 14:19:50 原文: <https://xcz.yuque.com/ombipo/rpc7ms/gcgxuuzvmyhs4eoe>
@@ -0,0 +1,675 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="jiesuandan" pageWidth="595" pageHeight="842" columnWidth="575" leftMargin="15" rightMargin="5" topMargin="10" bottomMargin="10" uuid="8fdb09f3-da43-46f9-a6cb-2b26a2247961">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
<style name="Table_TH" mode="Opaque" backcolor="#FFFFFF">
<box>
<pen lineWidth="0.5" lineColor="#000000"/>
<topPen lineWidth="0.5" lineColor="#000000"/>
<leftPen lineWidth="0.5" lineColor="#000000"/>
<bottomPen lineWidth="0.5" lineColor="#000000"/>
<rightPen lineWidth="0.5" lineColor="#000000"/>
</box>
</style>
<style name="Table_CH" mode="Opaque" backcolor="#FFFFFF">
<box>
<pen lineWidth="0.5" lineColor="#000000"/>
<topPen lineWidth="0.5" lineColor="#000000"/>
<leftPen lineWidth="0.5" lineColor="#000000"/>
<bottomPen lineWidth="0.5" lineColor="#000000"/>
<rightPen lineWidth="0.5" lineColor="#000000"/>
</box>
</style>
<style name="Table_TD" mode="Opaque" backcolor="#FFFFFF">
<box>
<pen lineWidth="0.5" lineColor="#000000"/>
<topPen lineWidth="0.5" lineColor="#000000"/>
<leftPen lineWidth="0.5" lineColor="#000000"/>
<bottomPen lineWidth="0.5" lineColor="#000000"/>
<rightPen lineWidth="0.5" lineColor="#000000"/>
</box>
</style>
<subDataset name="Dataset1" uuid="22e86b94-acb8-45ed-960f-04558f91ad82">
<queryString>
<![CDATA[]]>
</queryString>
<field name="row1" class="java.lang.String"/>
<field name="row2" class="java.lang.String"/>
<field name="row3" class="java.lang.String">
<fieldDescription><![CDATA[]]></fieldDescription>
</field>
<field name="row4" class="java.lang.String"/>
<field name="row5" class="java.lang.String"/>
<field name="row6" class="java.lang.String"/>
<field name="row7" class="java.lang.String"/>
<field name="row8" class="java.lang.String"/>
<field name="row9" class="java.lang.String"/>
<field name="row10" class="java.lang.String"/>
<field name="row11" class="java.lang.String"/>
<field name="row12" class="java.lang.String"/>
<field name="row13" class="java.lang.String"/>
<field name="row14" class="java.lang.String"/>
</subDataset>
<parameter name="orgName" class="java.lang.String"/>
<parameter name="gatheringTime" class="java.lang.String"/>
<parameter name="title" class="java.lang.String"/>
<parameter name="printTime" class="java.lang.String"/>
<parameter name="rowList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="column1" class="java.lang.String"/>
<parameter name="column2" class="java.lang.String"/>
<parameter name="column3" class="java.lang.String"/>
<parameter name="column4" class="java.lang.String"/>
<parameter name="column5" class="java.lang.String"/>
<parameter name="column6" class="java.lang.String"/>
<parameter name="column7" class="java.lang.String"/>
<parameter name="column8" class="java.lang.String"/>
<parameter name="column9" class="java.lang.String"/>
<parameter name="column10" class="java.lang.String"/>
<parameter name="column11" class="java.lang.String"/>
<parameter name="column12" class="java.lang.String"/>
<parameter name="column13" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="92">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="-10" width="575" height="84" uuid="7ddf9306-7451-4ea4-bd57-cee86f8e13c9">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="16"/>
</textElement>
<textFieldExpression><![CDATA[$P{title}]]></textFieldExpression>
</textField>
<staticText>
<reportElement positionType="Float" mode="Opaque" x="1" y="74" width="48" height="18" backcolor="#FFFFFF" uuid="5da51596-6830-44c5-98e4-691007be1a4e">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[所属门店:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="49" y="74" width="526" height="18" uuid="954bf3e6-7bf3-4deb-becf-217784bdc82a">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgName}]]></textFieldExpression>
</textField>
</band>
<band height="25">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="260" y="0" width="315" height="18" uuid="36caab98-d260-4527-b87e-568ac54f61eb">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="0" rightIndent="5"/>
</textElement>
<textFieldExpression><![CDATA["收款时间:"+$P{gatheringTime}]]></textFieldExpression>
</textField>
</band>
<band height="36">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="46" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="b7ced69b-d8a8-45d7-b05b-3a3b83324d93">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column2}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column2}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="90" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="c53a49f2-2d34-455e-b62d-b249f1fced35">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column3}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column3}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="134" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="3385087d-7234-411d-8d79-738cac0295fd">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column4}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column4}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="266" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="ccde0db9-971e-4912-9249-27361902f1b9">
<printWhenExpression><![CDATA[$P{column7}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column7}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="310" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="0ae42faa-c6a8-47d6-9620-48ec03fd35d0">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column8}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column8}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="178" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="62d27b1c-bebc-4a97-8fa0-2a6a497ab35e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column5}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column5}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="222" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="e459b7e6-0136-4c4e-ac76-6a4894311481">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column6}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column6}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="442" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="dbb18e36-2d02-4970-9440-c2d8634e5c39">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column11}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column11}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="398" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="49242b96-56f8-4c62-85f6-87a0342c936d">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column10}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column10}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="486" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="900e6d7d-dc55-4f19-8c11-3ad4429f66ac">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{column12}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column12}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="354" y="0" width="44" height="36" backcolor="#D4D4D4" uuid="e0da6daf-273a-4d28-b333-7c91e68a3ee6">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column9}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column9}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="530" y="0" width="45" height="36" backcolor="#D4D4D4" uuid="ad9f6d64-65f9-4dcd-8dbc-37e5899c528a">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{column13}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{column13}]]></textFieldExpression>
</textField>
<staticText>
<reportElement mode="Opaque" x="1" y="0" width="45" height="36" backcolor="#D4D4D4" uuid="162abd58-679f-4c66-a256-458805e89193">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<text><![CDATA[]]></text>
</staticText>
<staticText>
<reportElement x="16" y="0" width="30" height="18" uuid="652d5210-41df-4c62-a4ba-97f2e42b65ac">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<text><![CDATA[内容]]></text>
</staticText>
<staticText>
<reportElement x="1" y="18" width="30" height="18" uuid="6e239a7c-f268-4ba1-b26d-f59cea6bb5ff">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<text><![CDATA[方式]]></text>
</staticText>
<line>
<reportElement mode="Transparent" x="1" y="0" width="45" height="36" backcolor="#D9D9D9" uuid="b02cf093-98f9-4cb6-8bc2-d6611a303b4e"/>
<graphicElement>
<pen lineWidth="0.5"/>
</graphicElement>
</line>
</band>
<band height="19">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<componentElement>
<reportElement isPrintRepeatedValues="false" x="1" y="0" width="574" height="18" uuid="23ae66f5-6e0a-4c24-9fa5-2237d31a1aea">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table 1_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table 1_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table 1_TD"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
<datasetRun subDataset="Dataset1" uuid="f221c952-cad7-4dff-9cf7-ca32d2ddef3b">
<dataSourceExpression><![CDATA[$P{rowList}]]></dataSourceExpression>
</datasetRun>
<jr:column width="45" uuid="9627a917-c5b0-4b80-b21f-449e4a1af32a">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="0" y="0" width="45" height="18" backcolor="#D4D4D4" uuid="05c9f6b2-819e-415f-8505-e8be2889cab5">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$F{row1}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{row1}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="cd30f1df-f54b-42a6-bf80-fcecb57824dc">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="fe6de99c-5be1-4d46-9282-c4d77f126dc9">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<printWhenExpression><![CDATA[$F{row2}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row2}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="badfb61b-4c3d-443e-9d96-ce2f8871c148">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="bbc0c8d1-ff73-4d70-bfd9-a3f514bf894b">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$F{row3}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row3}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="e54a7626-9237-4d3e-bb62-d48c41475cfd">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column4"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="8b267259-7609-4612-b6f2-417498e8409f">
<printWhenExpression><![CDATA[$F{row4}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row4}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="1b3cbfb0-6d88-4b14-9215-cedc2a5b9c5e">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column5"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="7f6b00bd-a8ce-42ca-94e3-0f9202796b7d">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$F{row5}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row5}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="867931a4-9fef-46d7-85df-5d77f61bc7a9">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column6"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="e38c4d71-5e05-43c6-9d85-cbbdc4b2859b">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$F{row6}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row6}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="807004cb-f00a-4852-bc11-2a5ad1ae65b5">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column7"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="5cb940bc-d863-486d-a8e2-730002019abb">
<printWhenExpression><![CDATA[$F{row7}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row7}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="baa16adc-a1b3-4e93-a6ee-5f5b705fd72d">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column8"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="f66b1d3d-3144-4bea-b57a-571187c122f3">
<printWhenExpression><![CDATA[$F{row8}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row8}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="bee2d403-9d56-4d85-8687-2c4101cc6d06">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column9"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="9e46c6f4-841b-467d-97f1-0542179f7cc6">
<printWhenExpression><![CDATA[$F{row9}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row9}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="fd8e7505-d366-4fde-96ac-1df719f7272e">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column10"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="f0783afb-cce2-47d8-be32-55ab498515a6">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$F{row10}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row10}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="53cbd27d-048c-4200-8dd5-b21671c68e4b">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column11"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="cce29502-097d-4763-b5c0-330093e14041">
<printWhenExpression><![CDATA[$F{row11}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row11}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="44" uuid="9f2c3f4f-bc90-430d-b865-795def749d2d">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column12"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<pen lineWidth="0.4"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="44" height="18" uuid="29c0a84c-f4d0-4e4b-8d19-3d13a024552b">
<printWhenExpression><![CDATA[$F{row12}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row12}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="45" uuid="3def2e39-4c08-458e-8c36-df8eac735c65">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column13"/>
<jr:detailCell height="18">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<box>
<pen lineWidth="0.4"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement mode="Opaque" x="0" y="0" width="45" height="18" backcolor="#FFFFFF" uuid="573d45d1-bf4e-43e1-8282-49508130a3c0">
<printWhenExpression><![CDATA[$F{row13}!=null]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="0.4"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{row13}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="75">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="454" y="10" width="52" height="18" uuid="c1af5cd0-e851-4fd6-a16b-f461a743d336">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[出纳签字:]]></text>
</staticText>
<staticText>
<reportElement x="2" y="50" width="48" height="18" uuid="1cf7fa11-8f01-4764-a9a8-d981c9f32ca6">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[打印时间:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="50" width="124" height="18" uuid="b2daacc3-cb7c-443a-946b-be951bf080d5">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{printTime}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="454" y="50" width="52" height="18" uuid="3c4cbf80-712b-4e51-a861-1b1643642612">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收银签字:]]></text>
</staticText>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,243 @@
# 新版附表打印接口文档
# 新版附表打印接口文档
# 接口出参
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| billNo | 附表单号 | String |
| fromBillNo | 附表源工单号 | String |
| fromMaintainType | 来源单据类型 | String |
| billDate | 进厂日期 | String |
| creatorName | 创建人名称 | String |
| creationtime | 创建时间 | String |
| naEmployee | 服务顾问 | String |
| employeePhone | 服务顾问手机号 | String |
| businessTypeName | 业务类型 | String |
| nextMaintainDate | 下次保养日期 | String |
| oilCapacity | 当前油量 | String |
| mileage | 出厂里程(进厂里程) | Double |
| nextMileage | 下次保养里程 | Double |
| vin | 车辆VIN码 | String |
| carNoWhole | 车牌号 | String |
| carModel | 品牌车型全称 | String |
| carModelShort | 车型简称 | String |
| carColor | 车身颜色 | String |
| carCategoryName | 车辆分类名称 | String |
| carBrandName | 车辆品牌名称 | String |
| carSeriesName | 车系名称 | String |
| engineNumber | 发动机号 | String |
| transmissionNo | 变速箱号 | String |
| registerDate | 车辆注册日期 | String |
| cardDate | 车辆发证日期 | String |
| carNatureOfUseName | 车辆使用性质 | String |
| carFuelTypeName | 车辆燃料(能源)类型 | String |
| carSourceName | 车辆来源 | String |
| carOwnerName | 车辆所有人姓名 | String |
| naCustomer | 客户姓名 | String |
| customerSourceName | 客户来源名称 | String |
| customerDetailAddress | 客户详细地址 | String |
| cellPhone | 联系电话(客户) | String |
| memberCardNo | 会员卡号 | String |
| points | 客户积分 | String |
| customerLevelName | 客户等级名称 | String |
| repairPerson | 送修人 | String |
| repairPersonContact | 送修人联系方式 | String |
| memo | 备注 | String |
| completeDate | 完工日期 | String |
| firstGatheringTime | 初次收款时间 | String |
| estimatedDeliveryTime | 预计交车时间 | String |
| deliveryTime | 交车时间 | String |
| printContent | 免责条款 | String |
| printContentJs | 免责条款江苏 | printContentJs |
| storeLogo | 门店logo | String |
| orgName | 门店名称 | String |
| orgMemo | 门店备注 | String |
| orgContacts | 联系人(维修厂) | String |
| orgContactNumber | 联系电话(维修厂) | String |
| orgDetailAddress | 联系地址(维修厂) | String |
| orgContactMobile | 联系电话(维修厂) | String |
| fax | 传真 | String |
| email | 组织邮件 | String |
| bankAccount | 开户银行 | String |
| accountNumber | 账号 | String |
| businessLicenseCode | 企业执照号 | String |
| channelName | 来店途径名称 | String |
| printOrgName | 打印抬头(需读取配置) | String |
| amountAll | 应收总计(合计金额) | Double |
| amountAllChinese | 应收总计(合计金额)中文大写 | String |
| disCountAll | 总优惠合计(附表:项目优惠+材料优惠+收银优惠) | Double |
| disCountAllBak | 项目优惠+材料优惠+收银优惠,等同于disCountAll | Double |
| amountReal | 实收金额 | Double |
| chineseAmount | 实收金额(中文大写) | Double |
| oweAmount | 未收金额 | Double |
| vipExpense | 套餐卡消费金额(附表为0 | Double |
| vipExpenseFavourable | 套餐卡优惠金额(附表为0 | Double |
| czkExpense | 储值卡消费金额(附表为0 | Double |
| czkExpenseFavourable | 储值卡优惠金额(附表为0 | Double |
| remainAmount | 结算金额tsf<br/>附表:应收-项目优惠-材料优惠 | Double |
| receivedAmount | 收款金额tsf<br/>附表:等同于已收金额 | Double |
| serviceList | 项目集合 | List |
| └─ sortNumber | 序号 | String |
| └─ orderNumber | 序号(全部) | String |
| └─ name | 名称 | String |
| └─ serviceName | 项目名称 | String |
| └─ labelName | 业务分类名称 | String |
| └─ price | 工时单价 | Double |
| └─ workHour | 工时 | Double |
| └─ subtotal | 金额 | Double |
| └─ singleFavourable | 优惠金额 | Double |
| └─ discountedSubtotal | 折后金额 | Double |
| └─ serviceMemo | 附加信息备注 | String |
| └─ discount | 折扣 | Double |
| └─ empNameStr | 服务项目明细对应修理工名称组装字符串 | String |
| └─ infiniteFlag | 是否无限,0:否,1:是 | Integer |
| └─ customCode | 自定义编码 | String |
| totalWorkHour | 项目工时合计 | Double |
| totalWorkHourVip | VIP项目工时合计(附表为0 | Double |
| serviceSubtotalAll | 工时费小计 | Double |
| serviceSubtotalVip | 服务项目明细小计(会员项目,附表为0) | Double |
| serviceNum | 维修项目小计 | String |
| serviceFavourable | 服务项目(非会员项目)客户等级优惠(附表为0) | Double |
| serviceDiscountFavourable | 项目优惠 | Double |
| serviceFavourableTotal | 项目优惠金额合计 | Double |
| serviceFavourableCommonTotal | 普通项目优惠金额合计 | Double |
| serviceDisCountSubTotal | 项目折后金额合计 | Double |
| partList | 工单对应配件材料集合 | List |
| └─ sortNumber | 序号 | String |
| └─ orderNumber | 序号(全部) | String |
| └─ name | 名称 | String |
| └─ partName | 材料名称 | String |
| └─ partShowName | 材料名称(全) | String |
| └─ partBrand | 配件品牌 | String |
| └─ standard | 配件名称规格型号品牌 | String |
| └─ spec | 规格型号 | String |
| └─ supplierCode | 供应商编码 | String |
| └─ unit | 单位 | String |
| └─ number | 数量 | Double |
| └─ price | 价格(单价) | Double |
| └─ subtotal | 金额(材料金额) | Double |
| └─ discount | 折扣 | Double |
| └─ singleFavourable | 优惠金额 | Double |
| └─ discountedSubtotal | 折后金额 | Double |
| └─ partMemo | 备注 | String |
| └─ customCode | 自定义编码 | String |
| └─ employeeName | 员工名称 | String |
| └─ outStockEmployeeName | 领料人 | String |
| └─ empNameStr | 明细对应修理工名称组装字符串 | String |
| └─ labelName | 业务分类名称 | String |
| └─ idPart | 配件材料pk | BigInteger |
| └─ idInfo | 本地材料id(长码) | String |
| └─ applyModel | 适用车型 | String |
| stuffNum | 材料数目合计 | String |
| totalStuffNum | 材料数量合计 | String |
| totalStuffNumVip | Vip材料数量合计(附表为0 | String |
| stuffSubtotalAll | 材料费小计 | Double |
| stuffSubtotalVip | 材料收入小计(会员项目,附表为0) | Double |
| partinfoFavourable | 材料项目(非会员项目)客户等级优惠(附表为0) | Double |
| partinfoDiscountFavourable | 材料折扣优惠 | Double |
| partFavourableTotal | 材料优惠金额合计 | Double |
| partFavourableCommonTotal | 普通材料优惠金额合计 | Double |
| stuffDisCountTotal | 材料折后金额合计 | Double |
| pointFavourable | 积分优惠(附表为0 | Double |
| packageFavourable | 套餐优惠(附表为0 | Double |
| discountFavourable | 结清优惠(附表为0 | Double |
| gatheringFavourable | 收银优惠 | Double |
| couponFavourable | 优惠券优惠(附表为0 | Double |
| czkDiscountFavourable | 储值卡折扣优惠(附表为0 | Double |
| customerLevelFavourable | 客户级别优惠金额(附表为0 | Double |
| extraChargeList | 附加费用集合 | List |
| └─ sortNumber | 序号 | String |
| └─ extraName | 附加费名称 | String |
| └─ subtotal | 金额 | Double |
| └─ memo | 备注 | String |
| extraCostTotal | 附加费小计 | Double |
| extraNumber | 附加费数量小计 | String |
| managementCost | 管理费 | Double |
| extraPrintVo | 附加项目 | ExtraPrintAttribute |
| └─ commissionCustomName | 代办费自定义名称 | String |
| └─ commissionCost | 代办费金额 | Double |
| └─ commissionMemo | 代办费备注 | String |
| └─ diagnosisCustomName | 诊断费自定义名称 | String |
| └─ diagnosisCost | 诊断费金额 | Double |
| └─ diagnosisItemName | 诊断详细名称 | String |
| └─ diagnosisMemo | 诊断费备注 | String |
| └─ checkCustomName | 检查费自定义名称 | String |
| └─ checkItemName | 诊断详细名称 | String |
| └─ checkCost | 检查费金额 | Double |
| └─ checkMemo | 检查费备注 | String |
| └─ processCustomName | 加工费自定义名称 | String |
| └─ processCost | 加工费金额 | Double |
| └─ processMemo | 加工费备注 | String |
| └─ processItemName | 加工详细名称 | String |
| └─ managementCustomName | 管理费自定义名称 | String |
| └─ managementCost | 管理费金额 | Double |
| └─ managementMemo | 管理费备注 | String |
| └─ fuelName | 加油费 | String |
| └─ fuelAmount | 加油费金额 | Double |
| └─ trailName | 拖车费 | String |
| └─ trailAmount | 拖车费金额 | Double |
| allOtherCost | 附加费合计应收 | Double |
| receiptAmount | 收据金额(附表:应收-项目优惠-材料优惠-收银优惠) | Double |
| receiptAmountChinese | 收据金额中文大写 | String |
| payItemList | 付款方式集合 | List |
| └─ payWay | 付款方式 | String |
| └─ payAmount | 付款金额 | Double |
| └─ chinesePayAmount | 付款金额中文大写 | String |
| payItemTogether | 付款方式拼接 | String |
| payItemTogetherChinese | 付款方式总额中文大写 | String |
| paymentTypeDetails | 支付方式汇总 | String |
| settleOweAmout | 结算单中用的待付金额(未收) | Double |
| settleOweAmoutChinese | 结算单中用的待付金额大写(未收) | String |
| settleReceivedAmout | 结算单中用的实付金额(实收) | Double |
| settleReceivedAmoutChinese | 结算单中用的实付金额大写(未收) | String |
| realPayAmountChinese | 客户实付大写(应收-所有优惠) | String |
| naInsurer | 理赔公司名称 | String |
| insurancepolicyNo | 理赔单理赔保险单号 | String |
| insuranceCompany | 保险公司名称 | String |
| contactName | 联系人姓名 | String |
| contactCellphone | 联系人电话 | String |
| insuranceNo | 商业险单号 | String |
| insuranceNoTCI | 交强险单号 | String |
| insuranceExpiryDate | 商业险到期日 | String |
| insuranceExpiryDateTCI | 交强险到期日 | String |
| printEmployeeName | 打印人姓名 | String |
| printTime | 打印时间 | String |
| printCount | 打印次数 | String |
# 备注1
1. ++新版附表没有“状态”字段,所有模板中切勿使用 billStatus【单据状态】、balanceStatus【结算状态】来进行判断输出;采用直接取值方式填值++
1. ++没有了balanceStatus,采用收款方式列表payItemList判空的方式来验证是否有收款信息++
1. ++收银金额可写作:++$P{payItemList}.getRecordCount()==0?$P{receivedAmount}:$P{receivedAmount}.setScale( 2, BigDecimal.ROUND\_HALF\_EVEN ).toString()+"("+$P{paymentTypeDetails}+")"
2. ++待付金额可写作:$P{oweAmount}++
3. ++实付金额可写作:$P{amountReal}++
4. ++合计金额可写作:++$P{payItemList}.getRecordCount()==0?($P{settleOweAmout}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ):($P{settleReceivedAmout}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )
5. ++大写可写作:++$P{payItemList}.getRecordCount()==0?$P{settleOweAmoutChinese}:$P{chineseAmount}
2. ++新版附表没有“单据类型”字段(原附表的maintainType为"GDFB"、"LPDFB"两种附表类型),所有模板中切勿使用 maintainType【单据类型】字段作为判断条件。根据情况可以取 fromMaintainType【来源单据类型】进行判断。如:++
1. ++是否展示理赔公司、理赔单等理赔相关信息栏,老附表判断逻辑为 maintainType.equals('LPDFB')。可更换为 fromMaintainType.equals('LPD')++
# 备注2
1. 出参标注颜色为 **绿 色** 的字段,为 ++**附表本身内容**++(如车牌号、VIN码等) 或 ++**无法变更内容**++(如门店信息等)
1. 客户在附表页面直接变更信息,会直观反映在打印内容中。
2. 出参标注颜色为 **橙 色** 的字段,为 ++**通过客户ID、车辆ID、项目ID、材料ID**++ 等,++**反查**++ 基础数据获得内容,
1. 若客户通过 ++**附表页面选择组件方式**++ 修改附表信息,因ID发生变化,该部分打印内容会随之变化,变更信息将 ++**会体现在打印内容中**++。
2. 若客户通过 ++**手动填写文本内容方式**++ 修改附表信息,因ID未发生变化,该部分打印内容不会变化,变更信息将 ++**不会体现在打印内容中**++,若客户不满意结果,请客户直接将 ++**基础数据进行变更**++ 后,通过页面组件选择后再进行打印。
> 更新: 2024-01-11 12:11:34 原文: <https://xcz.yuque.com/ombipo/rpc7ms/nmoz9mzqf2q8micw>
@@ -0,0 +1,61 @@
# 材料标签接口参数
# 材料标签接口参数
打印平台模版分类:材料价格通用标签打印
材料标签支持一次打多张,与labelList集合里的元素个数相匹配:
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| labelList | 材料标签列表 | | List<Label> |
| <br/><br/> | | | |
Label:
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| name | 材料名称 | | String |
| supplierCode | 零件号 | | String |
| customCode | 材料编码 | | String |
| sellPrice | 销售价格 | | BigDecimal |
| date | 打印日期 | | String yyyy/MM/dd |
| <br/><br/> | | | |
# 采购库存材料标签打印
### 打印平台模版分类:
不带供应商信息:采购库存通用标签打印-新
带供应商信息:采购库存通用标签打印-包含供应商-新
![image](https://ddoc.f6yc.com/yuque/0/2023/png/227465/1694662514424-52b54134-ad88-4e10-b5ad-c3565633f157.png)
### 打印数据体
材料标签支持一次打多张,于labelList集合里的元素个数相匹配:
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| labelList | 材料名称 | | List<Object> |
Object:
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| partName | 材料名称 | | String |
| partBrand | 材料品牌 | | String |
| customCode | 材料编码 | | String |
| partShowName | 组合名称(材料品牌,名称,规格型号组合) | | String |
| supplierCode | 材料供应商编码 | | String |
| spec | 材料规格型号 | | String |
| unit | 材料单位 | | String |
| barCode | 材料编码 | | String |
| billDate | 单据日期 | | String yyyy-MM-dd |
| supplierName | 供应商名称 | | String |
| storageName | 材料仓库名称 | | String |
| defSeat | 材料货位 | | String |
| date | 打印的当前日期 | | String yyyy/MM/dd |
> 更新: 2023-09-14 14:27:45 原文: <https://xcz.yuque.com/ombipo/rpc7ms/mvq9tlfxeg2k6v9y>
@@ -0,0 +1,100 @@
# 检测单接口参数
# 检测单接口参数
# 参数说明
## 主单信息
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| carCheckPackageName | 模板名称 | | String |
| title | 标题 | | String |
| billNo | 检测单号 | | String |
| printTime | 打印时间 | | String |
| naEmployee | 服务顾问 | | String |
| billDate | 进厂时间 | | String |
| deliveryTime | 预计交车 | | String |
| naCustomer | 车主 | | String |
| carModel | 车型 | | String |
| cellPhone | 车主电话 | | String |
| carNoWhole | 车牌号 | | String |
| vin | vin码 | | String |
| repairPerson | 送修人 | | String |
| repairPersonContact | 送修人联系方式 | | String |
| mileage | 进厂里程 | | String |
| oilCapacity | 进厂油量 | | String |
| nextMileage | 下次保养里程 | | String |
| nextMaintainDate | 下次保养日期 | | String |
| customerMemo | 车主描述 | | String |
| merchantAddress | 联系地址 | | String |
| merchantPhone | 联系方式(手机 + 固定电话) | | String |
| qrCode | 二维码 | | String |
| qrCodeToB | 新版检测单B端二维码 | | String |
| qrCodeToC | 新版检测单C端二维码 | | String |
| icon | 车辆环视图 | | String |
| maintainBillNo | 结算单号 | | String |
| showComputerCheckInfo | 是否展示电脑检测 | | Boolean |
| computerCheckInfoList | 电脑检测 | | List<ComputerPrintItem> |
| personalCheckInfoList | 包含正常和异常人工检测项 | | List<PersonalPrintItem> |
| sortedPersonalCheckInfoList | 包含正常和异常人工检测项,问题项目排序靠前 | | List<PersonalPrintItem> |
| optionPersonalCheckInfoList | 异常人工检测项 | | List<PersonalPrintItem> |
| normalPersonalCheckInfoList | 正常人工检测项 | | List<PersonalPrintItem> |
| iconMemo | 环视图备注 | | String |
| iconResult | 环视图结论 | | String |
| warningLightResult | 警示灯结论 | | String |
| warningLightMemo | 警示灯备注 | | String |
| showWarningLightItem | 是否展示警示灯 | | Boolean |
| warningLightItemList | 警示灯 | | List<WarningLightItem> |
| warningLightBrightItemUrls | 警示灯列表字符串 | | String |
| employeeName | 服务技师 | | String |
| | | | |
## ComputerPrintItem
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| index | 序号 | String |
| errorCode | 故障码 | String |
| itemName | 检测项目 | String |
| optionNameS | 建议处理(Y | String |
| optionNameE | 择期处理(Y | String |
| optionNameU | 急需处理(Y | String |
| memo | 备注 | String |
| | | |
| | | |
| | | |
| | | |
| | | |
## PersonalPrintItem(检测小类)
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| index | 序号 | BigInteger |
| itemComponent | 检测部件(小类名称) | String |
| memo | 备注 | String |
| childList | 子项目列表 | List<PrintTinyItem> |
## PrintTinyItem(检测项目)
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| itemName | 检测项目 | String |
| itemResults | 检测结果 | String |
| optionNameS | 建议处理 Y | String |
| optionNameE | 择期处理 Y | String |
| optionNameU | 急需处理 Y | String |
| memo | 备注 | String |
## ComputerPrintItem
| 字段 | 含义 | 类型 |
| --- | --- | --- |
| warningIconCode | 警示灯 | String |
| | | |
| | | |
| | | |
| | | |
> 更新: 2025-02-25 13:55:59 原文: <https://xcz.yuque.com/ombipo/rpc7ms/zayna4s335straki>
@@ -0,0 +1,842 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="carWash" pageWidth="136" pageHeight="842" columnWidth="136" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="10" uuid="8fdb09f3-da43-46f9-a6cb-2b26a2247961">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<property name="com.jaspersoft.studio.unit." value="mm"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<style name="Table_TH" mode="Opaque" backcolor="#FFFFFF">
<box>
<pen lineWidth="0.5" lineColor="#000000"/>
<topPen lineWidth="0.5" lineColor="#000000"/>
<leftPen lineWidth="0.5" lineColor="#000000"/>
<bottomPen lineWidth="0.5" lineColor="#000000"/>
<rightPen lineWidth="0.5" lineColor="#000000"/>
</box>
</style>
<style name="Table_CH" mode="Opaque" backcolor="#FFFFFF">
<box>
<pen lineWidth="0.5" lineColor="#000000"/>
<topPen lineWidth="0.5" lineColor="#000000"/>
<leftPen lineWidth="0.5" lineColor="#000000"/>
<bottomPen lineWidth="0.5" lineColor="#000000"/>
<rightPen lineWidth="0.5" lineColor="#000000"/>
</box>
</style>
<style name="Table_TD" mode="Opaque" backcolor="#FFFFFF">
<box>
<pen lineWidth="0.5" lineColor="#000000"/>
<topPen lineWidth="0.5" lineColor="#000000"/>
<leftPen lineWidth="0.5" lineColor="#000000"/>
<bottomPen lineWidth="0.5" lineColor="#000000"/>
<rightPen lineWidth="0.5" lineColor="#000000"/>
</box>
</style>
<subDataset name="Dataset1" uuid="22e86b94-acb8-45ed-960f-04558f91ad82">
<queryString>
<![CDATA[]]>
</queryString>
<field name="serviceName" class="java.lang.String"/>
<field name="workHour" class="java.math.BigDecimal"/>
<field name="leftCount" class="java.math.BigDecimal"/>
<field name="unusedNumber" class="java.lang.Integer"/>
<field name="number" class="java.lang.Integer"/>
<field name="subtotal" class="java.math.BigDecimal"/>
<field name="empNameStr" class="java.lang.String"/>
</subDataset>
<subDataset name="Dataset1sub" uuid="22e86b94-acb8-45ed-960f-04558f91ad82">
<queryString>
<![CDATA[]]>
</queryString>
<field name="serviceName" class="java.lang.String"/>
<field name="workHour" class="java.math.BigDecimal"/>
<field name="leftCount" class="java.math.BigDecimal"/>
<field name="unusedNumber" class="java.lang.Integer"/>
<field name="number" class="java.lang.Integer"/>
<field name="subtotal" class="java.math.BigDecimal"/>
</subDataset>
<subDataset name="Dataset2" uuid="b9fe50e4-5070-473e-9238-1aa624bd7ae5">
<queryString>
<![CDATA[]]>
</queryString>
<field name="customCode" class="java.lang.String"/>
<field name="partName" class="java.lang.String"/>
<field name="unit" class="java.lang.String"/>
<field name="number" class="java.math.BigDecimal"/>
<field name="price" class="java.math.BigDecimal"/>
<field name="subtotal" class="java.math.BigDecimal"/>
<field name="partBrand" class="java.lang.String"/>
<field name="standard" class="java.lang.String"/>
<field name="supplierCode" class="java.lang.String"/>
<field name="discountedSubtotal" class="java.math.BigDecimal"/>
<field name="partMemo" class="java.lang.String"/>
<field name="isBring" class="java.lang.String"/>
<field name="singleFavourable" class="java.math.BigDecimal"/>
<field name="isMember" class="java.lang.Integer"/>
<field name="discount" class="java.math.BigDecimal"/>
<field name="spec" class="java.lang.String"/>
<sortField name="isMember" order="Descending"/>
</subDataset>
<subDataset name="DatasetPay" uuid="6f96d8c9-fa56-4586-9b6d-846e9a678fc3">
<queryString>
<![CDATA[]]>
</queryString>
<field name="payWay" class="java.lang.String"/>
<field name="payAmount" class="java.math.BigDecimal"/>
<field name="chinesePayAmount" class="java.lang.String"/>
</subDataset>
<subDataset name="DatasetExtra" uuid="6d2afb78-c6ff-45a0-90a2-6e65d25da398">
<queryString>
<![CDATA[]]>
</queryString>
<field name="extraName" class="java.lang.String"/>
<field name="subtotal" class="java.math.BigDecimal"/>
<field name="memo" class="java.lang.String"/>
</subDataset>
<subDataset name="Dataset_sub" uuid="6a79ccac-0c3d-4ada-a547-6d9f5ef3a7c9">
<queryString>
<![CDATA[]]>
</queryString>
<field name="serviceName" class="java.lang.String"/>
<field name="workHour" class="java.math.BigDecimal"/>
<field name="leftCount" class="java.math.BigDecimal"/>
<field name="unusedNumber" class="java.lang.Integer"/>
<field name="number" class="java.lang.Integer"/>
<field name="subtotal" class="java.math.BigDecimal"/>
</subDataset>
<subDataset name="CopyOfDataset_1" uuid="fad36331-4b02-4d0a-99c9-d3608c7abffb">
<queryString>
<![CDATA[]]>
</queryString>
<field name="serviceName" class="java.lang.String"/>
<field name="workHour" class="java.math.BigDecimal"/>
<field name="leftCount" class="java.math.BigDecimal"/>
<field name="unusedNumber" class="java.lang.Integer"/>
<field name="number" class="java.lang.Integer"/>
<field name="subtotal" class="java.math.BigDecimal"/>
</subDataset>
<parameter name="orgName" class="java.lang.String"/>
<parameter name="detailAddress" class="java.lang.String"/>
<parameter name="contactNumber" class="java.lang.String"/>
<parameter name="fax" class="java.lang.String"/>
<parameter name="bankAccount" class="java.lang.String"/>
<parameter name="email" class="java.lang.String"/>
<parameter name="accountNumber" class="java.lang.String"/>
<parameter name="naCustomer" class="java.lang.String"/>
<parameter name="contactName" class="java.lang.String"/>
<parameter name="contactCellphone" class="java.lang.String"/>
<parameter name="billNo" class="java.lang.String"/>
<parameter name="billDate" class="java.lang.String"/>
<parameter name="mileage" class="java.math.BigDecimal"/>
<parameter name="carNoWhole" class="java.lang.String"/>
<parameter name="carModelShort" class="java.lang.String"/>
<parameter name="deliveryTime" class="java.lang.String"/>
<parameter name="serviceSubtotalAll" class="java.math.BigDecimal"/>
<parameter name="stuffSubtotalAll" class="java.math.BigDecimal"/>
<parameter name="extraCostTotal" class="java.math.BigDecimal"/>
<parameter name="amountAll" class="java.math.BigDecimal"/>
<parameter name="totalWorkHour" class="java.math.BigDecimal"/>
<parameter name="numberCount" class="java.lang.String"/>
<parameter name="serviceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="partList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="businessTypeName" class="java.lang.String"/>
<parameter name="creatorName" class="java.lang.String"/>
<parameter name="carModel" class="java.lang.String"/>
<parameter name="vin" class="java.lang.String"/>
<parameter name="printCount" class="java.lang.String"/>
<parameter name="serviceNum" class="java.lang.String"/>
<parameter name="serviceDisCountSubTotal" class="java.math.BigDecimal"/>
<parameter name="totalStuffNum" class="java.lang.String"/>
<parameter name="couponFavourable" class="java.math.BigDecimal"/>
<parameter name="pointFavourable" class="java.math.BigDecimal"/>
<parameter name="partinfoDiscountFavourable" class="java.math.BigDecimal"/>
<parameter name="naEmployee" class="java.lang.String"/>
<parameter name="memo" class="java.lang.String"/>
<parameter name="repairPerson" class="java.lang.String"/>
<parameter name="cellPhone" class="java.lang.String"/>
<parameter name="carBrandName" class="java.lang.String"/>
<parameter name="orgContactMobile" class="java.lang.String"/>
<parameter name="orgDetailAddress" class="java.lang.String"/>
<parameter name="selfPartList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="payItemList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="extraChargeList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="stuffDisCountTotal" class="java.math.BigDecimal"/>
<parameter name="selfTotalStuffNum" class="java.lang.String"/>
<parameter name="czkExpense" class="java.math.BigDecimal"/>
<parameter name="disCountAll" class="java.math.BigDecimal"/>
<parameter name="packageFavourable" class="java.math.BigDecimal"/>
<parameter name="serviceFavourable" class="java.math.BigDecimal"/>
<parameter name="partinfoFavourable" class="java.math.BigDecimal"/>
<parameter name="gatheringFavourable" class="java.math.BigDecimal"/>
<parameter name="discountFavourable" class="java.math.BigDecimal"/>
<parameter name="oweAmount" class="java.math.BigDecimal"/>
<parameter name="amountReal" class="java.math.BigDecimal"/>
<parameter name="printTime" class="java.lang.String"/>
<parameter name="creationtime" class="java.lang.String"/>
<parameter name="extraNumber" class="java.lang.String"/>
<parameter name="carSeriesName" class="java.lang.String"/>
<parameter name="qRCodeStr" class="java.lang.String"/>
<parameter name="vipExpense" class="java.math.BigDecimal"/>
<parameter name="storeLogo" class="java.lang.String"/>
<parameter name="partFavourableTotal" class="java.math.BigDecimal"/>
<parameter name="serviceFavourableTotal" class="java.math.BigDecimal"/>
<parameter name="partFavourableCommonTotal" class="java.math.BigDecimal"/>
<parameter name="serviceFavourableCommonTotal" class="java.math.BigDecimal"/>
<parameter name="serviceSubtotalVip" class="java.math.BigDecimal"/>
<parameter name="stuffSubtotalVip" class="java.math.BigDecimal"/>
<parameter name="nextMileage" class="java.math.BigDecimal"/>
<parameter name="printContent" class="java.lang.String"/>
<parameter name="repairPersonContact" class="java.lang.String"/>
<parameter name="payItemTogether" class="java.lang.String"/>
<parameter name="payItemTogetherChinese" class="java.lang.String"/>
<parameter name="printOrgName" class="java.lang.String"/>
<parameter name="orgContactNumber" class="java.lang.String"/>
<parameter name="balanceStatus" class="java.lang.String"/>
<parameter name="receiptAmount" class="java.math.BigDecimal"/>
<parameter name="stubPrintFlag" class="java.lang.String"/>
<parameter name="stubServiceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource">
<parameterDescription><![CDATA[]]></parameterDescription>
</parameter>
<parameter name="czkDetailInfo" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<variable name="tempServiceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource">
<variableExpression><![CDATA[$P{serviceList}]]></variableExpression>
</variable>
<variable name="tempServiceList2" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource">
<variableExpression><![CDATA[$P{serviceList}]]></variableExpression>
</variable>
<detail>
<band height="43">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="1" width="136" height="40" uuid="749214fe-d497-4af5-809c-60db5b97deee">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="11"/>
</textElement>
<textFieldExpression><![CDATA[($P{printOrgName}==null?$P{orgName}:($P{printOrgName}.isEmpty()?$P{orgName}:$P{printOrgName}))+"洗车单"]]></textFieldExpression>
</textField>
<line>
<reportElement positionType="Float" x="0" y="42" width="136" height="1" uuid="9902b948-f3bc-4318-9162-169d3a18d40e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="0" width="40" height="11" uuid="a9ea9a61-8b35-4714-abd6-c1b3f01daf4c">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[打印时间:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="32" y="0" width="104" height="11" uuid="3e42cdc1-f715-455d-965b-a3862546efe3">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{printTime}.substring(0,16)]]></textFieldExpression>
</textField>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="0" width="40" height="11" uuid="d8179c10-e152-47cf-98ed-6d25100423b1">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[洗车单号:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="32" y="0" width="104" height="11" uuid="73392fa1-f678-4f97-b702-97d591431a76">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
</textField>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="32" y="0" width="104" height="11" uuid="4b8e8259-abcb-4b55-8932-645ab3c168db">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{naEmployee}]]></textFieldExpression>
</textField>
<staticText>
<reportElement key="" x="0" y="0" width="40" height="11" uuid="aa005a7f-11f1-4362-94dd-7d4b737f85fb">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[服务顾问:]]></text>
</staticText>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="24" y="0" width="112" height="11" uuid="4c9e2e05-12f0-4c10-a4a7-cfac0dedc4c7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{carNoWhole}]]></textFieldExpression>
</textField>
<staticText>
<reportElement key="" x="0" y="0" width="40" height="11" uuid="506d2cfc-d948-4a43-971c-e2ae1f4f4a6e">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[车牌号:]]></text>
</staticText>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="18" y="0" width="118" height="11" uuid="7c4f4788-dd00-484c-8762-c907f94aba06">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{naCustomer}.substring(0, 1) + "(*^_^*)" + $P{naCustomer}.substring($P{naCustomer}.length() > 10 ? 10 : $P{naCustomer}.length(), $P{naCustomer}.length())]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="29" height="11" uuid="1e0990e3-d082-4c2a-b861-ea88ae864819">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户:]]></text>
</staticText>
</band>
<band height="12">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="32" y="0" width="104" height="11" uuid="aa23dce0-7f6f-4324-8368-9040c5460fce">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{cellPhone}.length() <= 3 ? $P{cellPhone}: $P{cellPhone}.substring(0, 3) + "(*^_^*)" + $P{cellPhone}.substring($P{cellPhone}.length() > 7 ? 7 : $P{cellPhone}.length(), $P{cellPhone}.length())]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="40" height="11" uuid="17c41cd7-40bc-417a-b81f-31cb88c34931">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户电话:]]></text>
</staticText>
<line>
<reportElement positionType="Float" x="0" y="11" width="136" height="1" uuid="83b4e0cf-0321-463b-93ab-cf578aecc422">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="17">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{serviceList}!=null]]></printWhenExpression>
<staticText>
<reportElement positionType="Float" x="0" y="0" width="46" height="16" uuid="3b60f428-f1be-46b4-8844-000b66d37cb3">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[项目]]></text>
</staticText>
<staticText>
<reportElement positionType="Float" x="46" y="0" width="38" height="16" uuid="3dce837c-7930-438f-88cf-d0ca69283abf">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[工时费(元)]]></text>
</staticText>
<staticText>
<reportElement positionType="Float" x="84" y="0" width="52" height="16" uuid="4f6f1957-c5d3-45d4-9f9e-59f03d002151">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[卡剩余次数]]></text>
</staticText>
<line>
<reportElement positionType="Float" x="0" y="16" width="136" height="1" uuid="45924492-61d2-4ec2-9dae-9edf6181e7a4">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{serviceList}!=null]]></printWhenExpression>
<componentElement>
<reportElement isPrintRepeatedValues="false" x="2" y="0" width="136" height="11" uuid="ecd9cd2e-ad62-4a61-a8a7-6f894e8b881b">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table 1_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table 1_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table 1_TD"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd">
<datasetRun subDataset="Dataset1" uuid="12ce7a05-4dbc-42aa-ab97-ca225ebd48e1">
<dataSourceExpression><![CDATA[$P{serviceList}]]></dataSourceExpression>
</datasetRun>
<jr:column width="46" uuid="4a339d5c-d93e-4d9b-a7f5-478cb3bc4c66">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="11">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement key="" x="0" y="0" width="46" height="11" uuid="d1b9c27f-da5c-43d5-b245-b2ce10285445">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
</textElement>
<textFieldExpression><![CDATA[$F{serviceName}.indexOf("卡消费") > 0 ? $F{serviceName}.replaceAll("卡消费",$F{empNameStr}): $F{serviceName}+"("+$F{empNameStr}+")"]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="38" uuid="36d33a43-8dc0-421c-89b2-6c17b4c992c2">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<jr:detailCell height="11">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement key="" x="0" y="0" width="38" height="11" uuid="b0a21e7b-9ef2-453a-8b0b-f81e4a8d02ae">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
</textElement>
<textFieldExpression><![CDATA[$F{subtotal}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="52" uuid="922b68cd-f23e-4d67-9717-74621fa31ed2">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<jr:detailCell height="11">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement key="" x="0" y="0" width="52" height="11" uuid="683ff861-1457-4841-8904-a5b8030d6a63">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
</textElement>
<textFieldExpression><![CDATA[($F{unusedNumber}==null?"":$F{unusedNumber}+"/")+($F{number}==null?"":$F{number})]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="20">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="8" width="40" height="11" uuid="26dc81d8-29cc-4024-b7b4-d25a9ed72773">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[合计金额:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="37" y="8" width="99" height="11" uuid="2872a1d3-dafc-430d-9cfd-fef01cdcc46a">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{amountAll}.subtract($P{vipExpense}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="33" y="9" width="7" height="11" uuid="f8d751a0-c2f4-45c9-bd4d-168f7250ab40">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="7"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<line>
<reportElement positionType="Float" x="0" y="5" width="136" height="1" uuid="aa43bd00-5440-4c07-a3d5-bbbf892646bb">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="37" y="-1" width="99" height="11" uuid="d4b00569-8830-4a81-98b9-b02516e9e433">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="33" y="0" width="7" height="11" uuid="7d3b3e0c-8646-455a-ac17-10c3e46db01f">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="7"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<staticText>
<reportElement x="0" y="-1" width="40" height="11" uuid="0eb1e983-dbb7-4e21-b8ee-db6f492cdfab">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[优惠金额:]]></text>
</staticText>
</band>
<band height="22">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="37" y="10" width="99" height="11" uuid="00e5ff0d-525e-4d27-ab0d-5a988300256b">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN )):($P{oweAmount}.setScale( 2, BigDecimal.ROUND_HALF_EVEN ))]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="10" width="40" height="11" uuid="e5619fe5-5014-44c7-be70-428e7fa89a99">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[待付金额:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="-1" width="40" height="11" uuid="b3f329b3-6432-412c-ac54-3c3f1fdbe034">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[实付金额:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="37" y="-1" width="99" height="11" uuid="8bbae461-eac5-4e83-9f39-4a27a55bed68">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?"0.00":($P{amountAll}.subtract($P{vipExpense}).subtract($P{oweAmount}).subtract($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).subtract($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).subtract($P{czkExpense}).
subtract($P{serviceFavourable}).subtract($P{partinfoFavourable}).subtract($P{discountFavourable}).subtract($P{couponFavourable}).subtract($P{pointFavourable}).subtract($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ))]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="33" y="0" width="7" height="11" uuid="d257edd4-940d-4804-a256-5e23b7fc781e">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="7"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<staticText>
<reportElement x="33" y="11" width="7" height="11" uuid="162688e4-1a42-4f5c-b406-e5c7c780a696">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="7"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
</band>
<band height="11">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
<staticText>
<reportElement x="40" y="-1" width="7" height="11" uuid="389acfa8-bc97-47ca-b7de-61e45f6392f8">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="7"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<staticText>
<reportElement x="0" y="0" width="50" height="11" uuid="ef352217-3888-4dee-9291-2e1814474afa">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement verticalAlignment="Top">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[储值卡消费:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement positionType="Float" x="43" y="0" width="37" height="11" uuid="9ed79fa8-64a1-401d-b20e-2bd537aa60ce">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement verticalAlignment="Top">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{czkExpense}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement positionType="Float" x="80" y="0" width="56" height="11" uuid="9ed79fa8-64a1-401d-b20e-2bd537aa60ce">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement verticalAlignment="Top">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA["("+($P{czkDetailInfo}==null?"":$P{czkDetailInfo}) + ")"]]></textFieldExpression>
</textField>
</band>
<band height="15">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="0" width="40" height="11" uuid="107cd24a-9c65-454a-857b-d4343360c17c">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收银金额:]]></text>
</staticText>
<staticText>
<reportElement x="33" y="1" width="7" height="11" uuid="d3e37020-1703-4943-88cc-f6a27b30051d">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="7"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement positionType="Float" x="37" y="2" width="99" height="11" uuid="8cac6572-8613-4439-ac11-8a09c05356a6">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?"0.00":($P{amountAll}.subtract($P{vipExpense}).subtract($P{oweAmount}).subtract($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).subtract($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).subtract($P{czkExpense}).
subtract($P{serviceFavourable}).subtract($P{partinfoFavourable}).subtract($P{discountFavourable}).subtract($P{couponFavourable}).subtract($P{pointFavourable}).subtract($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).toString()+"("+$P{payItemTogether}+")")]]></textFieldExpression>
</textField>
</band>
<band height="26">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="2" width="29" height="11" uuid="fc491228-a3a9-47ab-b354-af9f4c4e0f3e">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Top">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[备注:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement positionType="Float" x="20" y="2" width="116" height="11" uuid="b7b78327-0448-424d-970e-bada89ec5d60">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Top">
<font fontName="黑体" size="7"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
</textField>
<line>
<reportElement x="1" y="0" width="136" height="1" uuid="1d0bcb53-619a-484c-a904-4e88f609d733">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,433 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3fa22123-efc4-4f3f-a186-6a8f692d17e6">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<subDataset name="List1" uuid="7366c5be-288c-41c7-b295-b8d023ec81ae">
<queryString>
<![CDATA[]]>
</queryString>
<field name="index" class="java.lang.String"/>
<field name="serviceName" class="java.lang.String"/>
<field name="cooperationServiceName" class="java.lang.String"/>
<field name="cooperationOrgName" class="java.lang.String"/>
<field name="auditStatus" class="java.lang.String"/>
<field name="cooperationCost" class="java.math.BigDecimal"/>
</subDataset>
<parameter name="billNo" class="java.lang.String"/>
<parameter name="tradingOrgName" class="java.lang.String"/>
<parameter name="tradingDate" class="java.lang.String"/>
<parameter name="customerName" class="java.lang.String"/>
<parameter name="carNo" class="java.lang.String"/>
<parameter name="employeeName" class="java.lang.String"/>
<parameter name="cardName" class="java.lang.String"/>
<parameter name="memberCardNo" class="java.lang.String"/>
<parameter name="cardEndDate" class="java.lang.String"/>
<parameter name="tradingAmount" class="java.lang.String"/>
<parameter name="amount" class="java.lang.String"/>
<parameter name="remark" class="java.lang.String"/>
<parameter name="orgOfPrinting" class="java.lang.String"/>
<parameter name="dateOfPrinting" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="103" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="10" width="555" height="40" uuid="7c3a0deb-dcb0-406e-ba9c-9f279e1518b0">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="16" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA["卡交易单"]]></textFieldExpression>
</textField>
<staticText>
<reportElement key="" x="387" y="58" width="50" height="18" isRemoveLineWhenBlank="true" uuid="86735eee-0435-4238-9b93-c08d86ed318f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[交易日期:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="58" width="50" height="18" uuid="fef0343d-31f4-4b1c-a521-1a1b736aa485">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[单号:]]></text>
</staticText>
<staticText>
<reportElement x="200" y="58" width="50" height="18" uuid="627d90fe-c235-45d8-9f4a-92f8d7ec0de7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[交易门店:]]></text>
</staticText>
<staticText>
<reportElement x="387" y="80" width="50" height="18" uuid="7af4c3ab-ae72-4f6b-8b40-7b3129bd7eff">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[服务顾问:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="80" width="50" height="18" uuid="74602da5-9195-47dd-9416-9cb229aeccad">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[客户姓名:]]></text>
</staticText>
<staticText>
<reportElement x="200" y="80" width="50" height="18" uuid="65483940-e0c7-4713-9e9a-6a5a7294d248">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[用卡车辆:]]></text>
</staticText>
<line>
<reportElement x="0" y="78" width="555" height="1" uuid="f6716829-054d-4fed-a12c-2f4d227724c7">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<line>
<reportElement x="0" y="100" width="555" height="1" uuid="9c9a29fc-1a65-4869-8d03-3242ff1fe667">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="80" width="140" height="18" uuid="01160695-dd7d-4038-8476-f20a7f997e62"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="58" width="140" height="18" uuid="d8bc8e14-2a41-4471-8be9-dae7dee48c2b"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="250" y="58" width="130" height="18" uuid="f258bb2c-70ce-4b49-a305-a4c0db4671b2"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{tradingOrgName}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="250" y="80" width="130" height="18" uuid="8ffee383-6250-4697-8381-b3017b0d67ad"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{carNo}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="437" y="58" width="118" height="18" uuid="7daab9f8-bc40-45fc-90b2-510a19fe0578"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{tradingDate}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="437" y="80" width="118" height="18" uuid="5cd6ce11-c901-42cb-b84c-fc7b3a0de3b3"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{employeeName}]]></textFieldExpression>
</textField>
</band>
<band height="78">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="51" width="50" height="18" uuid="2cf35160-a982-4dcc-881b-c53adcf3761a">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[剩余金额:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="2" width="50" height="18" uuid="6b4cf2c5-cb81-43b6-82c9-ee238923a378">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[卡名称:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="25" width="50" height="18" uuid="782a08a7-c605-4a2f-83b7-4e3ad8ee3c4c">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[消费金额:]]></text>
</staticText>
<line>
<reportElement x="0" y="47" width="555" height="1" uuid="8699bef5-cebd-4ebc-bb34-3697568375f7">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="51" width="140" height="18" uuid="4ff3b629-b9e5-49c5-ad89-ce856a556839">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{amount}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="71" width="555" height="1" uuid="98b923f9-1e14-4cc5-b508-12141cefb96e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="25" width="140" height="18" uuid="7ce3fef6-73c6-49e9-a676-64e402603d93"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{tradingAmount}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="200" y="2" width="50" height="18" uuid="04029be9-7783-4583-9f54-0b15047b2e76">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[卡号:]]></text>
</staticText>
<staticText>
<reportElement x="388" y="2" width="50" height="18" uuid="471ece50-ac4b-4d63-ba15-e82448944610">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[卡有效期:]]></text>
</staticText>
<line>
<reportElement x="0" y="22" width="555" height="1" uuid="7672b0ab-ea26-468c-a2f5-7188f85f733c">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="2" width="140" height="18" uuid="88cb0ab6-fc29-4d51-b688-0ab2bdb0b2f9"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{cardName}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="250" y="2" width="130" height="18" uuid="90dfc3ce-5cf5-42d0-8e6f-670b49922088"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{memberCardNo}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="438" y="2" width="117" height="18" uuid="48056694-c19b-4dc3-a0d7-ef3e32ea4cb4"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{cardEndDate}]]></textFieldExpression>
</textField>
</band>
<band height="92">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="17" width="50" height="18" uuid="fc27cb3d-0e5d-4143-b83d-daf432fa56c8">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[打印门店:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="17" width="140" height="18" uuid="1a73fa76-4113-4dcd-803c-da26e934f441"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgOfPrinting}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="200" y="17" width="50" height="18" uuid="786b4cf3-7636-455c-bf05-115ff8ca71b7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Right" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[打印日期:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="250" y="17" width="130" height="18" uuid="61b50ddd-d880-403c-855f-3ae2f0403592"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{dateOfPrinting}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="-4" width="504" height="18" uuid="d9928920-600f-4030-976c-bdf598467704"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{remark}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="-4" width="50" height="18" uuid="03491a27-e480-40c3-aa80-d95b2bed3d95">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[交易备注:]]></text>
</staticText>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,215 @@
# 结算单(指定内容)接口文档
# 结算单(指定内容)接口文档
# 接口
**接口: /blazer/maintenance/pr****int/file/dispatchCondition**
**方法: post**
**支持场景:打印 维保单/洗车单/维修单/贴膜单/理赔单/零售单,支持指定(项目/材料/附加费)打印**
**支持打印模块(**PrintModuleEnum**):****工时费、材料费、附加费、其他费用、服务费用**
**入参:**
```plaintext
{
"rowId": "10329",
"rowCode": "partialSettlementStatement",
"pkId": "16126085167868551253",
"serviceList": [
{
"pkId": 16126085168250232896,
"module": 1
},
{
"pkId": 16155904656688554043,
"module": 4
}
],
"partList": [
{
"pkId": 11004336,
"module": 2
},
{
"pkId": 11004811,
"module": 5
}
],
"extraList": [
{
"type": 4,
"module": 3
},
{
"type": 6,
"module": 4
}
]
}
```
**出参:**
```plaintext
{
"preScanUrl": "https://f.f6yc.com/printserver/pdfprint.html?url=https://f.f6yc.com/print-server/test/2024-10/default/49be06dc15444e2793a21fc8c16d3b5c.pdf?Expires=1729152323&OSSAccessKeyId=LTAI4Fcf2C1U99o3e3UQ2bHV&Signature=L86HWRF2ilyXU4BbzKhwXad%2BwXg%3D",
"url": "https://f.f6yc.com/print-server/test/2024-10/default/49be06dc15444e2793a21fc8c16d3b5c.pdf?Expires=1729152323&OSSAccessKeyId=LTAI4Fcf2C1U99o3e3UQ2bHV&Signature=L86HWRF2ilyXU4BbzKhwXad%2BwXg%3D"
}
```
# jasper取参说明
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| billNo | 工单号 | 是 | String |
| maintainType | 工单类型 | 是 | String |
| businessTypeName | 业务类别 | 是 | String |
| balanceStatus | 结算状态 | 是 | String |
| billStatus | 单据状态 | 是 | String |
| creatorName | 创建人名称 | 是 | String |
| naEmployee | 服务顾问 | 否 | String |
| employeePhone | 服务顾问手机号 | 否 | String |
| naInsurer | 理赔公司名称 | 否 | String |
| insurancepolicyNo | 理赔保险单号 | 否 | Double |
| serviceSubtotalVip | 套餐卡项目工时费小计 | 是 | Double |
| serviceFavourableCommonTotal | 普通项目优惠金额合计 | 是 | Double |
| totalStuffNum | 材料数量合计 | 是 | String |
| stuffSubtotalVip | 套餐卡项目材料费小计 | 是 | Double |
| stuffSubtotalAll | 材料费小计 | 是 | Double |
| partFavourableCommonTotal | 普通材料优惠金额合计 | 是 | Double |
| extraNumber | 附加费数量小计 | 是 | String |
| extraCostTotal | 附加费小计 | 是 | Double |
| favourableTotalList | 优惠明细小计 | 否 | List<FavourableDetailPrintAttribute> |
| naCustomer | 客户姓名 | 是 | String |
| cellPhone | 客户联系电话 | 是 | String |
| repairPerson | 送修人 | 否 | String |
| repairPersonContact | 送修人联系方式 | 否 | String |
| carNoWhole <br/> | 车牌号 | 是 | String |
| vin<br/> | 车辆VIN码 | 否 | String |
| carBrandName<br/> | 品牌名称 | 否 | String |
| carSeriesName<br/> | 车系名称 | 否 | String |
| carModelShort<br/> | 车型简称 | 否 | String |
| carModel<br/> | 车型 | 否 | String |
| transmissionNo | 变速箱号 | 否 | String |
| carFuelTypeName | 燃料类型 | 否 | String |
| carColor<br/> | 车身颜色 | 否 | String |
| billDate<br/> | 进厂日期 | 是 | String |
| estimatedDeliveryTime<br/> | 预计交车时间 | 是 | String |
| deliveryTime<br/> | 交车时间(出厂时间) | 是 | String |
| mileage<br/> | 进厂里程 | 是 | Double |
| nextMaintainDateRemind | 下次服务时间(服务提醒数据源) | 否 | Date |
| nextMileageRemind | 下次服务里程(服务提醒数据源) | 否 | Double |
| serviceList | 项目列表 | 否 | List<PartialServicePrintAttribute> |
| partList | 材料列表 | 否 | List<PartialPartPrintAttribute> |
| extraChargeList | 附加费列表 | 否 | List<ExtraChargePrintAttribute> |
| extendedModuleList | 扩展模块列表 | 否 | List<ExtendedModulePrintAttribute> |
| memo<br/> | 车主备注 | 否 | String |
| signaturePhotoUrl<br/> | 签名图片 | 否 | String |
| orgName<br/> | 维修厂名称 | 是 | String |
| orgContacts<br/> | 维修厂联系人 | 是 | String |
| orgDetailAddress<br/> | 维修厂地址 | 是 | String |
| orgContactMobile<br/> | 联系电话(维修厂) | 否 | String |
| storeLogo<br/> | logo | 否 | String |
| printOrgName<br/> | 打印抬头 | 否 | String |
| printContent<br/> | 免责条款 | 是 | String |
| printCount<br/> | 打印次数 | 是 | String |
| printTime<br/> | 打印时间 | 是 | String |
**PartialServicePrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| serviceName | 项目名称 | 是 | String |
| labelName | 业务分类名称 | 是 | String |
| isMember | 当前项目是否使用会员 | 是 | String |
| idService | 服务项目id | 是 | BigInteger |
| idInfo | 本地项目id | 是 | String |
| price | 工时单价 | 是 | Double |
| workHour | 工时 | 是 | Double |
| subtotal | 金额 | 是 | Double |
| singleFavourable | 优惠金额 | 是 | Double |
| discountedSubtotal | 折后金额 | 是 | Double |
| serviceMemo | 附加信息备注 | 否 | String |
| discount | 折扣 | 是 | Double |
| empNameStr | 服务项目明细对应修理工名称组装字符串 | 否 | String |
| favourableVoList | 优惠明细 | 否 | List<FavourableDetailPrintAttribute> |
**PartialPartPrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| partName | 材料名称 | 是 | String |
| partShowName | 材料名称(全) | 是 | String |
| partBrand | 配件品牌 | 是 | String |
| unit | 单位 | 是 | String |
| number | 数量 | 是 | Double |
| isMember | 当前项目是否使用会员 | 是 | Integer |
| price | 价格(单价) | 是 | Double |
| subtotal | 金额(材料金额) | 是 | Double |
| discount | 折扣 | 是 | Double |
| singleFavourable | 优惠金额 | 是 | Double |
| discountedSubtotal<br/> | 折后金额 | 是 | Double |
| partMemo<br/> | 备注 | 否 | String |
| isBring<br/> | 是否自带 | 是 | String |
| empNameStr<br/> | 明细对应修理工名称组装字符串 | 否 | String |
| outStockEmployeeName<br/> | 领料人 | 否 | String |
| labelName<br/> | 业务分类名称 | 是 | String |
| idPart<br/> | 配件材料pk | 是 | BigInteger |
| idInfo<br/> | 本地材料id | 是 | String |
| favourableVoList | 优惠明细 | 否 | List<FavourableDetailPrintAttribute> |
**ExtraChargePrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| extraName | 附加费名称 | 是 | String |
| subtotal | 金额 | 是 | Double |
| memo | 备注 | 否 | String |
**FavourableDetailPrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| discountType | 优惠类型 | 是 | Integer |
| discountTypeName | 优惠类型名称 | 是 | Double |
| amount | 优惠金额 | 是 | Double |
**ExtendedModulePrintAttribute**
| 字段 | 含义 | 是否必有 | 类型 |
| --- | --- | --- | --- |
| module | 打印模块,PrintModuleEnum.code | 是 | Integer |
| name | 项目/材料/附加费名称 | 是 | String |
| number | 项目对应工时,材料对应数量,附加费为“-” | 是 | Double |
| price | 项目对应工时单价,材料对应材料单价,,附加费为“-” | 是 | Double |
| subtotal | 项目对应工时费,材料对应材料费,附加费对应为金额 | 是 | Double |
| discount | 项目对应折扣,材料对应折扣,附加费为“1.00” | 是 | Double |
| discountedSubtotal | 项目对应折后金额,材料对应材料费折后金额,,附加费对应为金额 | 是 | Double |
| favourableVoList | 优惠明细 | 否 | List<FavourableDetailPrintAttribute> |
**PrintModuleEnum 打印模块枚举**
| **key** | **code** | **name** |
| --- | --- | --- |
| MAN\_HOUR\_COST | 1 | 工时费模块 |
| MATERIAL\_COST | 2 | 材料费模块 |
| EXTRA\_COST | 3 | 附加费模块 |
| OTHER\_COST | 4 | 其他费用模块 |
| SERVICE\_COST | 5 | 服务费用模块 |
# 工具类jar包附件下载
[附件: print-core-1.0.7.jar](./attachments/JUhN4OhWjosFCdDg/print-core-1.0.7.jar)
### 数字金额转中文方法调用示例:
**数字金额**$P{amount}==null?BigDecimal.ZERO:$P{amount} **转中文**com.f6car.printserver.core.CharacterUtil.chinese($P{amount}==null?BigDecimal.ZERO:$P{amount})
> 更新: 2024-10-17 15:07:46 原文: <https://xcz.yuque.com/ombipo/rpc7ms/qz7dm6e8b06r7t70>
@@ -0,0 +1,64 @@
# 调拨单打印
# 采购单模版打印说明
* 前端调用接口
/stock/allot/print?idAllot=
* 打印使用模版
模版编码=allotInPrint
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/4EZlweZ0zW4mZqxA/img/43819a24-8744-4fc0-96c6-0bba66f2e0b3.png)
* 打印模版参数
HashMap<String, Object> resultMap
| 字段 | 说明 | 备注 |
| --- | --- | --- |
| printFlag | 均价门店 false<br>批次门店 true | |
| showPrice | 参配-打印调拨价格及金额<br>勾选 true<br>未勾选 false | |
| orgName | 打印抬头<br>调入门店打印:XXX调入单<br>调出门店打印:XXX调出单 | |
| billNo | 调拨单号<br>调入门店打印:DRDXXX<br>调出门店打印:DCDXXX | |
| idOwnOrg | 门店ID<br>调入门店打印:调入门店ID<br>调出门店打印:调出门店ID | |
| naOrgIn | 调入门店名称 | |
| naOrgOut | 调出门店名称 | |
| detailAddress | 供应商详细地址 | |
| statusName | 调拨单状态(制单/待发货/待收货/已完成) | |
| billDate | 单据日期(yyyy-MM-dd | |
| creatorName | 创建单名称 | |
| nowDateTime | 打印时间(yyyy-MM-dd HH:mm | |
| remark | 备注信息 | |
| showRemark | 是否显示备注<br>备注为空 false<br>备注不为空 true | |
| sumNumber | 总数量 | |
| sumAmount | 总金额<br>价格脱敏时显示 \*\*\*\* | |
| printCount | 打印次数 | |
| allotDetailVoList | 调拨单明细行 | |
| sortNumber | 序号 | |
| partShowName | 材料组合名称 | |
| customCode | 材料自定义编码 | |
| partName | 材料名称 | 20250731追加 |
| partBrand | 材料品牌 | 20250731追加 |
| supplierCode | 材料零件号 | 20250731追加 |
| standard | 材料规格型号 | 20250731追加 |
| carNo | 车牌 | 20250731追加 |
| defSeat | 货位<br>调入门店打印:调入门店货位<br>调出门店打印:调出门店货位 | |
| unit | 单位 | |
| price | 单价<br>价格脱敏时显示 \*\*\*\* | |
| numCus | 均价门店<br>  调拨个数<br>批次门店&制单&未选择批次<br>   调拨个数<br>批次门店<br>   选择或使用的批次个数 | |
| amount | 均价门店<br>  调拨明细行金额<br>批次门店&制单&未选择批次<br>   调拨明细行金额<br>批次门店<br>   选择或使用的批次个数\*调拨单单价<br>备注:价格脱敏时显示 \*\*\*\* | |
| orderNo | 批次号 | |
| productDate | 批次生成日期 | |
| 明细合计行 | | |
| sortNumber | 合计行 | |
| partShowName | "" | |
| unit | "" | |
| numCus | 总数量 | |
| amount | 总金额<br>价格脱敏时显示 \*\*\*\* | |
| defSeat | "" | |
| stockNumber | "" | |
@@ -0,0 +1,452 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="3fa22123-efc4-4f3f-a186-6a8f692d17e6">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<subDataset name="List1" uuid="7366c5be-288c-41c7-b295-b8d023ec81ae">
<queryString>
<![CDATA[]]>
</queryString>
<field name="index" class="java.lang.String"/>
<field name="serviceName" class="java.lang.String"/>
<field name="cooperationServiceName" class="java.lang.String"/>
<field name="cooperationOrgName" class="java.lang.String"/>
<field name="auditStatus" class="java.lang.String"/>
<field name="cooperationCost" class="java.math.BigDecimal"/>
</subDataset>
<parameter name="tradingStoreName" class="java.lang.String"/>
<parameter name="tradingTime" class="java.lang.String"/>
<parameter name="idSource" class="java.lang.String"/>
<parameter name="cardName" class="java.lang.String"/>
<parameter name="memberNo" class="java.lang.String"/>
<parameter name="noCar" class="java.lang.String"/>
<parameter name="customerName" class="java.lang.String"/>
<parameter name="paymentTypeAndAmount" class="java.lang.String"/>
<parameter name="orgOfPrinting" class="java.lang.String"/>
<parameter name="dateOfPrinting" class="java.lang.String"/>
<parameter name="amount" class="java.lang.String">
<defaultValueExpression><![CDATA[$P{amount}]]></defaultValueExpression>
</parameter>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="97" splitType="Stretch">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="10" width="555" height="40" uuid="7c3a0deb-dcb0-406e-ba9c-9f279e1518b0">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="16" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA["会员卡交易单"]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="210" y="60" width="178" height="18" uuid="6849ccdb-a3a0-4d32-a556-7a8fd5a19cca">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{tradingTime}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="437" y="60" width="118" height="18" uuid="b347e5b3-390c-44c3-b746-481aa6dc808e">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{idSource}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="60" width="90" height="18" uuid="92646184-6d70-4b6e-9f0e-23b4a7b15fb5">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{tradingStoreName}]]></textFieldExpression>
</textField>
<staticText>
<reportElement key="" x="389" y="60" width="48" height="18" isRemoveLineWhenBlank="true" uuid="86735eee-0435-4238-9b93-c08d86ed318f">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[交易单号]]></text>
</staticText>
<staticText>
<reportElement x="0" y="60" width="50" height="18" uuid="fef0343d-31f4-4b1c-a521-1a1b736aa485">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[交易门店]]></text>
</staticText>
<staticText>
<reportElement x="140" y="60" width="70" height="18" uuid="627d90fe-c235-45d8-9f4a-92f8d7ec0de7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[交易日期]]></text>
</staticText>
<line>
<reportElement x="0" y="60" width="555" height="1" uuid="329fa736-2c18-4d9d-8fab-f54846315633">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<staticText>
<reportElement x="389" y="78" width="48" height="18" uuid="7af4c3ab-ae72-4f6b-8b40-7b3129bd7eff">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[车牌号]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="437" y="78" width="118" height="18" uuid="8971461a-8c2e-4c4e-8c3f-9bd177df4b7e">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{noCar}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="78" width="50" height="18" uuid="74602da5-9195-47dd-9416-9cb229aeccad">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[卡名称]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="185" y="78" width="94" height="18" uuid="49312902-8cda-4fe3-a92f-d6ecbf9cb893">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{memberNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="140" y="78" width="44" height="18" uuid="65483940-e0c7-4713-9e9a-6a5a7294d248">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[卡号]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="78" width="90" height="18" uuid="e9c8328d-22ea-4e66-a23f-5015ffbd8248">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{cardName}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="279" y="78" width="40" height="18" uuid="19b4e87c-4a50-42ef-bfd0-7ca0536d5526">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[持卡人]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="319" y="78" width="70" height="18" uuid="86c056e0-0ed9-4024-b868-4133a06c5bab">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{customerName}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="78" width="555" height="1" uuid="f6716829-054d-4fed-a12c-2f4d227724c7">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<line>
<reportElement x="0" y="96" width="555" height="1" uuid="9c9a29fc-1a65-4869-8d03-3242ff1fe667">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="78">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="36" width="50" height="18" uuid="2cf35160-a982-4dcc-881b-c53adcf3761a">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.5" lineStyle="Solid"/>
<topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
<rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[退款方式:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="0" width="120" height="18" uuid="6b4cf2c5-cb81-43b6-82c9-ee238923a378">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="10" isBold="true"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[退卡金额(元)]]></text>
</staticText>
<staticText>
<reportElement x="0" y="17" width="50" height="18" uuid="782a08a7-c605-4a2f-83b7-4e3ad8ee3c4c">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[退款金额]]></text>
</staticText>
<line>
<reportElement x="0" y="35" width="555" height="1" uuid="8699bef5-cebd-4ebc-bb34-3697568375f7">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<line>
<reportElement stretchType="RelativeToTallestObject" x="0" y="18" width="555" height="1" uuid="18287ba4-b04b-4846-9122-3679f693685c">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Dotted"/>
</graphicElement>
</line>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="37" width="504" height="16" uuid="4ff3b629-b9e5-49c5-ad89-ce856a556839">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{paymentTypeAndAmount}]]></textFieldExpression>
</textField>
<line>
<reportElement x="0" y="54" width="555" height="1" uuid="98b923f9-1e14-4cc5-b508-12141cefb96e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.4" lineStyle="Solid"/>
</graphicElement>
</line>
<staticText>
<reportElement x="0" y="55" width="50" height="18" uuid="fc27cb3d-0e5d-4143-b83d-daf432fa56c8">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[打印门店]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="55" width="110" height="18" uuid="1a73fa76-4113-4dcd-803c-da26e934f441"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgOfPrinting}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="160" y="55" width="80" height="18" uuid="786b4cf3-7636-455c-bf05-115ff8ca71b7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="5"/>
</textElement>
<text><![CDATA[打印日期]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="240" y="55" width="314" height="18" uuid="61b50ddd-d880-403c-855f-3ae2f0403592"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{dateOfPrinting}]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="50" y="18" width="504" height="16" uuid="7ce3fef6-73c6-49e9-a676-64e402603d93"/>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="9"/>
</textElement>
<textFieldExpression><![CDATA[$P{amount}]]></textFieldExpression>
</textField>
</band>
<band height="39">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,3 @@
# 采购单/采购退模版打印
[《采购单/采购退打印》](https://alidocs.dingtalk.com/i/nodes/7NkDwLng8ZMajY9YijOwlz4bJKMEvZBY)
@@ -0,0 +1,922 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.3.1.final using JasperReports Library version 6.3.1 -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4" pageWidth="222" pageHeight="352" whenNoDataType="NoPages" columnWidth="202" leftMargin="10" rightMargin="10" topMargin="0" bottomMargin="10" isIgnorePagination="true" uuid="5195ed21-e8c1-4daf-ab55-5f5dc9c07b0a">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="mm"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="mm"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="mm"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="mm"/>
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<property name="ireport.zoom" value="1.0"/>
<property name="ireport.x" value="0"/>
<property name="ireport.y" value="0"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="mm"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
<subDataset name="Dataset1" uuid="7e6cf9c7-d927-4f84-a0f6-e84863998e11">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="subtotal" class="java.math.BigDecimal"/>
<field name="serviceName" class="java.lang.String"/>
<field name="unusedNumber" class="java.lang.Integer"/>
<field name="number" class="java.lang.Integer"/>
<field name="isMember" class="java.lang.Integer"/>
</subDataset>
<subDataset name=" Dataset2" uuid="7dae8b10-73a8-43eb-a81d-296b60197ab3">
<field name="partName" class="java.lang.String"/>
<field name="number" class="java.math.BigDecimal"/>
<field name="subtotal" class="java.math.BigDecimal"/>
<field name="isMember" class="java.lang.Integer"/>
</subDataset>
<parameter name="title" class="java.lang.String"/>
<parameter name="abbreviation" class="java.lang.String"/>
<parameter name="billNo" class="java.lang.String">
<parameterDescription><![CDATA[]]></parameterDescription>
</parameter>
<parameter name="printTime" class="java.lang.String"/>
<parameter name="customerName" class="java.lang.String"/>
<parameter name="cellPhone" class="java.lang.String"/>
<parameter name="carNo" class="java.lang.String"/>
<parameter name="orgName" class="java.lang.String"/>
<parameter name="amountAll" class="java.math.BigDecimal"/>
<parameter name="memo" class="java.lang.String"/>
<parameter name="settlePerson" class="java.lang.String"/>
<parameter name="employeeName" class="java.lang.String"/>
<parameter name="billDate" class="java.lang.String"/>
<parameter name="detailAddress" class="java.lang.String"/>
<parameter name="contactMobile" class="java.lang.String"/>
<parameter name="partList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="transactionDate" class="java.lang.String"/>
<parameter name="naEmployee" class="java.lang.String"/>
<parameter name="mileage" class="java.math.BigDecimal"/>
<parameter name="deliveryTime" class="java.lang.String"/>
<parameter name="vin" class="java.lang.String"/>
<parameter name="carNoWhole" class="java.lang.String"/>
<parameter name="naCustomer" class="java.lang.String"/>
<parameter name="carModelShort" class="java.lang.String"/>
<parameter name="vipExpense" class="java.math.BigDecimal"/>
<parameter name="serviceFavourable" class="java.math.BigDecimal"/>
<parameter name="partFavourableCommonTotal" class="java.math.BigDecimal"/>
<parameter name="oweAmount" class="java.math.BigDecimal"/>
<parameter name="serviceFavourableCommonTotal" class="java.math.BigDecimal"/>
<parameter name="czkExpense" class="java.math.BigDecimal"/>
<parameter name="partinfoFavourable" class="java.math.BigDecimal"/>
<parameter name="discountFavourable" class="java.math.BigDecimal"/>
<parameter name="pointFavourable" class="java.math.BigDecimal"/>
<parameter name="couponFavourable" class="java.math.BigDecimal"/>
<parameter name="balanceStatus" class="java.lang.String"/>
<parameter name="gatheringFavourable" class="java.math.BigDecimal"/>
<parameter name="payItemTogether" class="java.lang.String"/>
<parameter name="orgContactMobile" class="java.lang.String"/>
<parameter name="orgDetailAddress" class="java.lang.String"/>
<parameter name="printOrgName" class="java.lang.String"/>
<parameter name="serviceList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="firstSettlementTime" class="java.lang.String"/>
<parameter name="extraCostTotal" class="java.math.BigDecimal">
<parameterDescription><![CDATA[]]></parameterDescription>
</parameter>
<parameter name="extraChargeList" class="net.sf.jasperreports.engine.data.JRMapCollectionDataSource"/>
<parameter name="orgContactNumber" class="java.lang.String"/>
<parameter name="czkDetailInfo" class="java.lang.String"/>
<parameter name="creatorName" class="java.lang.String"/>
<parameter name="totalStuffNum" class="java.lang.String"/>
<parameter name="stuffSubtotalAll" class="java.math.BigDecimal"/>
<parameter name="stuffSubtotalVip" class="java.math.BigDecimal"/>
<queryString>
<![CDATA[]]>
</queryString>
<detail>
<band height="20" splitType="Stretch">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="202" height="20" isPrintWhenDetailOverflows="true" uuid="fdd7c75d-7f0c-42a3-afa1-9927522a4dd1"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="黑体" size="16" isBold="false"/>
</textElement>
<textFieldExpression><![CDATA[($P{printOrgName}==null?$P{orgName}:($P{printOrgName}.isEmpty()?$P{orgName}:$P{printOrgName}))+"销售单"]]></textFieldExpression>
</textField>
</band>
<band height="20">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="4" width="51" height="16" uuid="8210a1bf-fb5f-465d-8e13-15ee265db665">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[工单号:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="51" y="4" width="151" height="16" uuid="e79b3ef6-3727-46d0-8de1-035b71f358f4">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{billNo}]]></textFieldExpression>
</textField>
</band>
<band height="16">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="2" width="51" height="12" uuid="c34510ca-1706-47a6-bd9f-f78a472136a9">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Top">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[销售人员:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="51" y="2" width="150" height="12" uuid="13df7d48-2be4-4a9f-848b-6a1fa1573a2e">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{naEmployee}]]></textFieldExpression>
</textField>
</band>
<band height="16">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="31" y="2" width="60" height="12" uuid="62146321-e5d6-4c36-b91c-db8c0a5653de">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{naCustomer}.substring(0, 1) + "(*^_^*)" + $P{naCustomer}.substring($P{naCustomer}.length() > 10 ? 10 : $P{naCustomer}.length(), $P{naCustomer}.length())]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="91" y="2" width="110" height="12" uuid="8c918e6d-69a1-4dc3-9336-f54cc900e659">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{cellPhone}.length() <= 3 ? $P{cellPhone}: $P{cellPhone}.substring(0, 3) + "(*^_^*)" + $P{cellPhone}.substring($P{cellPhone}.length() > 7 ? 7 : $P{cellPhone}.length(), $P{cellPhone}.length())]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="2" width="31" height="12" uuid="16d36e31-361b-4fee-a18e-a4ad7991e6c6">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户:]]></text>
</staticText>
</band>
<band height="14">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="2" width="31" height="12" uuid="53391abc-4be2-40a8-8c2d-a770e6c13f18">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[车牌:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="31" y="2" width="171" height="12" uuid="de1db383-eb89-4a11-acd1-ec590d10da96">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{carNoWhole}]]></textFieldExpression>
</textField>
</band>
<band height="4">
<line>
<reportElement x="3" y="3" width="196" height="1" uuid="fcbb9490-ed14-4cb0-8117-6d17915d2403">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.3" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="12">
<printWhenExpression><![CDATA[$P{partList}!=null]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="95" height="12" uuid="c3e05a21-6ba4-4708-991a-825a994a1b2f">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[材料]]></text>
</staticText>
<staticText>
<reportElement x="95" y="0" width="53" height="12" uuid="07662de2-32dd-4e19-9f70-9b1f2927b7ab">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[数量]]></text>
</staticText>
<staticText>
<reportElement x="148" y="0" width="54" height="12" uuid="9c43fe3b-01e3-4683-935a-993973d42461">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[材料费(元)]]></text>
</staticText>
</band>
<band height="12">
<printWhenExpression><![CDATA[$P{partList}!=null]]></printWhenExpression>
<componentElement>
<reportElement x="0" y="0" width="202" height="12" uuid="d0a60b3d-9ead-4191-b6c1-b76521bf47f6">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.VerticalRowLayout"/>
<property name="com.jaspersoft.studio.table.style.table_header" value="Table_TH"/>
<property name="com.jaspersoft.studio.table.style.column_header" value="Table_CH"/>
<property name="com.jaspersoft.studio.table.style.detail" value="Table_TD"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<jr:table xmlns:jr="http://jasperreports.sourceforge.net/jasperreports/components" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports/components http://jasperreports.sourceforge.net/xsd/components.xsd" whenNoDataType="Blank">
<datasetRun subDataset=" Dataset2" uuid="2d38cb48-be8c-41f9-a529-866d4b65253f">
<dataSourceExpression><![CDATA[$P{partList}]]></dataSourceExpression>
</datasetRun>
<jr:column width="95" uuid="13512459-4d71-4edc-b745-751e555e284e">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column1"/>
<jr:detailCell height="12">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="95" height="12" uuid="8d7ef19e-5295-4cfa-ba8e-dfac9dc5029c"/>
<box topPadding="2" bottomPadding="2"/>
<textElement>
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{partName}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="53" uuid="eb394fd2-532e-4da9-8058-785d644277ac">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column2"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="12">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="53" height="12" uuid="31f7ac04-d923-44d2-85ed-9c2d5f22d3b3"/>
<box topPadding="2" bottomPadding="2"/>
<textElement textAlignment="Left">
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{number}]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
<jr:column width="54" uuid="02e06895-4360-460c-ad3a-d4036bde9323">
<property name="com.jaspersoft.studio.components.table.model.column.name" value="Column3"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<jr:detailCell height="12">
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="54" height="12" uuid="544a881f-1055-4089-a49d-e1c979d0b452"/>
<box topPadding="2" bottomPadding="2"/>
<textElement textAlignment="Left">
<font fontName="黑体"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$F{isMember} == 1 ? "0.00" :$F{subtotal}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
</jr:detailCell>
</jr:column>
</jr:table>
</componentElement>
</band>
<band height="4">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{extraChargeList}!=null && $P{partList}!=null]]></printWhenExpression>
<line>
<reportElement x="0" y="3" width="196" height="1" uuid="e343ed32-d402-484e-9550-a3f92a5f1d95">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Dashed"/>
</graphicElement>
</line>
</band>
<band height="12">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<printWhenExpression><![CDATA[$P{extraChargeList}!=null]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="51" height="12" uuid="ab72a883-2de5-4b77-974f-86ce04a85321">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[数量小计:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="51" y="0" width="98" height="12" uuid="fd7e12a8-bb87-4181-b961-362f5011d2c7">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{totalStuffNum}]]></textFieldExpression>
</textField>
</band>
<band height="36">
<staticText>
<reportElement x="62" y="0" width="5" height="12" isRemoveLineWhenBlank="true" uuid="4b3ef70a-6f98-4270-996d-71c65a31b4b7">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="10"/>
<paragraph spacingBefore="2"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="67" y="0" width="82" height="12" uuid="c234c01c-7802-4177-9cf1-ddb4b3a64380">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{stuffSubtotalAll}.subtract($P{stuffSubtotalVip}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="0" width="62" height="12" uuid="0ee54c87-b5c2-43bf-a362-9c79fbd020b2">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[材料费小计:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="56" y="12" width="93" height="12" uuid="651ea460-07ba-4514-b29e-2f07cea1c7b5">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{partFavourableCommonTotal}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="51" y="12" width="5" height="12" uuid="ec51a4b4-84d6-4896-9b51-1bcb63db905f">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="10"/>
<paragraph spacingBefore="2"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<staticText>
<reportElement x="0" y="12" width="51" height="12" uuid="3b7564d7-fd10-45ae-b35d-8b9e7996b37b">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[优惠小计:]]></text>
</staticText>
<staticText>
<reportElement x="70" y="24" width="5" height="12" uuid="df93cf01-d9b9-4e5a-9b54-ecb6d1500ffc">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微软雅黑" size="10"/>
<paragraph spacingBefore="2"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="75" y="24" width="74" height="12" uuid="695cf981-09b0-4b5b-8634-6a6e80812934">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{stuffSubtotalAll}.subtract($P{stuffSubtotalVip}).subtract($P{partFavourableCommonTotal}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="24" width="70" height="12" uuid="8745ff2b-c3e7-4dfe-8c42-3b62f1d198ca">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<box>
<pen lineWidth="0.0"/>
</box>
<textElement verticalAlignment="Middle">
<font fontName="黑体" size="10"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[折后金额小计:]]></text>
</staticText>
</band>
<band height="6">
<printWhenExpression><![CDATA[$P{partList}!=null || $P{serviceList}!=null || $P{extraChargeList}!=null]]></printWhenExpression>
<line>
<reportElement x="0" y="5" width="196" height="1" uuid="92031c32-47e1-4778-9696-acfd36cea5c1">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="17">
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="56" y="4" width="45" height="12" isPrintWhenDetailOverflows="true" uuid="429e5e34-e7cc-4b2e-a7ec-f9bdc72268ce">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{amountAll}.subtract($P{vipExpense}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="0" y="4" width="51" height="12" uuid="91cf76df-ccf1-417b-8ad1-73b7e1be2306">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[合计金额:]]></text>
</staticText>
<staticText>
<reportElement key="" x="101" y="4" width="51" height="12" uuid="0dc06605-4e07-4c3c-855e-22f496455a6a">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<printWhenExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[优惠金额:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="157" y="4" width="45" height="12" isPrintWhenDetailOverflows="true" uuid="a7ee1bfa-c68a-441d-8cde-368dd1642208">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<printWhenExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="152" y="5" width="5" height="12" uuid="5334ca45-b9a7-4bda-949c-8532255cbf73">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<printWhenExpression><![CDATA[$P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
</reportElement>
<textElement verticalAlignment="Middle">
<font isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<staticText>
<reportElement x="51" y="5" width="5" height="12" uuid="93dba0ec-ff63-4524-ad00-7677de202fb0">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font isBold="false" isUnderline="false"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
</band>
<band height="13">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="0" width="51" height="12" uuid="ad0a3fc9-c823-44f3-90bb-ec4358472ece">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[实付金额:]]></text>
</staticText>
<staticText>
<reportElement key="" x="51" y="1" width="5" height="12" uuid="48a4a2fc-5e83-43b5-9faf-593ced47fba0">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="56" y="0" width="45" height="12" isPrintWhenDetailOverflows="true" uuid="d71691b9-8665-4b03-ba8b-e3dc6081afd2">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?"0.00":($P{amountAll}.subtract($P{vipExpense}).subtract($P{oweAmount}).subtract($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).subtract($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).
subtract($P{serviceFavourable}).subtract($P{partinfoFavourable}).subtract($P{discountFavourable}).subtract($P{couponFavourable}).subtract($P{pointFavourable}).subtract($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ))]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="101" y="0" width="51" height="12" uuid="60a16c6f-0ae8-412f-94ea-bcf27f9ae0a3">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[待付金额:]]></text>
</staticText>
<staticText>
<reportElement x="152" y="1" width="5" height="12" uuid="18cf2b7d-cf81-477b-8ef7-1ec2b9a3cb2b">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="157" y="0" width="45" height="12" isPrintWhenDetailOverflows="true" uuid="ce222378-cff4-47d0-9658-edb83a64f33e">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?($P{amountAll}.subtract($P{vipExpense}).subtract($P{czkExpense}).subtract($P{serviceFavourable}.add($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).add($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).add($P{partinfoFavourable}).add($P{discountFavourable}).add($P{couponFavourable}).add($P{pointFavourable}).add($P{gatheringFavourable})).setScale( 2, BigDecimal.ROUND_HALF_EVEN )):($P{oweAmount}.setScale( 2, BigDecimal.ROUND_HALF_EVEN ))]]></textFieldExpression>
</textField>
</band>
<band height="13">
<printWhenExpression><![CDATA[$P{czkExpense}.compareTo(BigDecimal.ZERO) != 0]]></printWhenExpression>
<staticText>
<reportElement x="0" y="0" width="61" height="12" uuid="e3d118e6-8e4c-4562-a92f-b02171737fa4">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[储值卡消费:]]></text>
</staticText>
<staticText>
<reportElement x="61" y="1" width="5" height="12" uuid="dc416d41-3d4e-4ec2-8df2-8fd963d34b2f">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="SansSerif" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="67" y="0" width="45" height="12" isPrintWhenDetailOverflows="true" uuid="93fc5f6c-5c7c-4e75-9054-783e6dbbfb0d">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{czkExpense}.setScale( 2, BigDecimal.ROUND_HALF_EVEN )]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="112" y="0" width="90" height="12" isPrintWhenDetailOverflows="true" uuid="7b247b30-a5cc-4d9a-84e2-24128a6b4775">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{czkDetailInfo}==null?"":"(" + $P{czkDetailInfo} + ")"]]></textFieldExpression>
</textField>
</band>
<band height="17">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="0" width="51" height="12" uuid="2f9041fb-4bfa-43a0-9642-e429c87c8326">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[收银金额:]]></text>
</staticText>
<staticText>
<reportElement x="51" y="1" width="5" height="12" uuid="9f6a7ec2-4243-454c-a1b6-2adc655dd86a">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="SansSerif" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<text><![CDATA[¥]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="57" y="0" width="55" height="12" isPrintWhenDetailOverflows="true" uuid="507cb11e-dfc7-477d-ae35-49de03f644f1">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")?"0.00":($P{amountAll}.subtract($P{vipExpense}).subtract($P{oweAmount}).subtract($P{partFavourableCommonTotal}==null?BigDecimal.ZERO:$P{partFavourableCommonTotal}).subtract($P{serviceFavourableCommonTotal}==null?BigDecimal.ZERO:$P{serviceFavourableCommonTotal}).subtract($P{czkExpense}).
subtract($P{serviceFavourable}).subtract($P{partinfoFavourable}).subtract($P{discountFavourable}).subtract($P{couponFavourable}).subtract($P{pointFavourable}).subtract($P{gatheringFavourable}).setScale( 2, BigDecimal.ROUND_HALF_EVEN ).toString())]]></textFieldExpression>
</textField>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="112" y="0" width="90" height="12" isPrintWhenDetailOverflows="true" uuid="1d396dda-a308-4062-addd-bf1e6d367ef0">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" size="9" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{balanceStatus}.equals("7000")? "" : $P{payItemTogether} == null ? "" : "("+$P{payItemTogether}+")"]]></textFieldExpression>
</textField>
</band>
<band height="1">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<line>
<reportElement x="0" y="0" width="196" height="1" uuid="232d5e4a-f361-483f-a3d5-b5ebe29dc22e">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.5" lineStyle="Solid"/>
</graphicElement>
</line>
</band>
<band height="47">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<staticText>
<reportElement x="0" y="4" width="53" height="12" uuid="1d22bc87-0fc0-4f8f-8c98-62adcc943822">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[客户签名:]]></text>
</staticText>
<staticText>
<reportElement x="0" y="30" width="31" height="12" uuid="caf074dd-5c62-4224-a0d8-ef4380188ecd">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[备注:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="31" y="30" width="81" height="12" uuid="049afdd2-76f6-4be7-86e9-b213c9fbf31a">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{memo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="112" y="30" width="31" height="12" uuid="bac0ce57-79c7-4cc7-842e-5f15284486a1">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[日期:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="143" y="30" width="59" height="12" isPrintWhenDetailOverflows="true" uuid="a8131af0-d94a-4b50-93f8-c187293efb33">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Top">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{deliveryTime}.isEmpty()?"":($P{deliveryTime}.substring( 0, 10))]]></textFieldExpression>
</textField>
</band>
<band height="1">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<line>
<reportElement x="0" y="0" width="202" height="1" uuid="c936fb70-4d8c-405d-ab85-2e8916a715c1">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
</reportElement>
<graphicElement>
<pen lineWidth="0.3"/>
</graphicElement>
</line>
</band>
<band height="20">
<staticText>
<reportElement x="0" y="4" width="51" height="16" uuid="d47fdd5c-eb46-4023-8e59-19de6fe9f43a">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[门店名称:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="51" y="6" width="151" height="12" uuid="519ca386-a8c3-47ee-bbe8-6046d7a8512b">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgName}]]></textFieldExpression>
</textField>
</band>
<band height="16">
<staticText>
<reportElement x="0" y="0" width="51" height="16" uuid="477d09a8-4cd9-4a13-88eb-65a6f70e13a6">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[联系电话:]]></text>
</staticText>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="51" y="0" width="151" height="16" uuid="5fe5a641-383e-488e-b59b-604282cdb856">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgContactMobile}==null?"":$P{orgContactMobile}]]></textFieldExpression>
</textField>
</band>
<band height="16">
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
<textField isStretchWithOverflow="true" isBlankWhenNull="true">
<reportElement x="0" y="0" width="202" height="16" uuid="8d8ad542-4e85-4c3c-93b1-dcf7fcb38720">
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
</reportElement>
<textElement verticalAlignment="Middle">
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<textFieldExpression><![CDATA[$P{orgDetailAddress}]]></textFieldExpression>
</textField>
</band>
<band height="12">
<staticText>
<reportElement x="62" y="0" width="51" height="12" uuid="fca6fa0d-2eaf-42f4-986c-62724d5e6e17">
<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="3"/>
</textElement>
<text><![CDATA[打印时间:]]></text>
</staticText>
<textField isBlankWhenNull="true">
<reportElement x="113" y="0" width="89" height="12" uuid="a987d0cf-8310-4b16-b278-5eeadb07c893">
<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
</reportElement>
<textElement>
<font fontName="黑体" isBold="false"/>
<paragraph leftIndent="0"/>
</textElement>
<textFieldExpression><![CDATA[$P{printTime}.substring( 0, 16)]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>
@@ -0,0 +1,13 @@
{
"kb_id": "db5e3844c382439dba91c29f4f29eeca",
"user_id": "d198ae3b32cd49f09736c4290dd1223a",
"name": "测试csv",
"description": "csv",
"created_at": "2026-05-23T15:31:44.119922+00:00",
"updated_at": "2026-05-23T15:31:44.119922+00:00",
"fields": [],
"templates": [],
"file_count": 0,
"chunk_count": 0,
"parse_status": "empty"
}
@@ -0,0 +1,5 @@
{
"user_id": "d198ae3b32cd49f09736c4290dd1223a",
"name": "默认用户",
"created_at": "2026-05-23T12:19:43.785929+00:00"
}
+12
View File
@@ -0,0 +1,12 @@
[
{
"user_id": "d198ae3b32cd49f09736c4290dd1223a",
"name": "默认用户",
"created_at": "2026-05-23T12:19:43.785929+00:00"
},
{
"user_id": "2db10c2ebbf6434aab28035026e196c3",
"name": "SmokeTest",
"created_at": "2026-05-23T12:21:32.399217+00:00"
}
]
+47
View File
@@ -0,0 +1,47 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.xml.*;
import java.io.*;
public class JrxmlDebug {
public static void main(String[] args) throws Exception {
String path = args.length > 0 ? args[0] : "D:/Idea Project/jaspersoft/tmp/test_simple.jrxml";
File f = new File(path);
System.out.println("File: " + path + " (exists=" + f.exists() + ", len=" + f.length() + ")");
// Test 1: JRXmlLoader.load()
System.out.println("\n=== JRXmlLoader.load() ===");
try {
JasperDesign design = JRXmlLoader.load(f);
System.out.println("PASS: " + design.getName()
+ " pages=" + design.getPageWidth() + "x" + design.getPageHeight());
System.out.println(" Title: " + (design.getTitle() != null ? design.getTitle().getHeight() + "px" : "null"));
System.out.println(" Detail: " + (design.getDetailSection() != null ? "present" : "null"));
} catch (Throwable t) {
System.out.println("FAIL: " + t.getMessage());
Throwable c = t;
int d = 0;
while (c != null) {
System.out.println(" [" + d + "] " + c.getClass().getName() + ": " + c.getMessage());
for (int i = 0; i < Math.min(5, c.getStackTrace().length); i++)
System.out.println(" at " + c.getStackTrace()[i]);
c = c.getCause();
d++;
}
}
// Test 2: JasperCompileManager.compileReport()
System.out.println("\n=== JasperCompileManager.compileReport() ===");
try {
JasperReport report = JasperCompileManager.compileReport(path);
System.out.println("PASS: " + report.getName());
} catch (Throwable t) {
System.out.println("FAIL: " + t.getMessage());
Throwable c = t;
while (c != null) {
System.out.println(" -> " + c.getClass().getName() + ": " + c.getMessage());
c = c.getCause();
}
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.jackson.util.*;
import java.io.*;
/**
* Generate a minimal JasperDesign programmatically, then serialize it
* via JacksonUtil to show the correct 7.x XML format.
*/
public class JrxmlGen {
public static void main(String[] args) throws Exception {
JasperDesign design = new JasperDesign();
design.setName("TestReport");
design.setPageWidth(595);
design.setPageHeight(842);
design.setColumnWidth(555);
design.setLeftMargin(20);
design.setRightMargin(20);
design.setTopMargin(20);
design.setBottomMargin(20);
design.setWhenNoDataType(com.fasterxml.jackson.databind.node.TextNode.valueOf("AllSectionsNoDetail"));
design.setQuery("SELECT 1");
JRDesignBand titleBand = new JRDesignBand();
titleBand.setHeight(50);
JRDesignStaticText st = new JRDesignStaticText();
st.setX(0);
st.setY(0);
st.setWidth(555);
st.setHeight(30);
st.setText("HELLO WORLD");
titleBand.addElement(st);
design.setTitle(titleBand);
JRDesignBand detailBand = new JRDesignBand();
detailBand.setHeight(20);
JRDesignStaticText dt = new JRDesignStaticText();
dt.setX(0);
dt.setY(0);
dt.setWidth(555);
dt.setHeight(20);
dt.setText("test row");
detailBand.addElement(dt);
design.setDetail(detailBand);
JacksonUtil util = JacksonUtil.getInstance(DefaultJasperReportsContext.getInstance());
String xml = util.saveXml(design);
System.out.println("=== Serialized 7.x JRXML ===");
System.out.println(xml);
String outPath = "D:/Idea Project/jaspersoft/tmp/test_reference.jrxml";
try (FileWriter fw = new FileWriter(outPath)) {
fw.write(xml);
}
System.out.println("\nSaved to: " + outPath);
}
}
+110
View File
@@ -0,0 +1,110 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.export.*;
import net.sf.jasperreports.export.*;
import java.io.*;
import java.util.*;
import java.awt.image.*;
import javax.imageio.*;
/**
* Minimal JRXML PNG renderer for pixel-level fidelity comparison.
* Usage: java JrxmlRenderer input.jrxml output.png [scale]
* scale: optional zoom factor (default 2.0 for readable text)
*
* Uses JasperReports 7.x exporter API (SimpleExporterInput / SimpleGraphics2DExporterOutput).
*/
public class JrxmlRenderer {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.err.println("Usage: java JrxmlRenderer <input.jrxml> <output.png> [scale]");
System.exit(1);
}
String jrxmlPath = args[0];
String outputPath = args[1];
float scale = args.length >= 3 ? Float.parseFloat(args[2]) : 2.0f;
File jrxmlFile = new File(jrxmlPath);
if (!jrxmlFile.exists()) {
System.err.println("ERROR: JRXML file not found: " + jrxmlPath);
System.exit(2);
}
try {
// 1. Compile JRXML JasperReport
JasperReport report = JasperCompileManager.compileReport(jrxmlPath);
// 2. Fill with empty data source
Map<String, Object> params = new HashMap<>();
JasperPrint print = JasperFillManager.fillReport(report, params, new JREmptyDataSource());
int pages = print.getPages().size();
if (pages == 0) {
System.err.println("ERROR: Report has 0 pages after filling");
System.exit(4);
}
// 3. Calculate image dimensions from page size
// JasperReports uses 72 DPI, convert to pixels with scale
int pageWidth = (int) Math.ceil(report.getPageWidth() * scale);
int pageHeight = (int) Math.ceil(report.getPageHeight() * scale);
// 4. Render each page to a BufferedImage
java.util.List<BufferedImage> pageImages = new ArrayList<>();
for (int i = 0; i < pages; i++) {
BufferedImage pageImage = new BufferedImage(pageWidth, pageHeight, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g2d = pageImage.createGraphics();
g2d.setColor(java.awt.Color.WHITE);
g2d.fillRect(0, 0, pageWidth, pageHeight);
// Scale the graphics context
g2d.scale(scale, scale);
JRGraphics2DExporter exporter = new JRGraphics2DExporter();
exporter.setExporterInput(new SimpleExporterInput(print));
SimpleGraphics2DExporterOutput output = new SimpleGraphics2DExporterOutput();
output.setGraphics2D(g2d);
exporter.setExporterOutput(output);
SimpleGraphics2DReportConfiguration config = new SimpleGraphics2DReportConfiguration();
config.setPageIndex(i);
exporter.setConfiguration(config);
exporter.exportReport();
g2d.dispose();
pageImages.add(pageImage);
}
// 5. Combine pages into single tall image
int totalHeight = 0;
for (BufferedImage img : pageImages) {
totalHeight += img.getHeight();
}
BufferedImage combined = new BufferedImage(pageWidth, totalHeight, BufferedImage.TYPE_INT_RGB);
java.awt.Graphics2D g = combined.createGraphics();
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, pageWidth, totalHeight);
int yOffset = 0;
for (BufferedImage img : pageImages) {
g.drawImage(img, 0, yOffset, null);
yOffset += img.getHeight();
}
g.dispose();
// 6. Write PNG
ImageIO.write(combined, "png", new File(outputPath));
System.out.println("OK: " + outputPath
+ " (pages=" + pages
+ ", size=" + pageWidth + "x" + totalHeight
+ ", scale=" + scale + ")");
} catch (JRException e) {
System.err.println("JASPER_ERROR: " + e.getMessage());
e.printStackTrace();
System.exit(3);
}
}
}
+36
View File
@@ -0,0 +1,36 @@
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.design.*;
import net.sf.jasperreports.engine.xml.*;
import java.io.*;
public class JrxmlRendererTest {
public static void main(String[] args) throws Exception {
String path = args.length > 0 ? args[0] : "/tmp/test_render.jrxml";
File f = new File(path);
System.out.println("File: " + f.getAbsolutePath() + " exists=" + f.exists() + " len=" + f.length());
// Read content to check XML validity
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
String line;
int lineNum = 0;
while ((line = br.readLine()) != null && lineNum < 5) {
System.out.println(" L" + (++lineNum) + ": " + line);
}
}
try {
System.out.println("Step 1: Loading JRXML...");
JasperDesign design = JRXmlLoader.load(f);
System.out.println(" OK - name=" + design.getName());
} catch (JRException e) {
System.err.println("JR ERROR: " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.err.println("GENERIC ERROR: " + e.getClass().getName() + ": " + e.getMessage());
e.printStackTrace();
} catch (Error e) {
System.err.println("FATAL ERROR: " + e.getClass().getName() + ": " + e.getMessage());
e.printStackTrace();
}
}
}
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Download JasperReports 6.21.0 and dependencies for JRXML-to-PNG rendering.
# Run this once after cloning the repo.
set -e
BASE="https://repo1.maven.org/maven2"
JARS=(
"net/sf/jasperreports/jasperreports/6.21.0/jasperreports-6.21.0.jar"
"commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar"
"org/apache/commons/commons-collections4/4.5.0/commons-collections4-4.5.0.jar"
"commons-beanutils/commons-beanutils/1.10.1/commons-beanutils-1.10.1.jar"
"org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar"
"commons-digester/commons-digester/2.1/commons-digester-2.1.jar"
"com/lowagie/itext/2.1.7/itext-2.1.7.jar"
"org/jfree/jfreechart/1.5.5/jfreechart-1.5.5.jar"
"org/eclipse/jdt/ecj/3.38.0/ecj-3.38.0.jar"
)
for jar in "${JARS[@]}"; do
fname=$(basename "$jar")
if [ -f "$fname" ]; then
echo "SKIP: $fname (exists)"
else
echo "DOWNLOAD: $fname"
curl -sL -o "$fname" "$BASE/$jar"
fi
done
echo ""
echo "All JARs ready. Compile with:"
echo " javac -cp \"jasperreports-6.21.0.jar;...\" JrxmlRenderer.java"
echo " java -cp \".;jasperreports-6.21.0.jar;...\" JrxmlRenderer input.jrxml output.png 2.0"
+16 -1
View File
@@ -3,7 +3,14 @@
关键规则:
- 只输出完整修复后的 JRXML 代码,不要解释,不要 markdown 标记。
- JRXML 必须与 JasperReports 7.0.6 兼容。
- 解决下面列出的特定错误。
- **一次只修复一个错误**:关注错误列表中的第 1 个错误,修复它即可。不要尝试修复所有错误。
- **输出不能与输入相同**:如果你发现自己要输出和当前 JRXML 完全相同的代码,说明你没有修复错误——必须做出实质性改动。
- 如果当前 JRXML 内容为空或过短(<200 字符),请根据下方提供的 OCR 识别数据和布局 schema 重新生成完整的 JRXML,而非输出一个占位桩。
- 如果错误是"字段 'field_N' 未在 <field> 部分声明"**必须**为每个缺失的 field_N 添加 `<field name="field_N" class="java.lang.String"/>` 声明。这些是占位字段,不可删除。同时确保所有 $F{{field_N}} 引用都有对应的 <field> 声明。
- 如果错误是"字段 'field_N' 未在 <field> 部分声明"且有 OCR 字段数据,尝试将 $F{{field_N}} 替换为 OCR 中对应的真实字段名(如 $F{{invoice_code}}),同时更新 <field> 声明和所有引用。
- 【强制】修正后的 JRXML 必须保证所有 $F{...} 引用都有对应的 <field name="..."> 声明。禁止出现 $F{field_name} 却没有对应 field 声明的情况。
- 【强制】font 标签必须符合 JasperReports XSD<font fontName="..." size="..." isBold="..." isItalic="..." isUnderline="..."/>。禁止在 <font> 标签上写 fontName= 属性(错误写法),必须使用嵌套属性格式(正确写法)。
- **始终检查并修复命名空间**:正确的根元素格式必须为:`<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd">`。删除所有 ns0: 前缀,删除所有 `xmlns:ns0` 声明,删除所有元素标签上的 `ns0:` 前缀。
当前 JRXML(带错误):
{current_jrxml}
@@ -14,4 +21,12 @@
错误的自然语言解释:
{explanation}
{ocr_context}
{layout_schema_text}
{fidelity_context}
{template_context}
立即生成修正后的 JRXML
+11 -4
View File
@@ -1,16 +1,23 @@
你是一位资深 JasperReports 工程师。当前有一个 JRXML 使用占位字段名($F{field_1}, $F{field_2}, ...),需要替换为从 OCR 提取的真实字段名。
你是一位资深 JasperReports 工程师。当前有一个 JRXML 使用占位字段名($F{{field_1}}, $F{{field_2}}, ...),需要替换为从 OCR 提取的真实字段名。
**你必须基于已有的 JRXML 进行修改,而不是重新生成。保留所有现有的元素,只替换字段名。**
关键规则:
- 只输出完整修改后的 JRXML 代码,不要解释,不要 markdown 标记。
- 将每个 $F{field_N} 占位符替换为 OCR 提取结果中对应的真实字段名。
- 替换规则:根据列的顺序映射——$F{field_1} 对应第 1 列的 OCR 字段名,$F{field_2} 对应第 2 列,以此类推。
- 同时更新 <field name="..."> 声明和所有 $F{...} 表达式中的引用
- **保留所有现有的 field、staticText、textField、band、reportElement 元素,一个都不能少。**
- **不要删除、简化、合并或重写任何元素。不要改动任何坐标值(x, y, width, height)。**
- 将每个 $F{{field_N}} 占位符替换为 OCR 提取结果中对应的真实字段名
- 替换规则:根据列的顺序映射——$F{{field_1}} 对应第 1 列的 OCR 字段名,$F{{field_2}} 对应第 2 列,以此类推。
- 同时更新 field name="..." 声明和所有 $F{{...}} 表达式中的引用。
- 如果 OCR 提取的字段数少于占位字段数,保留多余的占位字段。
- 不要修改 band 结构、元素位置或大小。
- 确保 JRXML 兼容 JasperReports 7.0.6。
- **输出的 JRXML 字符数应与输入的 JRXML 大致相同(允许 ±15% 偏差),因为只替换字段名,不增删修改任何元素。**
当前 JRXML(含占位字段):
{current_jrxml}
{template_context}
OCR 提取的结构化字段:
{ocr_fields}
+9 -1
View File
@@ -4,12 +4,20 @@ JRXML 必须兼容 JasperReports 7.0.6 schema。
关键规则:
- 只输出 JRXML 代码,不要解释,不要 markdown 标记。
- 报表正文中使用的每个字段必须在 <field name="..."> 部分中声明。
- 根元素为 <jasperReport>,包含正确的 xmlns 属性
- 【强制】在 <jasperReport> 下必须包含完整的 <fields> 节,列出所有用到的字段。每个字段格式:<field name="field_name" class="java.lang.String"/>。禁止出现 $F{field_name} 却没有对应 field 声明的情况
- 【强制】font 标签结构:使用 <font fontName="Serif" size="12"/> 而非 <fontName="Serif"/> 等属性写法。font 标签必须符合 JasperReports XSD<font fontName="..." size="..." isBold="..." isItalic="..." isUnderline="..."/>
- 根元素为 <jasperReport>,包含正确的 xmlns 属性。**禁止在元素标签上使用 ns0: 前缀**。正确的根元素格式:
```xml
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd">
```
删除所有 `xmlns:ns0` 声明,删除所有元素标签上的 `ns0:` 前缀。
- 包含 <queryString>,在 <![CDATA[...]]> 中包含 SQL 查询。
- 确保所有交叉引用(字段名称、band 元素)保持一致。
参考模板和组件:
{context}
{template_context}
用户需求:
{user_request}
+3 -1
View File
@@ -3,13 +3,15 @@
关键规则:
- 只输出完整修改后的 JRXML 代码,不要解释,不要 markdown 标记。
- 保留所有未被更改的现有结构。
- 结果必须继续与 JasperReports 7.0.6 兼容。
- 结果必须继续与 JasperReports 7.0.6 兼容。命名空间必须为 `xmlns="http://jasperreports.sourceforge.net/jasperreports"`,不可使用 jaspersoft.com 等错误 URL。
- 报表正文中使用的每个字段必须在 <field> 部分中声明。
- 如果添加新字段,正确声明它们。
- 确保 <queryString> 是 <![CDATA[...]]> 中有效的 SQL。
{ocr_context}
{template_context}
当前 JRXML
{current_jrxml}
+21 -13
View File
@@ -1,17 +1,25 @@
你是一位资深 JasperReports 工程师。当前有一个骨架 JRXML,需要根据精确的像素坐标调整每个元素的位置
你是一位 JRXML 坐标调整器。你的唯一任务是修改 <reportElement> 标签内的 x, y, width, height 属性值
关键规则:
- 只输出完整修改后的 JRXML 代码,不要解释,不要 markdown 标记。
- 根据提供的采样坐标,精确调整每个 textField/staticText 的 x, y, width, height。
- 表头行的坐标直接使用采样坐标中 header_row 对应列的 x, y, width, height。
- 数据行:根据 first_data_row 的坐标模式,向下插值生成剩余数据行(每行 y 递增行高)。
- 标题行(如有)和表尾行:保持其在骨架中的 y 位置大致不变,但调整 x 和 width 与列的采样坐标对齐。
- 不要修改字段名(保持 $F{field_N} 占位名不变)。
- 不要修改 band 结构。
- 确保 JRXML 兼容 JasperReports 7.0.6。
**这是 Band "{band_name}"(高度 {band_height}px)的第 {window_index}/{total_windows} 个窗口。你只看到该 band 的一个片段,不要尝试生成完整报表。**
当前骨架 JRXML
{current_jrxml}
严格规则
- 只修改 x, y, width, height 以及 band 的 height 属性。不改任何其他内容。
- 不添加、删除、重命名任何元素。
- 不修改文本内容(CDATA)、表达式(textFieldExpression)、样式属性。
- 只输出修改后的 XML 片段,不要解释,不要 markdown,不要代码块标记。
- 输出的字符数应与输入片段大致相同。
- **每个 reportElement 必须有有效的 x, y, width, height(全部 > 0)。禁止输出 x="0" y="0" 或 width="0" height="0"。**
采样坐标(表头行 + 第一行数据行,像素位置)
坐标调整规则
- 表头行:直接使用 header_row 对应列的 x, y, width, height
- 数据行:根据 first_data_row 的坐标模式,向下插值(每行 y 递增行高)
- 标题行和表尾行:保持 y 位置大致不变,但调整 x 和 width 与列的采样坐标对齐
- **调整完所有子元素坐标后,将 band height 更新为 max(所有子元素 y + height) + 20px。所有子元素的 y + height 不能超过 band height。**
{template_context}
采样坐标参考:
{sampled_coordinates}
请调整以下片段的坐标:
{xml_fragment}
+6 -2
View File
@@ -2,10 +2,12 @@
关键规则:
- 只输出 JRXML 代码,不要解释,不要 markdown 标记。
- 使用 $F{field_1}, $F{field_2}, ... 作为占位字段名,并在 <field> 部分声明它们。
- 使用 $F{{field_1}}, $F{{field_2}}, ... 作为占位字段名,并在 <field> 部分声明它们。
- 【强制】在 <jasperReport> 下必须包含完整的 <fields> 节,列出所有用到的字段。每个字段格式:<field name="field_name" class="java.lang.String"/>。禁止出现 $F{field_name} 却没有对应 field 声明的情况。
- 【强制】font 标签结构:使用 <font fontName="Serif" size="12"/> 而非 <fontName="Serif"/> 等属性写法。font 标签必须符合 JasperReports XSD<font fontName="..." size="..." isBold="..." isItalic="..." isUnderline="..."/>
- 报表结构必须正确(title, pageHeader, columnHeader, detail, pageFooter 等 band)。
- 元素位置使用近似值即可,后续会精确调整。
- 根元素为 <jasperReport>,包含正确的 xmlns 属性
- 根元素为 <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd">,命名空间和 schemaLocation 必须精确,不可使用其他 URL(如 jaspersoft.com)。**禁止在元素标签上使用 ns0: 前缀**
- 包含 <queryString>,在 <![CDATA[...]]> 中放置占位 SQLSELECT * FROM table_name)。
- 确保 JRXML 兼容 JasperReports 7.0.6。
@@ -15,5 +17,7 @@
参考模板和组件:
{context}
{template_context}
用户需求:
{user_request}

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