diff --git a/output/pdf/委外订单_签章调整样张.pdf b/output/pdf/委外订单_签章调整样张.pdf index 944874b..758b2ac 100644 Binary files a/output/pdf/委外订单_签章调整样张.pdf and b/output/pdf/委外订单_签章调整样张.pdf differ diff --git a/output/pdf/采购单_签章调整样张.pdf b/output/pdf/采购单_签章调整样张.pdf index 2731855..b80167e 100644 Binary files a/output/pdf/采购单_签章调整样张.pdf and b/output/pdf/采购单_签章调整样张.pdf differ diff --git a/packaging/win7/00_check_environment.cmd b/packaging/win7/00_check_environment.cmd new file mode 100644 index 0000000..10bd955 --- /dev/null +++ b/packaging/win7/00_check_environment.cmd @@ -0,0 +1,8 @@ +@echo off +setlocal EnableExtensions DisableDelayedExpansion +set "PO_KIT_ROOT=%~dp0" +cd /d "%PO_KIT_ROOT%" +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%PO_KIT_ROOT%win7\install_env.ps1" -ProbeOnly +set "PO_RESULT=%ERRORLEVEL%" +pause +exit /b %PO_RESULT% diff --git a/packaging/win7/01_install_env.cmd b/packaging/win7/01_install_env.cmd index 82b81e5..f5edc42 100644 --- a/packaging/win7/01_install_env.cmd +++ b/packaging/win7/01_install_env.cmd @@ -1,30 +1,23 @@ @echo off setlocal EnableExtensions DisableDelayedExpansion -cd /d "%~dp0" +set "PO_KIT_ROOT=%~dp0" +cd /d "%PO_KIT_ROOT%" set "PYTHONUTF8=1" set "PYTHONIOENCODING=utf-8" set "PYTHONHOME=" set "PYTHONPATH=" -set "PO_PYTHON=%~dp0.python38\python.exe" -set "PO_INSTALLER=%~dp0installers\python-3.8.10.exe" -if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "PO_INSTALLER=%~dp0installers\python-3.8.10-amd64.exe" -if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" set "PO_INSTALLER=%~dp0installers\python-3.8.10-amd64.exe" -if exist "%PO_PYTHON%" goto ready -echo Installing the bundled Python 3.8.10. Windows 7 SP1 is required. -echo If Windows requests a restart, restart and run this script again. -start /wait "" "%PO_INSTALLER%" /passive InstallAllUsers=0 TargetDir="%~dp0.python38" Include_launcher=0 InstallLauncherAllUsers=0 PrependPath=0 Include_doc=0 Include_test=0 Include_tcltk=0 Shortcuts=0 AssociateFiles=0 -if errorlevel 1 goto failed -:ready -if not exist "%PO_PYTHON%" goto failed -"%PO_PYTHON%" "%~dp0win7\build.py" setup -if errorlevel 1 goto failed -echo. +"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%PO_KIT_ROOT%win7\install_env.ps1" +set "PO_RESULT=%ERRORLEVEL%" +if "%PO_RESULT%"=="3010" goto restart +if "%PO_RESULT%"=="1641" goto restart +if not "%PO_RESULT%"=="0" goto failed echo Setup passed. Run 02_build_exe.cmd next. -pause -exit /b 0 +goto finish +:restart +echo Windows requested a restart. Restart, then run 01_install_env.cmd again. +goto finish :failed -echo. -echo SETUP FAILED. See README_WIN7.txt and the message above. -echo Do not install the modern Python 3.11 build on Windows 7. +echo SETUP FAILED. Exit code: %PO_RESULT%. Keep the logs folder and the message above. +:finish pause -exit /b 1 +exit /b %PO_RESULT% diff --git a/packaging/win7/02_build_exe.cmd b/packaging/win7/02_build_exe.cmd index 5484b27..94c0ede 100644 --- a/packaging/win7/02_build_exe.cmd +++ b/packaging/win7/02_build_exe.cmd @@ -1,12 +1,13 @@ @echo off setlocal EnableExtensions DisableDelayedExpansion -cd /d "%~dp0" +set "PO_KIT_ROOT=%~dp0" +cd /d "%PO_KIT_ROOT%" set "PYTHONUTF8=1" set "PYTHONIOENCODING=utf-8" set "PYTHONHOME=" set "PYTHONPATH=" -if not exist "%~dp0.buildenv\Scripts\python.exe" goto missing -"%~dp0.buildenv\Scripts\python.exe" "%~dp0win7\build.py" build +if not exist "%PO_KIT_ROOT%.buildenv\Scripts\python.exe" goto missing +"%PO_KIT_ROOT%.buildenv\Scripts\python.exe" "%PO_KIT_ROOT%win7\build.py" build if errorlevel 1 goto failed echo. echo EXE is in the new dated subfolder under dist. See BUILD_INFO.json. diff --git a/packaging/win7/README_WIN7.md b/packaging/win7/README_WIN7.md index f70b383..bc8a6e1 100644 --- a/packaging/win7/README_WIN7.md +++ b/packaging/win7/README_WIN7.md @@ -2,9 +2,11 @@ 本包用于在你的 Windows 7 服务电脑上,从 v4 源码生成 EXE。包括 Python 3.8.10 官方完整安装程序、32 位与 64 位离线依赖、构建脚本、环境与业务自检,以及现有 16 页使用升级手册。 +安装脚本修复:新版解压到另一文件夹时,先检查本包及系统登记的 Python 3.8.10,确认位数和运行正常后复用;不再仅因新目录没有 `.python38` 就重复安装。只有找不到可用环境时才运行官方安装器。安装器错误码、依赖安装及自检输出会保存在 `logs/`。 + 2026-09-17 文件名更新:在“供应商管理”的新增或编辑窗口填写“公司简称”,PDF 文件名即为“采购单_订单号_简称.pdf”(委外单使用对应类型名称);未填写简称时使用供应商全名。已审批订单重新下载也使用新文件名,归档 PDF 内容不变,无需重新审批。Excel 模板新增末列“公司简称”,旧表缺少该列时保留原简称。本次启动会先备份数据库,再升级到数据库结构版本 5,新增简称字段。 -2026-09-17 排版更新:采购专用章直径增加 30%,固定在右侧需方签章空位;供方和需方签章区之间留出 20 毫米间隔。采购单与委外订单均已调整,示例见 `samples/`。本次 PDF 模板版本为 `po-v4`。 +2026-09-17 排版更新:公司抬头从 12.5 磅加大到 20 磅,保持黑体居中。采购专用章直径增加 30%,固定在右侧需方签章空位;供方和需方签章区之间留出 20 毫米间隔。采购单与委外订单均已调整,示例见 `samples/`。本次 PDF 模板版本为 `po-v5`。 升级后新审批生成的正式 PDF 使用新排版。已审批归档的 PDF 保留原版;需要采用新版的订单,请通过系统的“变更重审”流程重新审批。使用本包重新解压、安装环境并生成 EXE 后,按下文备份和替换服务程序。 @@ -18,10 +20,11 @@ ## 2. 两步生成 EXE 1. 双击 `01_install_env.cmd`。 - - 使用包内 Python 3.8.10 安装程序;安装到包内 `.python38`,不把它加入系统 PATH,也不替换其他 Python 版本。 + - 优先复用已安装且位数匹配的 Python 3.8.10;没有可用环境时才用包内安装器安装到 `.python38`。不改系统 PATH。 + - 如果复用的是旧包文件夹内的 Python,请保留该文件夹,以便后续打包;生成的 EXE 运行不依赖该 Python 环境。 - 从本包 `wheels/x64` 或 `wheels/x86` 离线安装到 `.buildenv`,不连接 pip 源、不临时升级依赖。 - 自动检查密码哈希、中文 PDF、制单审批、部分/全部付款交货、Excel 台账和 HTTP 服务。自检仅使用临时数据库,通知不开启。 - - 看到 `SETUP COMPLETE` 后关闭窗口。失败时保留报错内容。 + - 看到 `SETUP COMPLETE` 后关闭窗口。失败时查看并保留 `logs/setup-时间.log`;运行过安装器时还会有 `logs/python-installer-时间.log` 及其子日志。返回码 3010 或 1641 表示需要先重启电脑。 2. 双击 `02_build_exe.cmd`。 - 再次核对文件校验值、Python 与依赖版本,然后构建单文件程序。 - 打包期间可能数分钟没有新输出;详细日志在 `build/时间-位数/build.log`。 @@ -42,6 +45,8 @@ | 现象 | 处理 | | --- | --- | +| 旧包可以安装,新包刚开始就提示 SETUP FAILED | 使用这个修复包,保留原来可用的 Python 所在文件夹。先运行 `00_check_environment.cmd` 可查看识别到的 Python 路径;这个检查不安装依赖 | +| Python 已装好但自动没有找到 | 在包根目录新建 `python_path.txt`,仅填一行完整路径,例如 `D:\旧打包包\.python38\python.exe`,再运行第一个脚本。会检查是否为匹配位数的 Python 3.8.10,不需要复制旧的 `.buildenv` | | Python 安装提示需要 SP1、更新或重启 | 先检查 Windows 7 SP1、DLL 加载更新及 UCRT。完成更新并重启,再运行第一个脚本 | | 缺少 api-ms-win-crt、VCRUNTIME140、找不到程序入口点 | 检查系统运行库和更新;若自检提示具体 DLL,保留完整名称及错误信息。不要从陌生网站下载单个 DLL 覆盖系统目录 | | 文件校验失败 | 重新完整解压本包;确认杀毒软件没有隔离文件、源码和 wheels 未被修改 | @@ -57,7 +62,7 @@ Waitress 使用本项目的 **3.0.2+win7.1** 兼容 wheel。HTTP 服务及安全修复代码与官方 3.0.2 保持逐字节一致,仅为命令行入口的名称解析增加 Python 3.8 回退,并修改版本元数据、重算 wheel 校验记录;原许可证保留。这样保留 3.0.1/3.0.2 的修复,没有降到旧的 3.0.0。上游官方支持范围仍为 Python 3.9+,这是本项目单独测试维护的兼容组合,不代表上游承诺 Win7 支持。原始 wheel 和重打包脚本随包提供。 -`PACKAGE_INFO.json` 记录源代码提交与验证范围;`SHA256SUMS.json` 记录本包文件校验值。构建输出的 `BUILD_INFO.json` 记录实际操作系统、Python、位数和 EXE SHA-256。最终以你的服务器上的自检和现场流程验收为准。 +`PACKAGE_INFO.json` 记录源代码提交与验证范围;`SHA256SUMS.json` 记录本包文件校验值。构建输出的 `BUILD_INFO.json` 记录实际操作系统、Python、位数和 EXE SHA-256。最终以你的服务器上的自检和现场流程验收为准。`00_check_environment.cmd` 只检查系统、可复用的 Python 并保存诊断日志,不安装软件或改系统设置。 ## 官方参考 diff --git a/packaging/win7/create_bundle.py b/packaging/win7/create_bundle.py index 6bb7956..7cdfeff 100644 --- a/packaging/win7/create_bundle.py +++ b/packaging/win7/create_bundle.py @@ -35,9 +35,9 @@ def assemble(cache, output): continue if name.endswith('.py') or name.startswith(('po_app/templates/', 'po_app/static/')): copy(REPO/name, name) - for name in ['build.py', 'selftest.py', 'probe_exe.py', 'requirements-win7.txt', 'make_waitress_wheel.py']: + for name in ['build.py', 'selftest.py', 'probe_exe.py', 'requirements-win7.txt', 'make_waitress_wheel.py', 'install_env.ps1']: copy(HERE/name, 'win7/' + name) - for name in ['01_install_env.cmd', '02_build_exe.cmd']: + for name in ['00_check_environment.cmd', '01_install_env.cmd', '02_build_exe.cmd']: (output/name).write_bytes((HERE/name).read_text(encoding='utf-8').replace('\r\n', '\n').replace('\n', '\r\n').encode('ascii')) (output/'README_WIN7.txt').write_text((HERE/'README_WIN7.md').read_text(encoding='utf-8'), encoding='utf-8-sig') copy(REPO/'output/pdf/博特维采购订单系统_v4_使用与升级手册.pdf', 'manual/博特维采购订单系统_v4_使用与升级手册.pdf') @@ -64,6 +64,7 @@ def assemble(cache, output): raise ValueError('Incomplete wheel set: %s, missing %s' % (arch, set(pins)-set(selected))) copy(cache/'waitress-3.0.2-py3-none-any.whl', 'third_party/waitress-3.0.2-py3-none-any.whl') info = {'source_commit': commit, 'created_at': datetime.now().isoformat(timespec='seconds'), + 'setup_revision': '20260917-reuse-installed-python', 'purpose': 'Build v4 on the target Windows 7 SP1 server', 'architectures': ['x86', 'x64'], 'python': '3.8.10', 'windows7_field_verified': False, diff --git a/packaging/win7/install_env.ps1 b/packaging/win7/install_env.ps1 new file mode 100644 index 0000000..dc029f5 --- /dev/null +++ b/packaging/win7/install_env.ps1 @@ -0,0 +1,143 @@ +# Compatible with Windows PowerShell 2.0. Reuse a verified full Python install. +param([switch]$ProbeOnly) + +$ErrorActionPreference = 'Stop' +$kitRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$logDirectory = Join-Path $kitRoot 'logs' +[void][IO.Directory]::CreateDirectory($logDirectory) +$runId = Get-Date -Format 'yyyyMMdd-HHmmss-fff' +$setupLog = Join-Path $logDirectory ('setup-' + $runId + '.log') +$utf8 = New-Object System.Text.UTF8Encoding($true) +[IO.File]::WriteAllText($setupLog, '', $utf8) +$env:PYTHONUTF8 = '1' +$env:PYTHONIOENCODING = 'utf-8' +$env:PYTHONHOME = $null +$env:PYTHONPATH = $null +[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false) +$targetBits = 32 +if ($env:PROCESSOR_ARCHITECTURE -eq 'AMD64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'AMD64') { + $targetBits = 64 +} + +function Write-Log([string]$message) { + Write-Host $message + [IO.File]::AppendAllText($setupLog, $message + [Environment]::NewLine, $utf8) +} + +function Invoke-Logged([string]$executable, [string[]]$arguments) { + # PowerShell 2 treats native stderr as ErrorRecord; preserve it in the log + # without mistaking harmless warnings for a failed process. + $savedPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $global:LASTEXITCODE = $null + & $executable @arguments 2>&1 | ForEach-Object { Write-Log ([string]$_) } + $code = $global:LASTEXITCODE + } finally { + $ErrorActionPreference = $savedPreference + } + if ($null -eq $code) { return 1 } + return $code +} + +function Test-Python([string]$candidate) { + if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) { return $false } + Write-Log ('Checking Python: ' + $candidate) + $probe = 'import sys, struct, venv, ensurepip, ssl, sqlite3; print(sys.version); print(sys.executable); sys.exit(0 if sys.version_info[:3] == (3,8,10) and struct.calcsize(''P'')*8 == ' + $targetBits + ' else 3)' + $code = Invoke-Logged $candidate @('-I', '-X', 'utf8', '-c', $probe) + if ($code -ne 0) { Write-Log ('Python rejected; exit code: ' + $code) } + return ($code -eq 0) +} + +function Find-Python { + $manualFile = Join-Path $kitRoot 'python_path.txt' + if (Test-Path -LiteralPath $manualFile) { + $manualPath = [IO.File]::ReadAllText($manualFile).Trim().Trim('"') + if (Test-Python $manualPath) { return $manualPath } + throw 'python_path.txt must contain the full path to a working Python 3.8.10 of the required architecture.' + } + $candidates = New-Object System.Collections.ArrayList + [void]$candidates.Add((Join-Path $kitRoot '.python38\python.exe')) + # Python's official installer registers the full interpreter, even when + # its files live in an older source kit directory. + foreach ($registryRoot in @('HKCU:\Software\Python\PythonCore', + 'HKCU:\Software\WOW6432Node\Python\PythonCore', + 'HKLM:\Software\Python\PythonCore', + 'HKLM:\Software\WOW6432Node\Python\PythonCore')) { + foreach ($tag in @('3.8', '3.8-32')) { + $keyPath = Join-Path $registryRoot ($tag + '\InstallPath') + try { + if (Test-Path -LiteralPath $keyPath) { + $key = Get-Item -LiteralPath $keyPath + $executable = [string]$key.GetValue('ExecutablePath') + $directory = [string]$key.GetValue('') + if ($executable) { [void]$candidates.Add($executable) } + if ($directory) { [void]$candidates.Add((Join-Path $directory 'python.exe')) } + } + } catch { + Write-Log ('Registry lookup skipped: ' + $keyPath + ' : ' + $_.Exception.Message) + } + } + } + foreach ($candidate in ($candidates | Select-Object -Unique)) { + if (Test-Python $candidate) { return $candidate } + } + return $null +} + +try { + Write-Log ('Setup log: ' + $setupLog) + Write-Log ('Package: ' + $kitRoot) + Write-Log ('Windows: ' + [Environment]::OSVersion.VersionString) + Write-Log ('PowerShell: ' + $PSVersionTable.PSVersion.ToString()) + Write-Log ('Required Python: 3.8.10, ' + $targetBits + '-bit') + $python = Find-Python + if ($ProbeOnly) { + if ($python) { Write-Log ('FOUND reusable Python: ' + $python) } + else { Write-Log 'No working matching Python was found. No installation was attempted.' } + Write-Log 'DIAGNOSTIC COMPLETE. No dependencies or system settings were changed.' + exit 0 + } + if (-not $python) { + $installerName = 'python-3.8.10.exe' + if ($targetBits -eq 64) { $installerName = 'python-3.8.10-amd64.exe' } + $installer = Join-Path $kitRoot ('installers\' + $installerName) + if (-not (Test-Path -LiteralPath $installer)) { throw ('Installer missing: ' + $installer) } + $installerLog = Join-Path $logDirectory ('python-installer-' + $runId + '.log') + $targetDirectory = Join-Path $kitRoot '.python38' + Write-Log ('No reusable Python found. Starting installer: ' + $installer) + Write-Log ('Installer log: ' + $installerLog) + $installerArgs = @('/passive', '/log', ('"' + $installerLog + '"'), + 'InstallAllUsers=0', ('TargetDir="' + $targetDirectory + '"'), + 'Include_launcher=0', 'InstallLauncherAllUsers=0', 'PrependPath=0', + 'Include_doc=0', 'Include_test=0', 'Include_tcltk=0', 'Shortcuts=0', 'AssociateFiles=0') + $process = Start-Process -FilePath $installer -ArgumentList $installerArgs -Wait -PassThru -WindowStyle Hidden + $installerCode = $process.ExitCode + Write-Log ('Python installer exit code: ' + $installerCode) + if ($installerCode -eq 3010 -or $installerCode -eq 1641) { + Write-Log 'Windows requested a restart. Restart the computer and run 01_install_env.cmd again.' + exit $installerCode + } + if ($installerCode -ne 0) { + throw ('Python installer failed with exit code ' + $installerCode + '. See ' + $installerLog) + } + # Maintenance mode may leave Python in its original registered folder. + $python = Find-Python + if (-not $python) { + throw 'Installer returned success but no matching Python was found. Keep logs; do not uninstall the old working environment.' + } + } + Write-Log ('Using existing Python: ' + $python) + Write-Log 'Keep this Python directory while using the build environment.' + $buildScript = Join-Path $kitRoot 'win7\build.py' + $code = Invoke-Logged $python @('-I', '-X', 'utf8', $buildScript, 'setup') + Write-Log ('Environment setup exit code: ' + $code) + if ($code -ne 0) { exit $code } + Write-Log 'SETUP COMPLETE. Run 02_build_exe.cmd next.' + exit 0 +} catch { + Write-Log ('FAILED: ' + $_.Exception.Message) + Write-Log ('Details: ' + $_.ScriptStackTrace) + Write-Log ('Keep the logs folder. Setup log: ' + $setupLog) + exit 1 +} diff --git a/po_app/pdf_export.py b/po_app/pdf_export.py index 24d9645..4d53f62 100644 --- a/po_app/pdf_export.py +++ b/po_app/pdf_export.py @@ -60,7 +60,7 @@ STATUS_WATERMARK = { SEAL_COLOR = colors.HexColor("#C8102E") SEAL_NUMBER = "3205070994140" -PDF_TEMPLATE_VERSION = "po-v4" +PDF_TEMPLATE_VERSION = "po-v5" PDF_SEAL_SIZE = 32 * 1.3 * mm @@ -278,7 +278,7 @@ def build_pdf(order, items, out_path, watermark=None): styles = { "title": ParagraphStyle("title", fontName=FONT_BOLD, fontSize=17, leading=22, alignment=TA_CENTER), - "company": ParagraphStyle("company", fontName=FONT_BOLD, fontSize=12.5, leading=17, + "company": ParagraphStyle("company", fontName=FONT_BOLD, fontSize=20, leading=26, alignment=TA_CENTER), "cell": ParagraphStyle("cell", fontName=FONT_BODY, fontSize=8.6, leading=11.5, alignment=TA_LEFT), diff --git a/tests/test_workflow.py b/tests/test_workflow.py index ad8014e..cfc79c4 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -86,7 +86,7 @@ def test_full_flow_and_frozen_pdf(app, clients, kind, n): v = db.query(OrderVersion).filter_by(order_id=oid, revision=1).one() assert v.pdf_sha256 == hashlib.sha256(response.data).hexdigest() assert len(json.loads(v.content_json)['items']) == n - assert v.template_version == 'po-v4' + assert v.template_version == 'po-v5' assert save(app, clients['maker'], oid).status_code == 403 assert clients['maker'].get('/orders/%s/edit' % oid).status_code == 302 assert clients['maker'].get('/orders/%s/versions/1' % oid).status_code == 200