Compare commits

42 Commits
Author SHA1 Message Date
jayus 575b55ebc4 fix(service): 更新访问密钥配置
- 替换原有的访问密钥 ID 和密钥秘密
- 确保服务使用最新的认证信息
2025-08-21 20:35:04 +08:00
liuxiaoqing 88e13624a2 perf(webapp): 优化代码并更新样式 2025-08-20 18:22:25 +08:00
liuxiaoqing 676502599e perf(webapp): 优化代码并更新样式 2025-08-20 18:18:41 +08:00
liuxiaoqing c68ff21be3 perf(webapp): 优化代码并更新样式
- 更新 Vite生成的 JS 和 CSS 文件引用- 在 manage.vue 中添加 rowKey 属性以优化表格性能
2025-08-20 18:15:58 +08:00
liuxiaoqing 2942a8e029 refactor(webapp): 优化 Check 组件布局并更新阿里云 OCR 配置
- 调整 Check 组件中 a-col 的属性,使布局在不同屏幕尺寸下更加适应
- 更新阿里云 OCR 服务的 access_key_id 和 access_key_secret
- 注释掉旧的 access_key_id 和 access_key_secret,以便备份
2025-08-20 18:02:25 +08:00
liuxiaoqing 11530baefb feat(database): 将数据库从 SQLite 迁移到 MySQL
- 修改了 DBService 类,增加了 MySQL 数据库连接配置
- 重新定义了表结构,适应 MySQL 数据库- 更新了相关的 SQL 查询语句
- 增加了创建数据库的逻辑
- 优化了表创建语句,使用 InnoDB 引擎和 UTF-8 字符集
2025-08-20 15:11:35 +08:00
liuxiaoqing 31c2394e8a feat(invoice): 更新发票状态- 在更新发票信息时,增加了状态字段
- 将发票状态设置为 'no',可能表示未通过审核
2025-08-20 11:46:45 +08:00
liuxiaoqing 9649b6e465 更新前端 2025-08-20 11:38:18 +08:00
liuxiaoqing c87546ca0d 更新前端 2025-08-20 11:38:13 +08:00
liuxiaoqing aef0abbecf fix(manage): 移除备注信息中的硬编码文本
移除了 manage.vue 文件中 Remarks 组件的 <a-typography-paragraph> 标签内硬编码的 "asdsad" 文本,确保备注信息只显示 record.desc 的内容。
2025-08-20 11:37:37 +08:00
liuxiaoqing c5818b7ec4 更新前端 2025-08-20 11:32:27 +08:00
liuxiaoqing 80b0c29ada 更新前端 2025-08-20 11:28:09 +08:00
liuxiaoqing c082b3070e feat(manage): 添加订单审核状态全部选项
- 在订单审核状态
2025-08-20 11:26:38 +08:00
liuxiaoqing 8c0887e705 fix(alibabacloud_sample): 修复数据查询逻辑
- 完善了 time_out 为 'all' 时的查询条件
-增加了 verify_status 为 'all' 时的通用查询条件
- 优化了 SQL 查询语句的生成逻辑
2025-08-20 11:24:02 +08:00
liuxiaoqing e133b6b99a style:优化 SQL 更新语句的格式
- 在 SET 子句中使用逗号和空格分隔列
- 在 WHERE子句前添加逗号和空格
- 使 SQL 语句的格式更加规范和易读
2025-08-20 11:13:58 +08:00
liuxiaoqing 90f561a0c9 refactor(alibabacloud_sample): 修改文件上传路径
- 将 '/uploads/<path:filename>'路由修改为 '/data/uploads/<path:filename>'
- 这个改动使得上传文件的 URL路径更加明确,方便管理和访问
2025-08-20 10:47:09 +08:00
liuxiaoqing eb5f5e37b5 refactor: 修改上传文件夹路径
- 将上传文件夹路径从 'uploads' 修改为 '/data/uploads'
- 此修改可能是为了适应服务器环境或提高文件管理的灵活性
2025-08-20 10:44:20 +08:00
liuxiaoqing 2cb3429dcd fix(file): 优化文件插入逻辑
- 修改文件数据插入流程,先插入再查询
- 优化代码结构,提高可读性和维护性
2025-08-20 10:37:26 +08:00
liuxiaoqing 182c7a3873 refactor: 修改 Flask 端口映射
- 将 Docker Compose 文件中的端口映射从 "5555:5555" 修改为 "8555:5555"
- 这个修改可能是为了与其他服务或主机端口冲突较少,提高系统兼容性
2025-08-20 10:33:52 +08:00
liuxiaoqing 9a004a3133 build(docker): 更新 Docker 配置并修改数据库文件路径
- 修改数据库文件路径为 /data/verify.db
- 在 docker-compose.yml 中添加数据卷映射
- 更新 Dockerfile,包含代码克隆、依赖安装和启动命令
2025-08-20 10:29:18 +08:00
jayus b122567c12 build(dockerfile): 移除多余的 COPY 指令
- 删除了将当前目录内容复制到 /app 目录的 COPY指令
- 优化了 Dockerfile 构建流程,减少镜像大小
2025-08-19 23:23:29 +08:00
jayus f62672d779 build: 更新 Dockerfile 并调整项目资源
- 移除 Dockerfile 中的 CMD 指令
- 更新模板文件中的资源链接
- 修正检查组件的当前步骤初始值
2025-08-19 23:21:37 +08:00
jayus 40367a4e12 refactor(webapp): 更新 API地址并优化代码
-将本地 API 地址替换为阿里云服务器地址
- 更新 HTML 文件中的脚本和样式链接- 优化 manage.vue 文件中的导入路径和类型定义
- 移除 report.vue 中的冗余 alert
2025-08-19 22:49:04 +08:00
jayus 31404cf225 refactor(webapp): 优化发票验证流程和界面
- 修改验证流程初始步骤为第3步- 更新验证结果展示方式,使用 Ant Design Result 组件
-优化上传页面样式,增加顶部间距
- 调整验证接口返回格式,统一错误处理
2025-08-19 22:42:38 +08:00
jayus 8c7422dc68 feat(invoice): 增加发票备注和附件功能
- 新增发票备注和附件上传功能- 实现发票列表按验证时间筛选
- 优化发票列表展示,增加验证时间筛选和关键字搜索
- 修复附件上传相关接口
2025-08-19 22:22:44 +08:00
liuxiaoqing 795620b0f8 feat(invoice): 增加发票入账功能
- 新增发票入账相关的前端界面和后端接口
- 实现发票列表的展示、筛选和分页功能
- 添加发票入账状态的切换和备注功能
- 优化发票列表的展示样式,增加扩展行显示备注信息
2025-08-19 17:35:34 +08:00
liuxiaoqing 1e24287327 refactor(webapp): 重构发票验证功能
- 修改基础 URL为本地地址
- 优化发票验证和列表查询逻辑
- 更新数据库结构和查询语句
- 改进前端发票管理页面,增加验证结果展示
- 修复后端发票列表接口,支持 JSON 请求
2025-08-19 13:52:25 +08:00
jayus c22a509633 refactor(webapp): 更新 API地址并添加测试请求
- 修改了 baseurl.ts 中的 API 地址,使用本地测试地址
- 在 manage.vue 中添加了测试请求函数 getList,用于获取发票列表
2025-08-18 22:19:28 +08:00
liuxiaoqing 1170d5238a refactor(webapp): 优化发票查验组件布局和功能
-调整发票代码输入框位置,移到发票类型下方
-修复文件上传服务中的错误提示信息
- 优化文件哈希计算方法,改为静态方法
- 更新后端接口命名,使用下划线风格
- 调整发票列表展示字段,将销方信息移到上方
2025-08-18 17:54:29 +08:00
liuxiaoqing 813a317ff6 feat(invoice): 增加发票验证功能
- 新增发票验证相关接口和页面
- 实现发票复验功能
- 添加发票列表和验证日志查询接口
- 更新发票状态和描述信息
- 优化文件上传逻辑,支持文件重复检测
2025-08-18 17:45:36 +08:00
liuxiaoqing ceb0b33656 asd 2025-08-18 14:40:32 +08:00
liuxiaoqing 439daf3b33 refactor(webapp): 更新 API基础地址并更新示例项目资源链接
- 将 baseurl.ts 中的 API 地址从本地地址修改为阿里云服务器地址
- 更新 alibabacloud_sample 项目中的 HTML 文件引用
2025-08-18 14:39:23 +08:00
liuxiaoqing 435e56b02a build(vite): 清空构建输出目录
- 在 vite.config.ts 中添加 emptyOutDir 配置项
- 确保每次构建时清空输出目录,避免冗余文件累积
2025-08-18 14:25:18 +08:00
liuxiaoqing 45ab672fed build: 更新 Docker Compose 文件版本
- 将 Docker Compose 文件版本从 '1.0' 修改为 '3.8'
-此更新提高了项目的可扩展性和管理能力
2025-08-18 14:19:59 +08:00
liuxiaoqing 148957a8ae build: 更新 Docker Compose 文件版本
- 将 Docker Compose 文件版本从 '1.0' 修改为 '3.8'
-此更新提高了项目的可扩展性和管理能力
2025-08-18 14:19:39 +08:00
liuxiaoqing 83fe44d7be build(docker): 更新 Docker Compose 配置和 Dockerfile
- 新增 docker-compose.yml 文件,配置 Python 应用服务
- 更新 Dockerfile:
  - 调整文件结构,先复制再安装依赖
  - 移除冗余的 COPY 指令  - 更新 CMD 指令,简化路径
  - 注释掉端口映射,可根据需要手动配置
2025-08-18 14:01:14 +08:00
liuxiaoqing c045214969 build(docker): 添加 Dockerfile并更新项目结构
- 新增 Dockerfile 用于构建和运行项目
- 更新 index.html 文件,替换静态资源链接
- 将 requirements.txt 文件移至项目根目录- 移除 alibabacloud_sample 依赖项
- 添加 setuptools依赖项
2025-08-18 13:53:27 +08:00
liuxiaoqing 7b1910a7a7 feat(alibabacloud_sample): 在 DBService 中添加 desc 字段
- 在 create_table_sql 中添加 desc 字段,类型为 text
-此修改用于扩展数据库表结构,增加描述信息字段
2025-08-18 11:37:56 +08:00
liuxiaoqing abf030baa6 build(alibabacloud_sample): 更新 requirements.txt 中的版本依赖
-将 alibabacloud_sample 的版本从 ==1.0.0 修改为 >=1.0.0
- 这个修改允许使用 1.0.0 及更高版本的 alibabacloud_sample,增加了项目的灵活性
2025-08-18 11:07:07 +08:00
jayus 7466af0368 build(alibabacloud_sample): 添加项目依赖文件
- 新增 requirements.txt 文件,列出 alibabacloud_sample项目的依赖项
- 包含 Alibaba Cloud 相关库、Flask 框架和其他辅助库
2025-08-17 22:45:09 +08:00
jayus cc867c471e refactor(webapp): 更新项目配置和功能
- 修改基础 URL为本地开发环境
- 更新数据库文件路径
- 调整发票报销流程,增加"完成"按钮
- 优化发票列表页面,添加搜索功能
- 新增发票复核页面
- 更新文件上传逻辑,使用动态 URL
2025-08-17 22:39:59 +08:00
liuxiaoqing 0c4e94b5af feat(webapp): 初始化项目结构和功能
- 创建项目目录和文件结构
- 添加基础的 Vue组件和布局
- 实现简单的路由和页面切换
- 添加数据库操作相关代码
- 引入 dateutil 库并添加相关测试
2025-08-17 13:24:36 +08:00
60 changed files with 9945 additions and 173 deletions
+4 -172
View File
@@ -1,173 +1,5 @@
# ---> Python
# Byte-compiled / optimized / DLL files
runtime/
.idea/
.vscode/
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> Vue
# gitignore template for Vue.js projects
#
# Recommended template: Node.gitignore
# TODO: where does this rule come from?
docs/_book
# TODO: where does this rule come from?
test/
.pytest_cache/
+39 -1
View File
@@ -1,2 +1,40 @@
# VerifVATInvoice
# 发票核验完整工程示例
该项目为VerifyVATInvoice的完整工程示例。
该示例**无法在线调试**,如需调试可下载到本地后替换 [AK](https://usercenter.console.aliyun.com/#/manage/ak) 以及参数后进行调试。
## 运行条件
- 下载并解压需要语言的代码;
- 在阿里云帐户中获取您的 [凭证](https://usercenter.console.aliyun.com/#/manage/ak) 并通过它替换下载后代码中的 ACCESS_KEY_ID 以及 ACCESS_KEY_SECRET;
- 执行对应语言的构建及运行语句
## 执行步骤
下载的代码包,在根据自己需要更改代码中的参数和 AK 以后,可以在**解压代码所在目录下**按如下的步骤执行:
- *Python 版本要求 Python3*
```sh
python3 setup.py install && python ./alibabacloud_sample/sample.py
```
## 使用的 API
- VerifyVATInvoice:发票核验接口支持包括:增值税专用发票、增值税普通发票(折叠票)、增值税普通发票(卷票)、增值税电子普通发票(含收费公路通行费增值税电子普通发票)、机动车销售统一发票、二手车销售统一发票多种类型发票核验。您可以通过输入发票的关键验证字段,返回真实的票面信息,包括发票类型、发票代码、发票号码、作废标志、开票日期、购方税号及其他发票信息等。当天开具发票当日可查验(T+0)。注意:可能有几小时到十几小时的延迟。 更多信息可参考:[文档](https://next.api.aliyun.com/document/ocr-api/2021-07-07/VerifyVATInvoice)
## API 返回示例
*实际输出结构可能稍有不同,属于正常返回;下列输出值仅作为参考,以实际调用为准*
- JSON 格式
```js
{
"RequestId": "43A29C77-405E-4CC0-BC55-EE694AD00655",
"Data": "{\n \"code\": \"001\",\n \"data\": {\n \"afterTaxCode\": \"\",\n \"allTax\": \"9.67\",\n \"allValoremTax\": \"332.00\",\n \"blueInvoiceCode\": \"\",\n \"blueInvoiceNo\": \"\",\n \"brandVersion\": \"\",\n \"businessUnit\": \"\",\n \"businessUnitTaxNo\": \"\",\n \"busmessUnitAddress\": \"\",\n \"busmessUnitBankAndAccount\": \"\",\n \"busmessUnitPhone\": \"\",\n \"carPrice\": \"\",\n \"carType\": \"\",\n \"carTypeAndNumber\": \"\",\n \"carframeCode\": \"\",\n \"carrierName\": \"\",\n \"carrierTaxNo\": \"\",\n \"checkCode\": \"07122942791187744XXXX\",\n \"code\": \"\",\n \"consignorName\": \"\",\n \"consignorTaxNo\": \"\",\n \"cyjgxx\": \"查验成功发票一致\",\n \"detailList\": [\n {\n \"allTax\": \"9.67\",\n \"detailAmount\": \"322.33\",\n \"detailNo\": \"1\",\n \"expenseItem\": \"\",\n \"goodsName\": \"*餐饮服务*餐费\",\n \"netValue\": \"322.330097\",\n \"num\": \"1\",\n \"plate_no\": \"\",\n \"rowNo\": \"1\",\n \"standard\": \"\",\n \"taxClassifyCode\": \"0\",\n \"taxDetailAmount\": \"\",\n \"taxRate\": \"3\",\n \"taxUnitPrice\": \"\",\n \"trafficDateEnd\": \"\",\n \"trafficDateStart\": \"\",\n \"type\": \"\",\n \"unit\": \"\"\n }\n ],\n \"draweeName\": \"\",\n \"draweeTaxNo\": \"\",\n \"engineCode\": \"\",\n \"idCard\": \"\",\n \"importLicense\": \"\",\n \"inspectionAmount\": \"\",\n \"inspectionNumber\": \"\",\n \"invalidMark\": \"N\",\n \"invoiceCode\": \"01100180XXXX\",\n \"invoiceDate\": \"2018XXXX\",\n \"invoiceMoney\": \"322.XX\",\n \"invoiceNumber\": \"3531XXXX\",\n \"invoiceType\": \"10\",\n \"lemonMarket\": \"\",\n \"lemonMarketAddress\": \"\",\n \"lemonMarketBankAndAccount\": \"\",\n \"lemonMarketPhone\": \"\",\n \"lemonMarketTaxNo\": \"\",\n \"licenseCode\": \"\",\n \"licensePlate\": \"\",\n \"limitAmount\": \"\",\n \"machineCode\": \"49992273XXXX\",\n \"note\": \"机器编号:49992273XXXX\",\n \"producingArea\": \"\",\n \"purchaserAddressOrPhone\": \"杭州余杭区XXXX0571-8502XXXX\",\n \"purchaserBankAndNumber\": \"招商银行杭州高新支行571906593XXXXXX\",\n \"purchaserName\": \"XXXX软件有限公司\",\n \"purchaserPhone\": \"\",\n \"purchaserTaxpayerNumber\": \"913301007682XXXXXX\",\n \"purchaserUnitOrIndividual\": \"\",\n \"purchaserUnitOrIndividualAddress\": \"\",\n \"purchaserUnitcodeOrIdNo\": \"\",\n \"receiveName\": \"\",\n \"receiveTaxNo\": \"\",\n \"registrationNo\": \"\",\n \"salerAddress\": \"\",\n \"salerAddressOrPhone\": \"北京市海淀区XXX8211XXXX\",\n \"salerBankAccount\": \"中国银行海淀支行345456XXXXXX\",\n \"salerBankAndNumber\": \"中国银行海淀支行345456XXXXXX\",\n \"salerBankName\": \"\",\n \"salerName\": \"北京市XXX酒家\",\n \"salerPhone\": \"\",\n \"salerTaxpayerNumber\": \"911101081020XXXXXX\",\n \"sellerPhone\": \"\",\n \"sellerUnitCodeOrIdno\": \"\",\n \"sellerUnitOrIndividual\": \"北京市XXX酒家\",\n \"sellerUnitOrIndividualAddress\": \"\",\n \"taxDiskNumber\": \"\",\n \"taxRate\": \"\",\n \"taxUnitCode\": \"\",\n \"taxUnitName\": \"\",\n \"throughAddress\": \"\",\n \"trafficFeeFlag\": \"\",\n \"transferredVehicleOffice\": \"\",\n \"transportGoodsInfo\": \"\",\n \"unit\": \"\",\n \"vehicleTonnage\": \"\",\n \"zeroTaxRateFlag\": \"\"\n },\n \"msg\": \"成功\"\n}"
}
```
+79
View File
@@ -0,0 +1,79 @@
Metadata-Version: 2.4
Name: alibabacloud_sample
Version: 1.0.0
Summary: Alibaba Cloud SDK Code Sample Library for Python
Home-page: https://github.com/aliyun/alibabacloud-sdk
Author:
Author-email:
License: Apache License 2.0
Keywords: alibabacloud,sample
Platform: any
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: Software Development
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Requires-Dist: alibabacloud_ocr-api20210707<4.0.0,>=3.1.3
Requires-Dist: alibabacloud_tea_openapi<1.0.0,>=0.3.15
Requires-Dist: alibabacloud_tea_console<1.0.0,>=0.0.1
Requires-Dist: alibabacloud_tea_util<1.0.0,>=0.3.13
Requires-Dist: alibabacloud_credentials<2.0.0,>=1.0.2
Requires-Dist: flask
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license
Dynamic: platform
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary
# 发票核验完整工程示例
该项目为VerifyVATInvoice的完整工程示例。
该示例**无法在线调试**,如需调试可下载到本地后替换 [AK](https://usercenter.console.aliyun.com/#/manage/ak) 以及参数后进行调试。
## 运行条件
- 下载并解压需要语言的代码;
- 在阿里云帐户中获取您的 [凭证](https://usercenter.console.aliyun.com/#/manage/ak) 并通过它替换下载后代码中的 ACCESS_KEY_ID 以及 ACCESS_KEY_SECRET;
- 执行对应语言的构建及运行语句
## 执行步骤
下载的代码包,在根据自己需要更改代码中的参数和 AK 以后,可以在**解压代码所在目录下**按如下的步骤执行:
- *Python 版本要求 Python3*
```sh
python3 setup.py install && python ./alibabacloud_sample/sample.py
```
## 使用的 API
- VerifyVATInvoice:发票核验接口支持包括:增值税专用发票、增值税普通发票(折叠票)、增值税普通发票(卷票)、增值税电子普通发票(含收费公路通行费增值税电子普通发票)、机动车销售统一发票、二手车销售统一发票多种类型发票核验。您可以通过输入发票的关键验证字段,返回真实的票面信息,包括发票类型、发票代码、发票号码、作废标志、开票日期、购方税号及其他发票信息等。当天开具发票当日可查验(T+0)。注意:可能有几小时到十几小时的延迟。 更多信息可参考:[文档](https://next.api.aliyun.com/document/ocr-api/2021-07-07/VerifyVATInvoice)
## API 返回示例
*实际输出结构可能稍有不同,属于正常返回;下列输出值仅作为参考,以实际调用为准*
- JSON 格式
```js
{
"RequestId": "43A29C77-405E-4CC0-BC55-EE694AD00655",
"Data": "{\n \"code\": \"001\",\n \"data\": {\n \"afterTaxCode\": \"\",\n \"allTax\": \"9.67\",\n \"allValoremTax\": \"332.00\",\n \"blueInvoiceCode\": \"\",\n \"blueInvoiceNo\": \"\",\n \"brandVersion\": \"\",\n \"businessUnit\": \"\",\n \"businessUnitTaxNo\": \"\",\n \"busmessUnitAddress\": \"\",\n \"busmessUnitBankAndAccount\": \"\",\n \"busmessUnitPhone\": \"\",\n \"carPrice\": \"\",\n \"carType\": \"\",\n \"carTypeAndNumber\": \"\",\n \"carframeCode\": \"\",\n \"carrierName\": \"\",\n \"carrierTaxNo\": \"\",\n \"checkCode\": \"07122942791187744XXXX\",\n \"code\": \"\",\n \"consignorName\": \"\",\n \"consignorTaxNo\": \"\",\n \"cyjgxx\": \"查验成功发票一致\",\n \"detailList\": [\n {\n \"allTax\": \"9.67\",\n \"detailAmount\": \"322.33\",\n \"detailNo\": \"1\",\n \"expenseItem\": \"\",\n \"goodsName\": \"*餐饮服务*餐费\",\n \"netValue\": \"322.330097\",\n \"num\": \"1\",\n \"plate_no\": \"\",\n \"rowNo\": \"1\",\n \"standard\": \"\",\n \"taxClassifyCode\": \"0\",\n \"taxDetailAmount\": \"\",\n \"taxRate\": \"3\",\n \"taxUnitPrice\": \"\",\n \"trafficDateEnd\": \"\",\n \"trafficDateStart\": \"\",\n \"type\": \"\",\n \"unit\": \"\"\n }\n ],\n \"draweeName\": \"\",\n \"draweeTaxNo\": \"\",\n \"engineCode\": \"\",\n \"idCard\": \"\",\n \"importLicense\": \"\",\n \"inspectionAmount\": \"\",\n \"inspectionNumber\": \"\",\n \"invalidMark\": \"N\",\n \"invoiceCode\": \"01100180XXXX\",\n \"invoiceDate\": \"2018XXXX\",\n \"invoiceMoney\": \"322.XX\",\n \"invoiceNumber\": \"3531XXXX\",\n \"invoiceType\": \"10\",\n \"lemonMarket\": \"\",\n \"lemonMarketAddress\": \"\",\n \"lemonMarketBankAndAccount\": \"\",\n \"lemonMarketPhone\": \"\",\n \"lemonMarketTaxNo\": \"\",\n \"licenseCode\": \"\",\n \"licensePlate\": \"\",\n \"limitAmount\": \"\",\n \"machineCode\": \"49992273XXXX\",\n \"note\": \"机器编号:49992273XXXX\",\n \"producingArea\": \"\",\n \"purchaserAddressOrPhone\": \"杭州余杭区XXXX0571-8502XXXX\",\n \"purchaserBankAndNumber\": \"招商银行杭州高新支行571906593XXXXXX\",\n \"purchaserName\": \"XXXX软件有限公司\",\n \"purchaserPhone\": \"\",\n \"purchaserTaxpayerNumber\": \"913301007682XXXXXX\",\n \"purchaserUnitOrIndividual\": \"\",\n \"purchaserUnitOrIndividualAddress\": \"\",\n \"purchaserUnitcodeOrIdNo\": \"\",\n \"receiveName\": \"\",\n \"receiveTaxNo\": \"\",\n \"registrationNo\": \"\",\n \"salerAddress\": \"\",\n \"salerAddressOrPhone\": \"北京市海淀区XXX8211XXXX\",\n \"salerBankAccount\": \"中国银行海淀支行345456XXXXXX\",\n \"salerBankAndNumber\": \"中国银行海淀支行345456XXXXXX\",\n \"salerBankName\": \"\",\n \"salerName\": \"北京市XXX酒家\",\n \"salerPhone\": \"\",\n \"salerTaxpayerNumber\": \"911101081020XXXXXX\",\n \"sellerPhone\": \"\",\n \"sellerUnitCodeOrIdno\": \"\",\n \"sellerUnitOrIndividual\": \"北京市XXX酒家\",\n \"sellerUnitOrIndividualAddress\": \"\",\n \"taxDiskNumber\": \"\",\n \"taxRate\": \"\",\n \"taxUnitCode\": \"\",\n \"taxUnitName\": \"\",\n \"throughAddress\": \"\",\n \"trafficFeeFlag\": \"\",\n \"transferredVehicleOffice\": \"\",\n \"transportGoodsInfo\": \"\",\n \"unit\": \"\",\n \"vehicleTonnage\": \"\",\n \"zeroTaxRateFlag\": \"\"\n },\n \"msg\": \"成功\"\n}"
}
```
+13
View File
@@ -0,0 +1,13 @@
README.md
setup.py
alibabacloud_sample/__init__.py
alibabacloud_sample/dataservice.py
alibabacloud_sample/dbservice.py
alibabacloud_sample/fileservice.py
alibabacloud_sample/main.py
alibabacloud_sample/service.py
alibabacloud_sample.egg-info/PKG-INFO
alibabacloud_sample.egg-info/SOURCES.txt
alibabacloud_sample.egg-info/dependency_links.txt
alibabacloud_sample.egg-info/requires.txt
alibabacloud_sample.egg-info/top_level.txt
@@ -0,0 +1 @@
@@ -0,0 +1,6 @@
alibabacloud_ocr-api20210707<4.0.0,>=3.1.3
alibabacloud_tea_openapi<1.0.0,>=0.3.15
alibabacloud_tea_console<1.0.0,>=0.0.1
alibabacloud_tea_util<1.0.0,>=0.3.13
alibabacloud_credentials<2.0.0,>=1.0.2
flask
@@ -0,0 +1 @@
alibabacloud_sample
+1
View File
@@ -0,0 +1 @@
__version__ = "1.0.0"
+273
View File
@@ -0,0 +1,273 @@
# 数据管理服务类
from datetime import datetime
from typing import Any
from dateutil import parser
from alibabacloud_sample.dbservice import DBService
class DataService:
conn = None
dbservice = DBService()
def __init__(self):
self.conn = self.dbservice.get_conn()
# 插入文件记录
def insert_file_data(self, file_name: str, file_path: str, file_size: int, file_type: str,file_time: str, file_hash: str):
conn = self.dbservice.get_conn()
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO file_data(file_name,file_path,file_size,file_type,file_time,file_hash) values(%s,%s,%s,%s,%s,%s)",
(file_name, file_path, file_size, file_type, file_time, file_hash))
conn.commit()
except Exception as e:
print(e)
conn.rollback()
cursor.execute("select file_name,file_path,file_size,file_type,file_time,file_hash from file_data where `file_hash` = %s", (file_hash,))
rows = cursor.fetchall()
data = {}
for i in range(len(rows[0])):
data[cursor.description[i][0]] = rows[0][i]
return data
return file_path
# 根据文件路径获取hash
def get_invoice(self, file_path: str = None,invice_id: int = None) -> dict[Any, Any]:
conn = self.dbservice.get_conn()
cursor = conn.cursor()
sql = """
select id, checkCode, drawer, formType, invoiceAmountPreTax, invoiceCode,
invoiceDate, invoiceNumber, invoiceTax, invoiceType, machineCode,
passwordArea, printedInvoiceCode, printedInvoiceNumber,
purchaserBankAccountInfo, purchaserContactInfo, purchaserName,
purchaserTaxNumber, recipient, remarks, reviewer,
sellerBankAccountInfo, sellerContactInfo, sellerName, sellerTaxNumber,
specialTag, title, totalAmount, totalAmountInWords,file_path,verify_time,
verify_time,verify_status,`desc`,desc_files,status
from invoice
"""
if invice_id:
sql += " where id = '"+str(invice_id)+"'"
cursor.execute(sql)
else:
sql += " where file_path = '"+file_path+"'"
cursor.execute(sql)
rows = cursor.fetchall()
data = {}
if len(rows) > 0:
for i in range(len(rows[0])):
data[cursor.description[i][0]] = rows[0][i]
return data
# 根据文件hash获取文件
def get_file(self, file_hash: str,file_size: int) -> dict[Any, Any] | None:
conn = self.dbservice.get_conn()
cursor = conn.cursor()
cursor.execute("""
select file_name,file_path,file_size,file_type,file_time,file_hash
from file_data where `file_hash` = %s and file_size = %s
"""
, (file_hash,file_size,))
rows = cursor.fetchall()
if len(rows) > 0:
data = {}
for i in range(len(rows[0])):
data[cursor.description[i][0]] = rows[0][i]
return data
return None
# 插入发票记录
def insert_invoice_log(self, data: dict, file_path: str):
conn = self.dbservice.get_conn()
cursor = conn.cursor()
# 插入主表数据
invoice_insert = """
INSERT INTO invoice (
checkCode, drawer, formType, invoiceAmountPreTax, invoiceCode,
invoiceDate, invoiceNumber, invoiceTax, invoiceType, machineCode,
passwordArea, printedInvoiceCode, printedInvoiceNumber,
purchaserBankAccountInfo, purchaserContactInfo, purchaserName,
purchaserTaxNumber, recipient, remarks, reviewer,
sellerBankAccountInfo, sellerContactInfo, sellerName, sellerTaxNumber,
specialTag, title, totalAmount, totalAmountInWords,file_path
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
invoice_values = (
data.get('checkCode', ''),
data.get('drawer', ''),
data.get('formType', ''),
float(data.get('invoiceAmountPreTax', 0.0)),
data.get('invoiceCode', ''),
data.get('invoiceDate', ''),
data.get('invoiceNumber', ''),
float(data.get('invoiceTax', 0.0)),
data.get('invoiceType', ''),
data.get('machineCode', ''),
data.get('passwordArea', ''),
data.get('printedInvoiceCode', ''),
data.get('printedInvoiceNumber', ''),
data.get('purchaserBankAccountInfo', ''),
data.get('purchaserContactInfo', ''),
data.get('purchaserName', ''),
data.get('purchaserTaxNumber', ''),
data.get('recipient', ''),
data.get('remarks', ''),
data.get('reviewer', ''),
data.get('sellerBankAccountInfo', ''),
data.get('sellerContactInfo', ''),
data.get('sellerName', ''),
data.get('sellerTaxNumber', ''),
data.get('specialTag', ''),
data.get('title', ''),
float(data.get('totalAmount', 0.0)),
data.get('totalAmountInWords', ''),
file_path
)
cursor.execute(invoice_insert, invoice_values)
conn.commit()
invoice_id = cursor.lastrowid
return invoice_id
# 更新发票状态
def update_invoice_status(self, invoice_id: int, verify_status: str,inspectionAmount :str ):
conn = self.dbservice.get_conn()
cursor = conn.cursor()
cursor.execute(
"UPDATE invoice SET verify_time = %s, "
"verify_status = %s, "
"inspectionAmount = %s, "
"status = 'no'"
"WHERE id = %s",
(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), verify_status,inspectionAmount, invoice_id))
conn.commit()
def update_invoice_desc(self, invoice_id: int|str, desc: str, desc_file: str,status: str) -> str:
conn = self.dbservice.get_conn()
cursor = conn.cursor()
cursor.execute(
"UPDATE invoice SET "
"`desc` = %s,"
"desc_files = %s,"
"status = %s "
"WHERE id = %s",
(desc, desc_file,status, invoice_id))
conn.commit()
return "success"
# 查询验证记录
def get_verify_log(self, file_path: str) -> dict[Any, Any]|None:
conn = self.dbservice.get_conn()
cursor = conn.cursor()
cursor.execute("""
select inspectionAmount,cyjgxx,verify_time,file_path from verify_log where file_path = %s order by verify_time desc limit 1
"""
, (file_path,))
rows = cursor.fetchall()
if len(rows) > 0:
data = {}
for i in range(len(rows[0])):
data[cursor.description[i][0]] = rows[0][i]
return data
return None
# 插入验证记录
def insert_verify_log(self, inspection_amount: str, cyjgxx: str, file_path: str):
conn = self.dbservice.get_conn()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO verify_log(inspectionAmount,cyjgxx,verify_time,file_path) values(%s,%s,%s,%s)",
(inspection_amount,cyjgxx,parser.parse(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),file_path))
conn.commit()
# 查询验证记录
def get_verify_log_list(self, file_path: str) -> list[dict[Any, Any]]:
conn = self.dbservice.get_conn()
cursor = conn.cursor()
cursor.execute("""
select inspectionAmount,cyjgxx,verify_time,file_path from verify_log where file_path = %s
"""
, (file_path,))
rows = cursor.fetchall()
data = []
for row in rows:
data.append({
'inspectionAmount': row[0],
'cyjgxx': row[1],
'verify_time': row[2],
'file_path': row[3]
})
return data
# 查询发票列表
def get_invoice_list(self, value: str, verify_status: str, time_out = 'all' ,start_date: datetime = None, end_date: datetime = None) -> list[dict[Any, Any]]:
conn = self.dbservice.get_conn()
cursor = conn.cursor()
sql = """
select id, checkCode, drawer, formType, invoiceAmountPreTax, invoiceCode,
invoiceDate, invoiceNumber, invoiceTax, invoiceType, machineCode,
passwordArea, printedInvoiceCode, printedInvoiceNumber,
purchaserBankAccountInfo, purchaserContactInfo, purchaserName,
purchaserTaxNumber, recipient, remarks, reviewer,
sellerBankAccountInfo, sellerContactInfo, sellerName, sellerTaxNumber,
specialTag, title, totalAmount, totalAmountInWords,file_path,verify_time,verify_status,inspectionAmount,`desc`,desc_files,status
from invoice where
"""
if time_out != 'all':
sql += " date('now', '-"+time_out+" day') >= date(verify_time) and verify_status == 'success' "
elif verify_status != 'all':
sql += " verify_status = '" + verify_status+"'"
else:
sql += " 1 = 1 "
if start_date is not None:
sql += " and verify_time >= '" + start_date + "'"
if end_date is not None:
sql += " and verify_time <= '" + end_date + "'"
if value is not None and value != '':
sql += " and ( "
sql += "file_path like '%" + value + "%' or "
sql += "checkCode like '%" + value + "%' or "
sql += "drawer like '%" + value + "%' or "
sql += "formType like '%" + value + "%' or "
sql += "invoiceAmountPreTax like '%" + value + "%' or "
sql += "invoiceCode like '%" + value + "%' or "
sql += "invoiceDate like '%" + value + "%' or "
sql += "invoiceNumber like '%" + value + "%' or "
sql += "invoiceTax like '%" + value + "%' or "
sql += "invoiceType like '%" + value + "%' or "
sql += "machineCode like '%" + value + "%' or "
sql += "passwordArea like '%" + value + "%' or "
sql += "printedInvoiceCode like '%" + value + "%' or "
sql += "printedInvoiceNumber like '%" + value + "%' or "
sql += "purchaserBankAccountInfo like '%" + value + "%' or "
sql += "purchaserContactInfo like '%" + value + "%' or "
sql += "purchaserName like '%" + value + "%' or "
sql += "purchaserTaxNumber like '%" + value + "%' or "
sql += "recipient like '%" + value + "%' or "
sql += "remarks like '%" + value + "%' or "
sql += "reviewer like '%" + value + "%' or "
sql += "sellerBankAccountInfo like '%" + value + "%' or "
sql += "sellerContactInfo like '%" + value + "%' or "
sql += "sellerName like '%" + value + "%' or "
sql += "sellerTaxNumber like '%" + value + "%' or "
sql += "specialTag like '%" + value + "%' or "
sql += "title like '%" + value + "%' or "
sql += "totalAmount like '%" + value + "%' or "
sql += "totalAmountInWords like '%" + value + "%' or "
sql += "`desc` like '%" + value + "%' or "
sql += "desc_files like '%" + value + "%' "
sql += ")"
sql += " order by verify_time desc"
print(sql)
cursor.execute(sql)
rows = cursor.fetchall()
datas = []
for row in rows:
data = {}
for i in range(len(row)):
data[cursor.description[i][0]] = row[i]
datas.append(data)
return datas
+165
View File
@@ -0,0 +1,165 @@
import mysql.connector
from mysql.connector import Error
class DBService:
# MySQL数据库连接配置
db_config = {
'host': '101.200.148.149',
'port': 4406,
'user': 'root',
'password': 'Jian(1@3)',
'database': 'verify_db'
}
conn = None
def __init__(self):
"""
初始化函数
"""
self.conn = self.get_conn()
print("DBService init")
self.create_database()
self.create_file_data_table()
self.create_invoice_table()
self.create_verify_log_table()
pass
def get_conn(self):
"""
创建数据库连接
该函数用于创建数据库连接,并返回连接对象
返回值:
连接对象
"""
if self.conn is not None and self.conn.is_connected():
return self.conn
try:
self.conn = mysql.connector.connect(**self.db_config)
except Error as e:
print(e)
return self.conn
def create_database(self):
"""
创建数据库
"""
try:
# 连接 MySQL 服务器(不指定数据库)
server_config = self.db_config.copy()
server_config.pop('database', None)
server_conn = mysql.connector.connect(**server_config)
cursor = server_conn.cursor()
# 创建数据库
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {self.db_config['database']} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
cursor.close()
server_conn.close()
# 重新连接到指定数据库
self.conn = self.get_conn()
except Error as e:
print(e)
def create_verify_log_table(self):
"""
创建验证记录表
该函数用于创建表,并返回创建结果
"""
try:
# delete_table_sql = """DROP TABLE IF EXISTS verify_log;"""
# c = self.get_conn().cursor()
# c.execute(delete_table_sql)
create_table_sql = """CREATE TABLE IF NOT EXISTS verify_log (
id INT AUTO_INCREMENT PRIMARY KEY,
cyjgxx TEXT NOT NULL,
inspectionAmount VARCHAR(255) NOT NULL,
verify_time VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
`desc` TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"""
c = self.get_conn().cursor()
c.execute(create_table_sql)
self.get_conn().commit()
except Error as e:
print(e)
# 文件数据表
def create_file_data_table(self):
"""
创建文件数据表
该函数用于创建表,并返回创建结果
"""
try:
# delete_table_sql = """DROP TABLE IF EXISTS file_data;"""
# c = self.get_conn().cursor()
# c.execute(delete_table_sql)
create_table_sql = """CREATE TABLE IF NOT EXISTS file_data (
id INT AUTO_INCREMENT PRIMARY KEY,
file_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INT NOT NULL,
file_type VARCHAR(50) NOT NULL,
file_time VARCHAR(50) NOT NULL,
file_hash VARCHAR(255) NOT NULL,
UNIQUE KEY unique_file_hash (file_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"""
c = self.get_conn().cursor()
c.execute(create_table_sql)
self.get_conn().commit()
except Error as e:
print(e)
# 发票数据表
def create_invoice_table(self):
# delete_table_sql = """DROP TABLE IF EXISTS invoice;"""
# c = self.get_conn().cursor()
# c.execute(delete_table_sql)
create_table_sql = """
-- 主表存储发票基本信息
CREATE TABLE IF NOT EXISTS invoice (
id INT AUTO_INCREMENT PRIMARY KEY,
checkCode VARCHAR(255), -- 校验码(唯一)
drawer VARCHAR(100), -- 开票人
formType VARCHAR(100), -- 表单类型
invoiceAmountPreTax DECIMAL(15,2), -- 税前金额
invoiceCode VARCHAR(50) NOT NULL, -- 发票代码
invoiceDate VARCHAR(50), -- 开票日期
invoiceNumber VARCHAR(50) NOT NULL, -- 发票号码
invoiceTax DECIMAL(15,2), -- 发票税额
invoiceType VARCHAR(100), -- 发票类型
machineCode VARCHAR(50), -- 机器编号
passwordArea TEXT, -- 密码区
printedInvoiceCode VARCHAR(50), -- 打印发票代码
printedInvoiceNumber VARCHAR(50), -- 打印发票号码
purchaserBankAccountInfo TEXT, -- 购买方银行信息
purchaserContactInfo TEXT, -- 购买方联系信息
purchaserName VARCHAR(255), -- 购买方名称
purchaserTaxNumber VARCHAR(100), -- 购买方税号
recipient VARCHAR(100), -- 收款人
remarks TEXT, -- 备注
reviewer VARCHAR(100), -- 复核人
sellerBankAccountInfo TEXT, -- 销售方银行信息
sellerContactInfo TEXT, -- 销售方联系信息
sellerName VARCHAR(255), -- 销售方名称
sellerTaxNumber VARCHAR(100), -- 销售方税号
specialTag VARCHAR(50), -- 特殊标记
title VARCHAR(255), -- 发票标题
totalAmount DECIMAL(15,2), -- 合计金额
totalAmountInWords VARCHAR(255), -- 大写金额
file_path TEXT, -- 文件哈希
verify_time VARCHAR(50), -- 验证时间
verify_status VARCHAR(50), -- 验证结果
inspectionAmount VARCHAR(50), -- 验证次数
`desc` TEXT, -- 备注
desc_files TEXT, -- 备注附件
status VARCHAR(20), -- 是否报销(yes,no
UNIQUE KEY unique_invoice (invoiceCode, invoiceNumber)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"""
try:
c = self.get_conn().cursor()
c.execute(create_table_sql)
self.get_conn().commit()
except Error as e:
print(e)
+54
View File
@@ -0,0 +1,54 @@
# 文件服务接口
from typing import Any
from flask import Request
from werkzeug.utils import secure_filename
import os , time, hashlib
from alibabacloud_sample.dataservice import DataService
dataservice = DataService()
class FileService:
# 创建文件服务实例
def __init__(self):
pass
# 上传文件
def upload(self, request: Request, file_path: str) -> None | str | dict[Any, Any] | Any:
if request.method == 'POST':
# 检查是否有文件在请求中
if 'file' not in request.files:
return '没有文件被上传'
file = request.files['file']
if file.filename == '':
return '请重新选择文件'
if file:
date = time.strftime("%Y%m%d", time.localtime())
filetype = secure_filename(file.filename)
filename = str(int(time.time())) + "_" + file.filename
filepath = os.path.join(file_path, date, filename)
os.makedirs(os.path.dirname(filepath), exist_ok=True)
file.save(filepath)
file_size = os.path.getsize(filepath)
file_hash = self.get_hash(filepath)
v_file = dataservice.get_file(file_hash,file_size)
if v_file:
os.remove(filepath)
return v_file
else:
dataservice.insert_file_data(file.filename, filepath, file_size, filetype, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),file_hash)
v_file = dataservice.get_file(file_hash, file_size)
print("insert v_file:", v_file)
return v_file
return None
# 获取文件md5
@staticmethod
def get_hash(file_path: str) -> str:
with open(file_path, 'rb') as f:
md5 = hashlib.md5()
while True:
data = f.read(8192)
if not data:
break
md5.update(data)
return md5.hexdigest()
+97
View File
@@ -0,0 +1,97 @@
from flask import Flask, request, render_template, send_from_directory
from flask_cors import CORS
from alibabacloud_sample.dataservice import DataService
from alibabacloud_sample.fileservice import FileService
from alibabacloud_sample.service import Service
app = Flask(__name__)
CORS(app)
app.config['UPLOAD_FOLDER'] = '/data/uploads' # 指定上传文件夹
app.config['WEBAPP'] = 'templates'
dataservice = DataService()
@app.route('/index')
def index():
return render_template('index.html')
@app.route('/assets/<path:filename>')
def return_js(filename):
return send_from_directory(app.config.get('WEBAPP')+'/assets', str(filename))
@app.route('/data/uploads/<path:filename>')
def return_file(filename):
return send_from_directory(app.config.get('UPLOAD_FOLDER'), str(filename))
# 上传文件
@app.route('/upload', methods=['POST'])
def upload_file():
if request.method == 'POST':
file_service = FileService()
msg = file_service.upload(request,app.config['UPLOAD_FOLDER'])
if msg == "No file part" or msg == "No selected file":
return "请选择文件"
else:
return msg
return None
# 发票识别
@app.route('/recognize', methods=['GET'])
def recognize():
file_path = request.args.get('filePath')
service = Service()
data = dataservice.get_invoice(file_path=file_path)
if data is not None and data.get('file_path') is not None:
return data
return service.recognize(file_path=file_path)
# 发票验证
@app.route('/verify', methods=['GET'])
def verify():
file_path = request.args.get('filePath')
invoice_id = request.args.get('invoiceId')
if file_path is None:
return {"msg":"请选择文件","status":'fail'}
service = Service()
data = dataservice.get_invoice(file_path=file_path,invice_id=invoice_id)
if data is not None:
if data.get('status') == 'yes':
return {"msg":"请勿重复验证","status":'fail'}
return service.verify(data=data,file_path=file_path)
else:
return {"msg":"请先识别发票","status":'fail'}
# 发票复验
@app.route('/reverify', methods=['GET'])
def reverify():
file_path = request.args.get('filePath')
if file_path is None:
return "请选择文件"
service = Service()
data = dataservice.get_invoice(file_path=file_path)
return service.verify(data=data,file_path=file_path,reverify=True)
# 发票列表
@app.route('/listInvoice',methods=['POST'])
def list_invoice():
value = request.json.get('value')
verify_status = request.json.get('verify_status')
verify_time = request.json.get('verify_time')
start_date = None
end_date = None
if verify_time is not None:
start_date = verify_time[0].split(' ')[0]
end_date = verify_time[1].split(' ')[0]
return dataservice.get_invoice_list(value=value,verify_status=verify_status,start_date=start_date,end_date=end_date)
@app.route('/listInvoiceTimeOut',methods=['POST'])
def list_invoice_time_out():
time_out = request.form.get('timeOut')
return dataservice.get_invoice_list(value='',verify_status='',time_out=time_out)
# 发票日志列表
@app.route('/listLogs',methods=['GET'])
def list_logs():
file_path = request.args.get('filePath')
return dataservice.get_verify_log_list(file_path=file_path)
# 更新发票标注
@app.route('/updateInvoice',methods=['POST'])
def update_invoice():
invoice_id = request.json.get('invoiceId')
status = request.json.get('status')
desc = request.json.get('desc')
desc_files = request.json.get('desc_files')
return dataservice.update_invoice_desc(invoice_id=invoice_id,desc=desc,desc_file=desc_files,status=status)
if __name__ == '__main__':
# Service.verify(sys.argv[1:])
# Service.recognize(file_path="/home/jayus/图片/wechat_2025-07-31_131911_822.png")
app.run(host='0.0.0.0', port=5555, debug=True)
+150
View File
@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import os
import statistics
import sys
from typing import List
from alibabacloud_credentials.models import Config
from alibabacloud_ocr_api20210707.client import Client as ocr_api20210707Client, Client
from alibabacloud_credentials.client import Client as CredentialClient
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_ocr_api20210707 import models as ocr_api_20210707_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_console.client import Client as ConsoleClient
from alibabacloud_tea_util.client import Client as UtilClient
from alibabacloud_darabonba_stream.client import Client as StreamClient
from flask import json
from alibabacloud_sample.dataservice import DataService
class Service:
def __init__(self):
pass
# 发票类型字典
typeDict = {
"增值税专用发票": 1,
"增值税普通发票": 4,
"增值税普通发票(电子)": 10,
"增值税普通发票(卷式)": 11,
"增值税普通发票(通行费)": 14,
"增值税电子专用发票": 20,
"数电发票(增值税专用发票)": 31,
"数电发票(增值税普通发票)": 32,
"数电发票(纸质专用发票)": 85,
"数电发票(纸质普通发票)": 86,
}
@staticmethod
def create_client() -> ocr_api20210707Client:
"""
使用凭据初始化账号Client
@return: Client
@throws Exception
"""
# 工程代码建议使用更安全的无AK方式,凭据配置方式请参见:https://help.aliyun.com/document_detail/378659.html。
# credential = CredentialClient()
# config = open_api_models.Config(
# credential=credential
# )
config = Config(
type='access_key',
access_key_id='LTAI5t7481fKEcZjrvWyZp48',
access_key_secret='FwRfm4gst2Dk10tbXeJl0OV7EzL6qM'
# access_key_id = 'LTAI5tHvCWb5uHZ9Q7iUJTLp',
# access_key_secret = 'zKAL7WqvnJ0lN0GHsvJptAGRiuCOfM'
)
config.endpoint = f'ocr-api.cn-hangzhou.aliyuncs.com'
cred = CredentialClient(config)
cred.get_credential()
nconfig = open_api_models.Config(
credential=cred
)
# Endpoint 请参考 https://api.aliyun.com/product/ocr-api
nconfig.endpoint = f'ocr-api.cn-hangzhou.aliyuncs.com'
return ocr_api20210707Client(nconfig)
# 发票验证
@staticmethod
def verify(data: dict, file_path: str,reverify: bool = False):
dataservice = DataService()
# 复验
if not reverify:
data1 = dataservice.get_verify_log(file_path=file_path)
print(data1)
if data1 is not None and int(data1.get('inspectionAmount')) > 0:
return data1
client = Service.create_client()
type = Service.typeDict.get(data.get('invoiceType'))
sum = data.get('totalAmount')
if type == 1 or type == 20:
sum = data.get('totalAmountWithoutTax')
if data.get('checkCode') is not None:
verify_code = data.get('checkCode')[-6:]
else:
verify_code = None
date = data.get('invoiceDate').replace('', "").replace('', "").replace('', "")
verify_vatinvoice_request = ocr_api_20210707_models.VerifyVATInvoiceRequest(
invoice_no= data.get('invoiceNumber'),
invoice_code= data.get('invoiceCode'),
invoice_date= date,
invoice_sum= sum,
invoice_kind= type,
verify_code= verify_code,
)
runtime = util_models.RuntimeOptions()
try:
resp = client.verify_vatinvoice_with_options(verify_vatinvoice_request, runtime)
ConsoleClient.log(UtilClient.to_jsonstring(resp))
respjson = json.loads(UtilClient.to_jsonstring(resp.body.data))
code = respjson.get('code')
if code != '001':
return {'cyjgxx': respjson.get('msg'), 'inspectionAmount': -1,'status': 'fail'}
resdata = respjson.get('data')
if resdata is None:
resdata = {'cyjgxx': "未查询到发票信息", 'inspectionAmount': -1,'status': 'fail'}
else:
resdata.update({'status': 'success'})
resdata['status'] = 'success'
dataservice.insert_verify_log(resdata.get('inspectionAmount'), resdata.get('cyjgxx'), file_path)
dataservice.update_invoice_status(data.get('id'), resdata.get('status'), resdata.get('inspectionAmount'))
return resdata
except Exception as error:
# 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
# 错误 message
print(error)
# 诊断地址
print(error.data.get("Recommend"))
UtilClient.assert_as_string(error.message)
# 发票识别
@staticmethod
def recognize(file_path: str):
client = Service.create_client()
dataservice = DataService()
body_stream = StreamClient.read_from_file_path(file_path)
recognize_mixed_invoices_request = ocr_api_20210707_models.RecognizeMixedInvoicesRequest(
body=body_stream
)
runtime = util_models.RuntimeOptions()
try:
# 复制代码运行请自行打印 API 的返回值
res = client.recognize_mixed_invoices_with_options(recognize_mixed_invoices_request, runtime)
print(UtilClient.to_jsonstring(res.body.data))
data = json.loads(UtilClient.to_jsonstring(res.body.data)).get('subMsgs')[0].get('result').get('data')
data_id = dataservice.insert_invoice_log(data, file_path)
data.update({'id': data_id})
return data
except Exception as error:
# 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
# 错误 message
print(error)
# 诊断地址
UtilClient.assert_as_string(error)
# 发票验证记录
@staticmethod
def get_verify_log_list(file_path):
dataservice = DataService()
return dataservice.get_verify_log_list(file_path)
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.card{padding:2em}#app{width:100%;height:100%;margin:0 auto;padding:2rem;text-align:center}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.pdf[data-v-d33302c4]{width:100%;height:100%}.container[data-v-80a50284]{margin-top:50px;height:100%}.pdf[data-v-c4a3080b]{width:100%;height:100%}.ant-steps[data-v-f2a9b4bc]{padding:0 20px 20px}.table-operations[data-v-8ed0a2a4]{margin-bottom:16px;text-align:right}.table-operations>button[data-v-8ed0a2a4]{margin-right:8px}#components-layout-demo-top-side .logo[data-v-274d3151]{float:left;height:31px;margin:16px 24px 16px 0;background:#ffffff4d}.ant-row-rtl #components-layout-demo-top-side .logo[data-v-274d3151]{float:right;margin:16px 0 16px 24px}.container[data-v-274d3151]{height:100%}html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<script type="module" crossorigin src="/assets/index-B0bdG1dl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C6TrMyLy.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.
@@ -0,0 +1 @@
__version__ = "1.0.0"
+81
View File
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import os
import sys
from typing import List
from alibabacloud_ocr_api20210707.client import Client as ocr_api20210707Client
from alibabacloud_credentials.client import Client as CredentialClient
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_ocr_api20210707 import models as ocr_api_20210707_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_console.client import Client as ConsoleClient
from alibabacloud_tea_util.client import Client as UtilClient
class Sample:
def __init__(self):
pass
@staticmethod
def create_client() -> ocr_api20210707Client:
"""
使用凭据初始化账号Client
@return: Client
@throws Exception
"""
# 工程代码建议使用更安全的无AK方式,凭据配置方式请参见:https://help.aliyun.com/document_detail/378659.html。
credential = CredentialClient()
config = open_api_models.Config(
credential=credential
)
# Endpoint 请参考 https://api.aliyun.com/product/ocr-api
config.endpoint = f'ocr-api.cn-hangzhou.aliyuncs.com'
return ocr_api20210707Client(config)
@staticmethod
def main(
args: List[str],
) -> None:
client = Sample.create_client()
verify_vatinvoice_request = ocr_api_20210707_models.VerifyVATInvoiceRequest(
invoice_no='25617000000124363744',
invoice_date='20250620'
)
runtime = util_models.RuntimeOptions()
try:
resp = client.verify_vatinvoice_with_options(verify_vatinvoice_request, runtime)
ConsoleClient.log(UtilClient.to_jsonstring(resp))
except Exception as error:
# 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
# 错误 message
print(error.message)
# 诊断地址
print(error.data.get("Recommend"))
UtilClient.assert_as_string(error.message)
@staticmethod
async def main_async(
args: List[str],
) -> None:
client = Sample.create_client()
verify_vatinvoice_request = ocr_api_20210707_models.VerifyVATInvoiceRequest(
invoice_no='25617000000124363744',
invoice_date='20250620'
)
runtime = util_models.RuntimeOptions()
try:
resp = await client.verify_vatinvoice_with_options_async(verify_vatinvoice_request, runtime)
ConsoleClient.log(UtilClient.to_jsonstring(resp))
except Exception as error:
# 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
# 错误 message
print(error.message)
# 诊断地址
print(error.data.get("Recommend"))
UtilClient.assert_as_string(error.message)
if __name__ == '__main__':
Sample.main(sys.argv[1:])
+10
View File
@@ -0,0 +1,10 @@
version: '3.8'
services:
python-app:
build: .
container_name: python-app
volumes:
- ./:/app
- ./data:/data
ports:
- "8555:5555" # 如果需要暴露 Flask 端口
+10
View File
@@ -0,0 +1,10 @@
FROM python:latest
WORKDIR /app
RUN git clone http://192.168.1.68:8418/jayus/VerifVATInvoice.git .
COPY . .
RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple
RUN python setup.py install
WORKDIR /app/alibabacloud_sample
CMD ["python", "main.py"]
+12
View File
@@ -0,0 +1,12 @@
alibabacloud_credentials==1.0.2
alibabacloud_darabonba_stream==0.0.2
alibabacloud_ocr_api20210707==3.1.3
alibabacloud_tea_console==0.0.1
alibabacloud_tea_openapi==0.4.1
alibabacloud_tea_util==0.3.13
Flask==3.1.1
flask_cors==6.0.1
python_dateutil==2.9.0.post0
Werkzeug==3.1.3
setuptools==80.9.0
mysql-connector-python==9.4.0
+80
View File
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
"""
import os
from setuptools import setup, find_packages
"""
setup module for alibabacloud_sample.
Created on 31/07/2025
@author:
"""
PACKAGE = "alibabacloud_sample"
NAME = "alibabacloud_sample" or "alibabacloud-package"
DESCRIPTION = "Alibaba Cloud SDK Code Sample Library for Python"
AUTHOR = ""
AUTHOR_EMAIL = ""
URL = "https://github.com/aliyun/alibabacloud-sdk"
VERSION = __import__(PACKAGE).__version__
REQUIRES = [
"alibabacloud_ocr-api20210707>=3.1.3, <4.0.0",
"alibabacloud_tea_openapi>=0.3.15, <1.0.0",
"alibabacloud_tea_console>=0.0.1, <1.0.0",
"alibabacloud_tea_util>=0.3.13, <1.0.0",
"alibabacloud_credentials>=1.0.2, <2.0.0", 'flask'
]
LONG_DESCRIPTION = ''
if os.path.exists('./README.md'):
with open("README.md", encoding='utf-8') as fp:
LONG_DESCRIPTION = fp.read()
setup(
name=NAME,
version=VERSION,
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
long_description_content_type='text/markdown',
author=AUTHOR,
author_email=AUTHOR_EMAIL,
license="Apache License 2.0",
url=URL,
keywords=["alibabacloud","sample"],
packages=find_packages(exclude=["tests*"]),
include_package_data=True,
platforms="any",
install_requires=REQUIRES,
python_requires=">=3.6",
classifiers=(
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
"Topic :: Software Development"
)
)
BIN
View File
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
{
"name": "webapp",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/dateutil": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/dateutil/-/dateutil-0.1.0.tgz",
"integrity": "sha512-bI8Lm50mageQER89AvYr+bAxXn4MCAkjSg3M58QY2evrIypUk2aCQcMZ86f31tjmt2ceMrPkrHkhItYqOJsObw==",
"engines": {
"node": "*"
}
}
}
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2010 Borgar Þorsteinsson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+224
View File
@@ -0,0 +1,224 @@
# Dateutil
Dateutil is a modest collection of utility methods for manipulating dates. It works both in Node.JS and in browsers. It's goal is to provide extendable mechanisms for parsing and formatting dates, as well as other convenient date methods.
Currently, the library supports formatting of nearly full spec of PHP style dates (minus the really useless bits), and parses about the full range of ISO 8901 formats. It supports translations (for formatter) but no timezones.
## Dateutil methods
### dateutil.date( [year], [month], [day], [hour], [min], [sec], [ms] )
Function returns a new *Date* instance set to the expected date. If no arguments are given, then it returns the current date. When any arguments are given they are handled in much the same way the native date constructor does, except the first argument is always treated as a year.
It is safe to pass strings as arguments.
**Please note:** Months are zero based in set just as they are in native *Date* construction.
### dateutil.isLeapYear( date / year )
Function accepts a *Date*, or a year, and will return true if the year is a leap year or false if not.
dateutil.isLeapYear( 1468 ) == true
### dateutil.daysInMonth( date )
Function accepts a *Date* and returns the number of days in it's month.
dateutil.daysInMonth( new Date(1920, 3, 1) ) == 30
### dateutil.isocalendar( date )
Function accepts a *Date* and returns a three value array containing the ISO 8901 year, week, and day (of the week), respectively.
dateutil.isocalendar( new Date(2010, 0, 3) ) == [2009, 53, 7]
### dateutil.set( date, values )
Function accepts a *Date* and an object of values. The function returns the input date (same instance) having set the units specified in the values collection.
var mydate = new Date( 2000, 9, 16, 10, 45, 12 );
dateutil.set( mydate, { hour: 0, minute: 0, second: 0 });
result: mydate == new Date( 2000, 9, 16, 0, 0, 0 );
The function will accept all or any of these keys:
`year`, `month`, `day`, `hour`, `minute`, `second`, `millisecond`. and their plural forms (`years`, `minutes`, etc. ); and these shorthand variations: `yr` (year), `mn` (month), `day` (day), `hr` (hour), `min` (minute), `sec` (second), `ms` (millisecond).
**Please note:** Months are zero based in set just as they are in native *Date* construction.
This function tries to avoid rollover gotchas that can occur when using native members to set date values. A simplified example of this:
var d = new Date( 1999, 1, 20 );
d.setUTCDate( 30 ); // Tue Mar 02 1999 00:00:00 GMT+0000 (GMT)
d.setUTCMonth( 0 ); // Sat Jan 02 1999 00:00:00 GMT+0000 (GMT)
The `dateutil.set` function is takes steps to avoid the problem:
var d = new Date( 1999, 1, 20 );
dateutil.set(d, {
'date': 30,
'month': 0
}); // Sat Jan 30 1999 00:00:00 GMT+0000 (GMT)
var d = new Date( 1999, 1, 20 );
dateutil.set(d, {
'month': 0,
'date': 30
}); // Sat Jan 30 1999 00:00:00 GMT+0000 (GMT)
If you want to be sure you get the correct date then you need to use the `dateutil.date`
### dateutil.parse( string )
Function accepts a string (representing a date) and will output a *Date* based on parsing the string.
dateutil.parse('2005-01-01') == new Date(2005, 0, 1)
The function will aways return a *Date* instance, valid or not. Internally it falls back to native parser if it fails to recognize the format.
A mostly complete list of recognized formats:
* yyyy-mm-dd hh:mm:ss.ffff+0000 &nbsp; &mdash; *(timezone is currently ignored)*
* yyyy-mm-dd hh:mm:ss.ffffZ
* yyyy-mm-dd hh:mm:ss.ffff
* yyyy-mm-dd hh:mm
* yyyy-mm-ddThh:mm:ss.ffff+0000 &nbsp; &mdash; *(timezone is currently ignored)*
* yyyy-mm-ddThh:mm:ss.ffffZ
* yyyy-mm-ddThh:mm:ss.ffff
* yyyy-mm-ddThh:mm
* yyyy-mm-dd
* yyyy-mmdd
* yyyymmdd
* yyyy-ddd &nbsp; &mdash; *year with day-of-the-year*
* yyyy-Www-d
* yyyy-Wwwd
* yyyy-Www
* yyyyWww-d
* yyyyWwwd
* yyyyWww
* yyyy-Qq &nbsp; &mdash; *year with quarter*
* yyyyQq
* yyyy-mm
* yyyy/mm
* yyyy
... In addition to the formats JavaScript can natively parse.
#### Adding a custom parser
You may add your own parser by assigning them into `dateutil._parsers`. A parser is simply an object with two members:
* `test`: a regular expression used to test if this date can be parsed by the parser. Keep these simple and avoid capture groups, or suffer the slowdown cost.
* `parse`: a function that handles turning the string into a `Date` instance.
An contrived example:
// year + excel week
dateutil._parsers['year_and_excel_week'] = {
test: /^\d{4}_[0-5]\d$/,
parse: function ( str ) {
var bits = str.split('_');
var year = parseInt( bits[0], 10 );
var dofy = parseInt( bits[1], 10 );
return new Date( year, 0, ((dofy - 1) * 7) + 1);
}
};
// usage:
dateutil.parse('2002_31') == new Date(2002, 6, 30)
### dateutil.format( date, format_string, [language] )
Function accepts a *Date, a *PHP* style [format string][1], and an optional language identifyer, and will return a formatted date string. Refer to the [PHP docs][1] for the full spec of available format characters.
var mydate = new Date( 2000, 9, 16, 10, 45, 12 );
dateutil.format( mydate, 'jS F Y' ) == "16th October 2000"
The `B`, `Z`, and `I` characters have been purposely omitted, and the following new characters added:
* `q` &mdash; quarter of the year
The third optional parameter will get passed through the formatting system until it reaches
The format method is smart about being assigned to the `Date.prototype` object and will work as expected:
Date.prototype.format = dateutil.format;
new Date( 1975, 9, 16 ).format( 'Y-m-d' ) == "1975-10-16"
#### Adding a custom formatter
You may add your own formatter by assigning them into `dateutil._formats`. A parser is simply a function that takes a *Date* parameter, a language parameter, and returns a string.
An example that adds *Swatch internet time* formatter:
// Swatch Internet time
dateutil._formats['B'] = function (d) {
var mo = 0; // This should really be: isDST * 60 + timeZoneOffsetInMinutes
return Math.round( ( d.getUTCHours() * 3600 +
( ( d.getUTCMinutes() - mo + 60 ) * 60 ) +
d.getUTCSeconds() ) * 1000 / 86400 ) % 1000;
};
var mydate = new Date( 1961, 0, 3, 1, 51, 10, 1 );
dateutil.format( mydate, 'B' ) == '119'
If you call `dateutil._`, or recursively call formatting or other specific formatting functions for delegating work, it is important that you keep passing the second parameter so that a specified language is perserved.
An example that adds upper case 4 letter month name:
dateutil._formats['R'] = function (d,l) {
var mont = this['F']( d, l ); // fetch translated month in "l" language
return mont.substr( 0, 4 ).toUpperCase();
};
### dateutil.today()
Function returns a new *Date* instance set to the current day.
### dateutil.now()
Function returns a new *Date* instance set to the current moment. Uses native implementation if it exists.
### dateutil._( string, [language] )
A string translation function. This function looks for the string argument as a key in `dateutil.lang[ language ]` and uses the the value if it exists, otherwise it falls back on the string argument.
You can feed the system translation strings and use it, or alternatively, if you already have another translation system then you can overwrite this function with your own translation function.
In order to use the built in system you would add translated strings like in this example (Icelandic language):
dateutil.lang.is = {
'January': 'janúar',
'February': 'febrúar',
'March': 'mars',
'April': 'apríl',
'May': 'maí',
'June': 'júní',
// and so on ...
};
You should provide translations for the following strings:
* January February March April May June July August September October November December
* Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
* Sunday Monday Tuesday Wednesday Thursday Friday Saturday
* Sun Mon Tue Wed Thu Fri Sat
[1]: http://php.net/manual/en/function.date.php
+398
View File
@@ -0,0 +1,398 @@
/*
* Dateutil
* - provides formatting, parsing and other utility functions for dates.
*
* Copyright (c) 2009 Borgar Þorsteinsson
* Licensed under the terms of the MIT (LICENSE.txt) software license.
*
*/
/*jslint laxbreak: true, undef: true *//*global module */
(function(__global__){
var SECOND_SIZE = 1000;
var MINUTE_SIZE = SECOND_SIZE * 60;
var HOUR_SIZE = MINUTE_SIZE * 60;
var DAY_SIZE = HOUR_SIZE * 24;
var WEEK_SIZE = DAY_SIZE * 7;
var MONTH_SIZE = DAY_SIZE * 30.436875; // average month size
var YEAR_SIZE = DAY_SIZE * 365.2425; // average year size
var _gl = __global__.lang = { 'en': {} };
var _m = 'January February March April May June July August September October November December'.split(' ');
var _d = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' ');
var method_size = {
'FullYear': 6, 'Month': 5, 'Date': 4, 'Hours': 3,
'Minutes': 2, 'Seconds': 1, 'Milliseconds': 0
};
var method_map = {
'yr': 'FullYear',
'year': 'FullYear',
'years': 'FullYear',
'mn': 'Month',
'month': 'Month',
'months': 'Month',
'day': 'Date',
'days': 'Date',
'date': 'Date',
'hr': 'Hours',
'hour': 'Hours',
'hours': 'Hours',
'min': 'Minutes',
'minute': 'Minutes',
'minutes': 'Minutes',
'sec': 'Seconds',
'second': 'Seconds',
'seconds': 'Seconds',
'ms': 'Milliseconds',
'millisecond': 'Milliseconds',
'milliseconds': 'Milliseconds'
};
// *****************************************
// *** *** *** formats & parsers *** *** ***
// *****************************************
var date_parsers = __global__._parsers = {
// year + month + day + time
// -- currently doesn't really support fractions on anything other than seconds >> FIXME
// -- does not support timezones other than Zulu
date_and_time: {
test: /^[+\-]?\d{4,6}(?:(?:\-\d\d){1,2}|\d{4})[T ](?:\d\d)(?::?\d\d){0,2}(?:[\.,]\d+)?(?:Z|[+\-]\d\d(:?\d\d)?)?$/,
size: 1,
parse: function ( str ) {
var b = str.split( /[T ]/ );
var date = date_parsers.date.parse( b[0] );
var m = b[1].replace( /:/g, '' )
.match( /^(\d\d)(\d\d)?(\d\d)?(?:[.,](\d+))?([+\-](?:\d\d){1,2})?/ );
// TODO: timezone (I have no need for this feature yet)
// if ( m[5] ) { var zone = m[5] || '0000'; }
var fs = 0, t = date.getTime() +
parseInt( m[1], 10 ) * HOUR_SIZE +
parseInt( m[2] || '0', 10 ) * MINUTE_SIZE +
parseInt( m[3] || '0', 10 ) * SECOND_SIZE;
if ( m[3] ) { fs = SECOND_SIZE; }
else if ( m[2] ) { fs = MINUTE_SIZE; }
else if ( m[1] ) { fs = HOUR_SIZE; }
t += parseFloat( '0.' + ( m[4] || '0' ) ) * fs;
date.setTime( t );
date.size = 0;
return date;
}
},
// year + month + day
date: {
test: /^[+\-]?\d{4,6}(?:\-\d\d\-\d\d|-?\d\d\d\d)$/,
size: DAY_SIZE,
parse: function ( str ) {
var s = str.replace( /\D/g, '' ),
d = __global__.date(
s.substring( 0, s.length -4 ),
+s.substr( s.length -4, 2 ) - 1,
s.substr( s.length -2 )
);
d.size = DAY_SIZE;
return d;
}
},
// year + month
year_and_month: {
test: /^[+\-]?\d{4,6}[\/\-](?:0[1-9]|1[012])$/,
size: MONTH_SIZE,
parse: function ( str ) {
var b = str.split( /[\/\-]/ );
var d = __global__.date( b[0], +b[1] - 1, 1 );
d.size = __global__.daysInMonth( d ) * DAY_SIZE;
return d;
}
},
// year
year: {
test: /^[+\-]?\d{4,6}$/,
size: YEAR_SIZE,
parse: function ( str ) {
var d = __global__.date( str, 0, 1 );
d.size = DAY_SIZE * ( __global__.isLeapYear( d ) ? 366 : 365 );
return d;
}
},
// year + iso week + [day]
year_and_week: {
test: /^[+\-]?\d{4,6}\-?[Ww]\d\d(?:\-?\d)?$/,
size: WEEK_SIZE,
parse: function ( str ) {
var s = str.toLowerCase().replace( /[^w\d]/g, '' ).split('w');
var d = __global__.date( s[0], 0, 3 ); // Jan 3
d.setUTCDate( 3 - d.getUTCDay() +
( parseInt( s[1].substr( 0, 2 ), 10 ) - 1 ) * 7 +
parseInt( s[1].substr( 2, 1 ) || '1', 10 ) );
d.size = WEEK_SIZE;
return d;
}
},
// year + day-of-year
// -- we don't allow the short form yyyyddd because of ambiguity with yyyymmdd
// -- 5 letter years would clash with cal-dates: yyyyyddd ~ yyyymmdd
year_and_ordinal: {
test: /^[+\-]?\d{4,6}\-[0-3]\d\d$/,
size: DAY_SIZE,
parse: function ( str ) {
var d = new Date(0);
d.setUTCFullYear( parseInt( str.substr(0,str.length-4), 10 ) );
d.setDate( parseInt( str.substr(str.length-3), 10 ) );
d.size = DAY_SIZE;
return d;
}
},
// year + quarter
year_and_quarter: {
test: /^[+\-]?\d{4,6}\-?[Qq][1-4]$/,
size: YEAR_SIZE / 4,
parse: function ( str ) {
var b = str.split(/\-?[Qq]/),
d = __global__.date( b[0], ( parseInt( b[1], 10 ) - 1 ) * 3 );
d.size = DAY_SIZE;
return d;
}
}
};
var date_formatters = __global__._formats = {
// Lowercase Ante meridiem and Post meridiem
a: function (d) { return d.getUTCHours() >= 12 ? 'pm' : 'am'; },
// Uppercase Ante meridiem and Post meridiem
A: function (d) { return d.getUTCHours() >= 12 ? 'PM' : 'AM'; },
// ISO 8601 date
c: function (d,l) { return __global__.format( d, 'Y-m-d\\TH:i:s.', l ) + __global__.pad( d.getUTCMilliseconds(), 3 ) + 'Z'; },
// Day of the month, 2 digits with leading zeros
d: function (d) { return __global__.pad( d.getUTCDate() ); },
// A textual representation of a day, three letters
D: function (d,l) { return __global__._( _d[ d.getUTCDay() ].substr( 0, 3 ), l ); },
// Time zone identifier
e: function (d) { return 'UTC'; },
// A full textual representation of a month
F: function (d,l) { return __global__._( _m[ d.getUTCMonth() ], l ); },
// 12-hour format of an hour without leading zeros
g: function (d) { return d.getUTCHours() % 12 || 12; },
// 24-hour format of an hour without leading zeros
G: function (d) { return d.getUTCHours(); },
// 12-hour format of an hour with leading zeros
h: function (d) { return __global__.pad( d.getUTCHours() % 12 || 12 ); },
// 24-hour format of an hour with leading zeros
H: function (d) { return __global__.pad( d.getUTCHours() ); },
// Minutes with leading zeros
i: function (d) { return __global__.pad( d.getUTCMinutes() ); },
// Day of the month without leading zeros
j: function (d) { return d.getUTCDate(); },
// A full textual representation of the day of the week
l: function (d,l) { return __global__._( _d[ d.getUTCDay() ], l ); },
// Whether it's a leap year (0 = yes, 1 = no)
L: function (d) { return __global__.isLeapYear( d ) * 1; },
// Numeric representation of a month, with leading zeros
m: function (d) { return __global__.pad( d.getUTCMonth() + 1 ); },
// A short textual representation of a month, three letters
M: function (d,l) { return __global__._( _m[ d.getUTCMonth() ].substr( 0, 3 ), l ); },
// Numeric representation of a month, without leading zeros
n: function (d) { return d.getUTCMonth() + 1; },
// ISO-8601 numeric representation of the day of the week
N: function (d) { return d.getUTCDay() || 7; },
// ISO-8601 year number
o: function (d) { return __global__.pad( __global__.isocalendar(d)[0], 4 ); },
// Time zone designator
O: function (d) { return '+0000'; },
// Time zone difference
P: function (d) { return '+00:00'; },
// Quarter of the year
q: function (d) { return ~~( d.getUTCMonth() / 3 ) + 1; },
// RFC 2822 formatted date
r: function (d,l) { return __global__.format( d, 'D, d M Y H:i:s O', l ); },
// Seconds, with leading zeros
s: function (d) { return __global__.pad( d.getUTCSeconds() ); },
// English ordinal suffix for the day of the month, 2 characters
S: function (d) {
var a = d.getUTCDate() % 10, b = d.getUTCDate() % 100;
return (a === 1) && (b !== 11) && 'st' ||
(a === 2) && (b !== 12) && 'nd' ||
(a === 3) && (b !== 13) && 'rd' || 'th';
},
// Number of days in the given month
t: function (d) { return __global__.daysInMonth( d ); },
// Time zone abbreviation
T: function (d) { return 'UTC'; },
// Microseconds
u: function (d) { return __global__.pad( d.getUTCMilliseconds(), 6 ); },
// Seconds since the Unix Epoch
U: function (d) { return ~~( d / 1000 ); },
// Numeric representation of the day of the week
w: function (d) { return d.getUTCDay(); },
// ISO-8601 week number of year, weeks starting on Monday
W: function (d) { return __global__.pad( __global__.isocalendar(d)[1] ); },
// A short numeric representation of a year, 2 digits
y: function (d) { return (d.getUTCFullYear() + '').substr(2); },
// A full numeric representation of a year, 4 digits
Y: function (d) { return d.getUTCFullYear(); },
// The day of the year (starting from 0)
z: function (d) { return Math.floor( ( d - (new Date(Date.UTC(d.getUTCFullYear(), 0, 1))) ) / DAY_SIZE ); }
};
// **************************************
// *** *** *** module methods *** *** ***
// **************************************
__global__.date = function ( y, m, d, h, n, s, ms ) {
var ts = ( !arguments.length ) ? __global__.now()
: Date.UTC( parseInt( y||0, 10 ), parseInt( m||0, 10 ), parseInt( d||1, 10 ),
parseInt( h||0, 10 ), parseInt( n||0, 10 ), parseInt( s||0, 10 ),
parseInt( ms||0, 10 ) );
return new Date( ts );
};
// zero pad a string n to l places
__global__.pad = function ( n, l ) {
var s = __global__.pad.z;
if ( !s ) { // This mess is here because JSlint breaks on new Array(999)
var a = []; a[999] = '';
s = __global__.pad.z = a.join('0');
}
s += n;
return s.substring( s.length -( l || 2 ) );
};
// is a given year a leap year
__global__.isLeapYear = function ( y ) {
if ( y instanceof Date ) { y = y.getUTCFullYear(); }
return (( y % 4 === 0 ) && ( y % 100 !== 0 )) || ( y % 400 === 0 );
};
// return the number of days in a date's month
__global__.daysInMonth = function ( dt ) {
var m = dt.getUTCMonth();
if ( m === 1 ) {
return __global__.isLeapYear( dt ) ? 29 : 28;
}
return [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ][ m ];
};
// return a 3-tuple, (ISO year, ISO week number, ISO weekday).
__global__.isocalendar = function ( dt ) {
var d = dt.getUTCDay();
var t = new Date( dt.valueOf() );
t.setDate( t.getDate() - ((d + 6) % 7) + 3 );
var iso_year = t.getUTCFullYear();
var w = Math.floor( (t.getTime() - __global__.date(iso_year, 0, 1, -6)) / 86400000 );
return [ iso_year, 1+Math.floor(w/7), d||7 ];
};
// Allow setting multiple properties at once using object notation:
// `mydate.set({ hour: 8, minute: 12, second: 0 });`
__global__.set = function ( dt, values ) {
if ( typeof values === 'object' ) {
var s = [], n, i;
// step 1: collect a list of values to modify
for ( var key in values ) {
if ( key in method_map ) {
n = method_map[ key ];
s.push( [ values[ key ], n, method_size[ n ] ] );
}
}
// step 2: order values by size
s = s.sort(function ( a, b ) { return a[2] - b[2]; });
// step 3: little endian value zeroing
for (i=0; i<s.length; i++) {
dt[ 'setUTC' + s[i][1] ]( s[i][1] === 'Date' ? 1 : 0 );
}
// step 4: big endian value setting
for (i=s.length; i--; ) {
dt[ 'setUTC' + s[i][1] ]( s[i][0] );
}
}
return dt;
};
// parse a date
__global__.parse = function ( str ) {
var d;
if ( typeof str !== 'string' ) {
throw new Error( "dateutil parser can't parse non-strings." );
}
for ( var dtype in date_parsers ) {
if ( date_parsers[dtype].test.test( str ) ) {
d = date_parsers[ dtype ].parse( str );
d.type = dtype;
d.size = d.size || 0;
break;
}
}
// default parser supports RFC and a few more, or returns an "invalid date"
if ( !d ) {
d = new Date( str );
d.size = 0;
d.type = 'unknown_date';
}
return d;
};
// format a date to string
__global__.format = function ( d, fmt, lang ) {
if ( arguments.length === 1 && this instanceof Date ) {
fmt = d;
d = this;
}
else if ( !(d instanceof Date) ) {
throw new Error('No date passed to format.');
}
for ( var r=[], c, l=fmt.length, i=0; i<l; i++ ) {
c = fmt.charAt( i );
// format characters
if ( c !== '\\' ) {
r.push( (c in date_formatters) ? date_formatters[ c ]( d, lang ) : c );
}
// escaped characters & unreconized characters
else {
c = i < fmt.length ? fmt.charAt( ++i ) : c;
r.push( c );
}
}
return r.join( '' );
};
// return a Date object for the current date (0 time)
__global__.today = function () {
return __global__.set( __global__.date(), {
hour: 0, minute: 0, second: 0, millisecond: 0
});
};
// return timestamp for the moment
__global__.now = ( typeof Date.now === 'function' )
? Date.now
: function () { return +new Date(); };
// translation hook
__global__._ = function ( s, lang ) {
var l = lang && __global__.lang[ lang ];
return ( l && s in l ) ? l[s] : s;
};
}(
(typeof module !== 'undefined' && module.exports)
? module.exports
: (this.dateutil = {})
));
+25
View File
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=utf-8 />
<title>dateutil tests</title>
<!-- links to qunit + jquery -->
<link rel="stylesheet" href="http://github.com/jquery/qunit/raw/master/qunit/qunit.css" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="http://github.com/jquery/qunit/raw/master/qunit/qunit.js"></script>
<!-- JSON tests -->
<script src="dateutil.js"></script>
<script src="dateutil.testdates.js"></script>
<script src="dateutil.test.js"></script>
</head>
<body>
<h1 id="qunit-header">dateutil tests</h1>
<h2 id="qunit-banner"></h2>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
</body>
</html>
+396
View File
@@ -0,0 +1,396 @@
test('dateutil', function () {
ok( typeof dateutil === 'object', 'have dateutil' );
});
test("dateutil.isLeapYear", function () {
// expect( 10 );
ok( typeof dateutil.isLeapYear === 'function', 'dateutil.isLeapYear() is a function' );
same( dateutil.isLeapYear(1468), true , 'dateutil.isLeapYear(1468)' );
same( dateutil.isLeapYear(1600), true , 'dateutil.isLeapYear(1600)' );
same( dateutil.isLeapYear(1852), true , 'dateutil.isLeapYear(1852)' );
same( dateutil.isLeapYear(1920), true , 'dateutil.isLeapYear(1920)' );
same( dateutil.isLeapYear(1968), true , 'dateutil.isLeapYear(1968)' );
same( dateutil.isLeapYear(2000), true , 'dateutil.isLeapYear(2000)' );
same( dateutil.isLeapYear(2008), true , 'dateutil.isLeapYear(2008)' );
same( dateutil.isLeapYear(2400), true , 'dateutil.isLeapYear(2400)' );
same( dateutil.isLeapYear(2800), true , 'dateutil.isLeapYear(2800)' );
same( dateutil.isLeapYear(1933), false, 'dateutil.isLeapYear(1933)' );
same( dateutil.isLeapYear(1999), false, 'dateutil.isLeapYear(1999)' );
same( dateutil.isLeapYear(1800), false, 'dateutil.isLeapYear(1800)' );
same( dateutil.isLeapYear(1900), false, 'dateutil.isLeapYear(1900)' );
same( dateutil.isLeapYear(2100), false, 'dateutil.isLeapYear(2100)' );
same( dateutil.isLeapYear(2200), false, 'dateutil.isLeapYear(2200)' );
});
test("dateutil.daysInMonth", function () {
// expect( 10 );
ok( typeof dateutil.daysInMonth === 'function', 'dateutil.daysInMonth() is a function' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 0,1) )), 31, 'dateutil.daysInMonth( Jan. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 1,1) )), 28, 'dateutil.daysInMonth( Feb. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 2,1) )), 31, 'dateutil.daysInMonth( Mar. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 3,1) )), 30, 'dateutil.daysInMonth( Apr. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 4,1) )), 31, 'dateutil.daysInMonth( May. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 5,1) )), 30, 'dateutil.daysInMonth( Jun. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 6,1) )), 31, 'dateutil.daysInMonth( Jul. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 7,1) )), 31, 'dateutil.daysInMonth( Aug. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 8,1) )), 30, 'dateutil.daysInMonth( Sep. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999, 9,1) )), 31, 'dateutil.daysInMonth( Oct. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999,10,1) )), 30, 'dateutil.daysInMonth( Nov. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1999,11,1) )), 31, 'dateutil.daysInMonth( Dec. 1999 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 0,1) )), 31, 'dateutil.daysInMonth( Jan. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 1,1) )), 29, 'dateutil.daysInMonth( Feb. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 2,1) )), 31, 'dateutil.daysInMonth( Mar. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 3,1) )), 30, 'dateutil.daysInMonth( Apr. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 4,1) )), 31, 'dateutil.daysInMonth( May. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 5,1) )), 30, 'dateutil.daysInMonth( Jun. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 6,1) )), 31, 'dateutil.daysInMonth( Jul. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 7,1) )), 31, 'dateutil.daysInMonth( Aug. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 8,1) )), 30, 'dateutil.daysInMonth( Sep. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920, 9,1) )), 31, 'dateutil.daysInMonth( Oct. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920,10,1) )), 30, 'dateutil.daysInMonth( Nov. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(1920,11,1) )), 31, 'dateutil.daysInMonth( Dec. 1920 )' );
same( dateutil.daysInMonth(new Date( Date.UTC(2100, 1,1) )), 28, 'dateutil.daysInMonth( Feb. 2100 )' );
});
test('dateutil.isocalendar', function () {
same( dateutil.isocalendar(new Date(Date.UTC(2005, 0, 1))), [2004,53,6], "2004-W53-6" );
same( dateutil.isocalendar(new Date(Date.UTC(2005, 0, 2))), [2004,53,7], "2004-W53-7" );
same( dateutil.isocalendar(new Date(Date.UTC(2005,11,31))), [2005,52,6], "2005-W52-6" );
same( dateutil.isocalendar(new Date(Date.UTC(2007, 0, 1))), [2007, 1,1], "2007-W01-1" );
same( dateutil.isocalendar(new Date(Date.UTC(2007,11,30))), [2007,52,7], "2007-W52-7" );
same( dateutil.isocalendar(new Date(Date.UTC(2007,11,31))), [2008, 1,1], "2008-W01-1" );
same( dateutil.isocalendar(new Date(Date.UTC(2008, 0, 1))), [2008, 1,2], "2008-W01-2" );
same( dateutil.isocalendar(new Date(Date.UTC(2008,11,29))), [2009, 1,1], "2009-W01-1" );
same( dateutil.isocalendar(new Date(Date.UTC(2008,11,31))), [2009, 1,3], "2009-W01-3" );
same( dateutil.isocalendar(new Date(Date.UTC(2009, 0, 1))), [2009, 1,4], "2009-W01-4" );
same( dateutil.isocalendar(new Date(Date.UTC(2009,11,31))), [2009,53,4], "2009-W53-4" );
same( dateutil.isocalendar(new Date(Date.UTC(2010, 0, 3))), [2009,53,7], "2009-W53-7" );
same( dateutil.isocalendar(new Date(Date.UTC(2009,11,31))), [2009,53,4], "2009-W53-4" );
same( dateutil.isocalendar(new Date(Date.UTC(2010, 0, 1))), [2009,53,5], "2009-W53-5" );
same( dateutil.isocalendar(new Date(Date.UTC(2010, 0, 2))), [2009,53,6], "2009-W53-6" );
same( dateutil.isocalendar(new Date(Date.UTC(2010, 0, 3))), [2009,53,7], "2009-W53-7" );
same( dateutil.isocalendar(new Date(Date.UTC(2008,11,28))), [2008,52,7], "2008-W52-7" );
same( dateutil.isocalendar(new Date(Date.UTC(2008,11,29))), [2009, 1,1], "2009-W01-1" );
same( dateutil.isocalendar(new Date(Date.UTC(2008,11,30))), [2009, 1,2], "2009-W01-2" );
same( dateutil.isocalendar(new Date(Date.UTC(2008,11,31))), [2009, 1,3], "2009-W01-3" );
same( dateutil.isocalendar(new Date(Date.UTC(2009, 0, 1))), [2009, 1,4], "2009-W01-4" );
});
test("dateutil.set", function () {
// expect( 10 );
ok( typeof dateutil.set === 'function', 'dateutil.set() is a function' );
var dt = new Date(Date.UTC(1975,9,16,10,24,12,50));
var bt = dateutil.set(new Date(0), {
year: 1975,
month: 9,
day: 16,
hour: 10,
minute: 24,
second: 12,
millisecond: 50
});
ok( +bt == +dt, 'dateutil.set' );
// testing rollover mechanism
var d = dateutil.set(new Date( Date.UTC( 1999, 1, 20 ) ), {
'date': 30,
'month': 0
});
ok( +d === 917654400000, 'prevent rollovers: 1999-02-20 date,month' );
var d = dateutil.set(new Date( Date.UTC( 1999, 1, 20 ) ), {
'month': 0,
'date': 30
});
ok( +d === 917654400000, 'prevent rollovers: 1999-02-20 month,date' );
var d = dateutil.set(new Date( Date.UTC( 1600, 1, 29 ) ), {
'date': 30,
'month': 0
});
same( +d, -11673590400000, 'prevent rollovers: 1600-02-29 date,month' );
var d = dateutil.set(new Date( Date.UTC( 1600, 1, 29 ) ), {
'month': 0,
'date': 30
});
same( +d, -11673590400000, 'prevent rollovers: 1600-02-29 month,date' );
});
test('dateutil.parse', function(){
var corduroy_full = new Date(Date.UTC(2011,10,11, 11,11,11, 111));
var corduroy_sec = new Date(Date.UTC(2011,10,11, 11,11,11));
var corduroy_mfrc = new Date(Date.UTC(2011,10,11, 11,11,6, 660));
var corduroy_min = new Date(Date.UTC(2011,10,11, 11,11));
var corduroy_hfrc = new Date(Date.UTC(2011,10,11, 11,6,39,600));
var corduroy_hour = new Date(Date.UTC(2011,10,11, 11));
var corduroy_day = new Date(Date.UTC(2011,10,11));
var corduroy_mon = new Date(Date.UTC(2011,10,1));
var corduroy_year = new Date(Date.UTC(2011,0,1));
var INVALID = new Date('borked')+'';
// date_and_time
same( +dateutil.parse('2011-11-11T11:11:11.111Z'), +corduroy_full, "2011-11-11T11:11:11.111Z" );
same( +dateutil.parse('2011-11-11T11:11:11Z'), +corduroy_sec, "2011-11-11T11:11:11Z" );
same( +dateutil.parse('2011-11-11T11:11.111Z'), +corduroy_mfrc, "2011-11-11T11:11.111Z" );
same( +dateutil.parse('2011-11-11T11:11Z'), +corduroy_min, "2011-11-11T11:11Z" );
same( +dateutil.parse('2011-11-11T11.111Z'), +corduroy_hfrc, "2011-11-11T11.111Z" );
same( +dateutil.parse('2011-11-11T11Z'), +corduroy_hour, "2011-11-11T11Z" );
same( +dateutil.parse('2011-11-11T11:11:11.111'), +corduroy_full, "2011-11-11T11:11:11.111" );
same( +dateutil.parse('2011-11-11T11:11:11'), +corduroy_sec, "2011-11-11T11:11:11" );
same( +dateutil.parse('2011-11-11T11:11.111'), +corduroy_mfrc, "2011-11-11T11:11.111" );
same( +dateutil.parse('2011-11-11T11:11'), +corduroy_min, "2011-11-11T11:11" );
same( +dateutil.parse('2011-11-11T11.111'), +corduroy_hfrc, "2011-11-11T11.111" );
same( +dateutil.parse('2011-11-11T11'), +corduroy_hour, "2011-11-11T11" );
same( +dateutil.parse('2011-11-11T11:11:11.111+00:00'), +corduroy_full, "2011-11-11T11:11:11.111+00:00" );
same( +dateutil.parse('2011-11-11T11:11:11+00:00'), +corduroy_sec, "2011-11-11T11:11:11+00:00" );
same( +dateutil.parse('2011-11-11T11:11.111+00:00'), +corduroy_mfrc, "2011-11-11T11:11.111+00:00" );
same( +dateutil.parse('2011-11-11T11:11+00:00'), +corduroy_min, "2011-11-11T11:11+00:00" );
same( +dateutil.parse('2011-11-11T11.111+00:00'), +corduroy_hfrc, "2011-11-11T11.111+00:00" );
same( +dateutil.parse('2011-11-11T11+00:00'), +corduroy_hour, "2011-11-11T11+00:00" );
same( +dateutil.parse('2011-11-11T11:11:11.111+00'), +corduroy_full, "2011-11-11T11:11:11.111+00" );
same( +dateutil.parse('2011-11-11T11:11:11+00'), +corduroy_sec, "2011-11-11T11:11:11+00" );
same( +dateutil.parse('2011-11-11T11:11.111+00'), +corduroy_mfrc, "2011-11-11T11:11.111+00" );
same( +dateutil.parse('2011-11-11T11:11+00'), +corduroy_min, "2011-11-11T11:11+00" );
same( +dateutil.parse('2011-11-11T11.111+00'), +corduroy_hfrc, "2011-11-11T11.111+00" );
same( +dateutil.parse('2011-11-11T11+00'), +corduroy_hour, "2011-11-11T11+00" );
// date
same( +dateutil.parse('2011-11-11'), +corduroy_day, "2011-11-11" );
same( +dateutil.parse('2011-1111'), +corduroy_day, "2011-1111" );
// year_and_month
same( +dateutil.parse('2011-11'), +corduroy_mon, "2011-11" );
same( +dateutil.parse('2011/11'), +corduroy_mon, "2011/11" );
same( ''+dateutil.parse('2011-13'), INVALID, "2011-13" ); // should fail!
// year
same( +dateutil.parse('2011'), +corduroy_year, "2011" );
same( +dateutil.parse('12011'), +new Date(Date.UTC(12011,0,1)), "12011" );
same( +dateutil.parse('-12011'), +new Date(Date.UTC(-12011,0,1)), "-12011" );
// year_and_week
ok( +corduroy_day == +dateutil.parse('2011-W45-5'), "2011-W45-5" );
ok( +corduroy_day == +dateutil.parse('2011W45-5'), "2011W45-5" );
ok( +corduroy_day == +dateutil.parse('2011W455'), "2011W455" );
ok( +new Date(Date.UTC(2011,10,7)) == +dateutil.parse('2011-W45'), "2011-W45" );
ok( +new Date(Date.UTC(2011,10,7)) == +dateutil.parse('2011W45'), "2011W45" );
// year_and_ordinal
same( +dateutil.parse('-12011-314'), +new Date( Date.UTC( -12011,0,314 ) ), "-12011-314" );
same( +dateutil.parse('2011-314'), +new Date( Date.UTC( 2011,0,314 ) ), "2011-314" );
same( ''+dateutil.parse('2011-500'), INVALID, "2011-500" ); // should fail!
// year_and_quarter
var corduroy_q = new Date( Date.UTC( 2011,9,1 ) );
ok( +dateutil.parse('-12011-Q4') == +new Date(Date.UTC(-12011,9,1)), "-12011-Q4" );
ok( +corduroy_q == +dateutil.parse('2011Q4'), "2011Q4" );
ok( +corduroy_q == +dateutil.parse('2011-Q4'), "2011-Q4" );
ok( +corduroy_q == +dateutil.parse('2011q4'), "2011q4" );
ok( +corduroy_q == +dateutil.parse('2011-q4'), "2011-q4" );
});
test("dateutil.parse: JSON style calendar dates", function () {
for (var i=0,l=test_dates.length; i<l; i++) {
var test_date = test_dates[i];
var parsed_caldate = dateutil.parse( test_date[0] );
var org_timestamp = test_date[2];
with (Math) {
// I anticipate that there may be some floating point artifacts
// in the milliseconds. Currently allowing a variation of a about
// a microsecond
var diff = parsed_caldate - floor( org_timestamp * 1000 );
ok(
abs( diff ) <= 1,
test_date[0]
);
}
}
});
test("dateutil.parse: yyyy-Www-d", function () {
for (var i=0,l=test_dates.length; i<l; i++) {
var test_date = test_dates[i];
with (Math) {
var d = new Date( floor( test_date[2] * 1000 ) );
d.setUTCHours(0); d.setUTCMinutes(0); d.setUTCSeconds(0); d.setUTCMilliseconds(0);
same( +dateutil.parse( test_date[1] ), +d, test_date[1] );
}
}
});
test("dateutil.format", function () {
// expect( 10 );
ok( typeof dateutil.format === 'function', 'dateutil.format() is a function' );
var d = new Date( Date.UTC( 2001, 2, 10, 17, 16, 18, 13 ) );
same( dateutil.format( d, "F j, Y, g:i a"), "March 10, 2001, 5:16 pm", "F j, Y, g:i a" );
same( dateutil.format( d, "m.d.y"), "03.10.01", "m.d.y" );
same( dateutil.format( d, "j, n, Y"), "10, 3, 2001", "j, n, Y" );
same( dateutil.format( d, "Ymd"), "20010310", "Ymd" );
same( dateutil.format( d, 'h-i-s, j-m-y, it is w Day'), "05-16-18, 10-03-01, 1631 1618 6 Satpm01", "h-i-s, j-m-y, it is w Day" );
same( dateutil.format( d, '\\i\\t \\i\\s \\t\\h\\e jS \\d\\a\\y.'), "it is the 10th day.", "\\i\\t \\i\\s \\t\\h\\e jS \\d\\a\\y." );
same( dateutil.format( d, "D M j G:i:s T Y"), "Sat Mar 10 17:16:18 UTC 2001", "D M j G:i:s T Y" );
same( dateutil.format( d, 'H:m:s \\m \\i\\s\\ \\m\\o\\n\\t\\h'), "17:03:18 m is month", "H:m:s \\m \\i\\s\\ \\m\\o\\n\\t\\h" );
same( dateutil.format( d, "H:i:s"), "17:16:18", "Format: 17:16:18" );
var d = new Date( Date.UTC( 2006, 7, 10, 15, 5, 8, 123 ) );
equals( dateutil.format( d, 'd'), '10', 'format: d' );
equals( dateutil.format( d, 'D'), 'Thu', 'format: D' );
equals( dateutil.format( d, 'j'), '10', 'format: j' );
equals( dateutil.format( d, 'l'), 'Thursday', 'format: l' );
equals( dateutil.format( d, 'N'), '4', 'format: N' );
equals( dateutil.format( d, 'S'), 'th', 'format: S' );
equals( dateutil.format( d, 'w'), '4', 'format: w' );
equals( dateutil.format( d, 'z'), '221', 'format: z' );
equals( dateutil.format( d, 'W'), '32', 'format: W' );
equals( dateutil.format( d, 'F'), 'August', 'format: F' );
equals( dateutil.format( d, 'm'), '08', 'format: m' );
equals( dateutil.format( d, 'M'), 'Aug', 'format: M' );
equals( dateutil.format( d, 'n'), '8', 'format: n' );
equals( dateutil.format( d, 't'), '31', 'format: t' );
equals( dateutil.format( d, 'L'), '0', 'format: L' );
equals( dateutil.format( d, 'o'), '2006', 'format: o' );
equals( dateutil.format( d, 'Y'), '2006', 'format: Y' );
equals( dateutil.format( d, 'y'), '06', 'format: y' );
equals( dateutil.format( d, 'a'), 'pm', 'format: a' );
equals( dateutil.format( d, 'A'), 'PM', 'format: A' );
equals( dateutil.format( d, 'B'), 'B', 'format: B' ); // not supported
equals( dateutil.format( d, 'g'), '3', 'format: g' );
equals( dateutil.format( d, 'G'), '15', 'format: G' );
equals( dateutil.format( d, 'h'), '03', 'format: h' );
equals( dateutil.format( d, 'H'), '15', 'format: H' );
equals( dateutil.format( d, 'i'), '05', 'format: i' );
equals( dateutil.format( d, 's'), '08', 'format: s' );
equals( dateutil.format( d, 'u'), '000123', 'format: u' );
equals( dateutil.format( d, 'e'), 'UTC', 'format: e' );
equals( dateutil.format( d, 'I'), 'I', 'format: I' ); // not supported
equals( dateutil.format( d, 'O'), '+0000', 'format: O' );
equals( dateutil.format( d, 'P'), '+00:00', 'format: P' );
equals( dateutil.format( d, 'T'), 'UTC', 'format: T' );
equals( dateutil.format( d, 'Z'), 'Z', 'format: Z' ); // not supported
equals( dateutil.format( d, 'c'), '2006-08-10T15:05:08.123Z', 'format: c' );
equals( dateutil.format( d, 'r'), 'Thu, 10 Aug 2006 15:05:08 +0000', 'format: r' );
equals( dateutil.format( d, 'U'), '1155222308', 'format: U' );
var d = new Date( Date.UTC( 1961, 0, 3, 1, 51, 10, 1 ) );
equals( dateutil.format( d, 'd'), '03', 'format: d' );
equals( dateutil.format( d, 'D'), 'Tue', 'format: D' );
equals( dateutil.format( d, 'j'), '3', 'format: j' );
equals( dateutil.format( d, 'l'), 'Tuesday', 'format: l' );
equals( dateutil.format( d, 'N'), '2', 'format: N' );
equals( dateutil.format( d, 'S'), 'rd', 'format: S' );
equals( dateutil.format( d, 'w'), '2', 'format: w' );
equals( dateutil.format( d, 'z'), '2', 'format: z' );
equals( dateutil.format( d, 'W'), '01', 'format: W' );
equals( dateutil.format( d, 'F'), 'January', 'format: F' );
equals( dateutil.format( d, 'm'), '01', 'format: m' );
equals( dateutil.format( d, 'M'), 'Jan', 'format: M' );
equals( dateutil.format( d, 'n'), '1', 'format: n' );
equals( dateutil.format( d, 't'), '31', 'format: t' );
equals( dateutil.format( d, 'L'), '0', 'format: L' );
equals( dateutil.format( d, 'o'), '1961', 'format: o' );
equals( dateutil.format( d, 'Y'), '1961', 'format: Y' );
equals( dateutil.format( d, 'y'), '61', 'format: y' );
equals( dateutil.format( d, 'a'), 'am', 'format: a' );
equals( dateutil.format( d, 'A'), 'AM', 'format: A' );
equals( dateutil.format( d, 'B'), 'B', 'format: B' ); // unsupported
equals( dateutil.format( d, 'g'), '1', 'format: g' );
equals( dateutil.format( d, 'G'), '1', 'format: G' );
equals( dateutil.format( d, 'h'), '01', 'format: h' );
equals( dateutil.format( d, 'H'), '01', 'format: H' );
equals( dateutil.format( d, 'i'), '51', 'format: i' );
equals( dateutil.format( d, 's'), '10', 'format: s' );
equals( dateutil.format( d, 'u'), '000001', 'format: u' );
equals( dateutil.format( d, 'e'), 'UTC', 'format: e' );
equals( dateutil.format( d, 'I'), 'I', 'format: I' ); // not supported
equals( dateutil.format( d, 'O'), '+0000', 'format: O' );
equals( dateutil.format( d, 'P'), '+00:00', 'format: P' );
equals( dateutil.format( d, 'T'), 'UTC', 'format: T' );
equals( dateutil.format( d, 'Z'), 'Z', 'format: Z' ); // not supported
equals( dateutil.format( d, 'c'), '1961-01-03T01:51:10.001Z', 'format: c' );
equals( dateutil.format( d, 'r'), 'Tue, 03 Jan 1961 01:51:10 +0000', 'format: r' );
equals( dateutil.format( d, 'U'), '-283817329', 'format: U' );
// allow transfering to prototype
Date.prototype.format = dateutil.format;
var d = new Date( Date.UTC(1975, 9, 16) );
equals( d.format('Y-m-d'), "1975-10-16", 'format prototype assignment' );
try { delete Date.prototype.format; }
catch (err) {}
});
test("dateutil.today", function () {
// expect( 10 );
ok( typeof dateutil.today === 'function', 'dateutil.today() is a function' );
var d = new Date();
var today = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
same( +dateutil.today(), +today, 'dateutil.today()' );
same( +dateutil.today(), +dateutil.set(new Date, { hour:0, minute:0, second:0, millisecond:0 }), 'dateutil.today() vs. dateutil.set()' );
});
test("dateutil.now", function () {
var d = new Date(), now = dateutil.now();
// expect( 10 );
ok( typeof dateutil.now === 'function', 'dateutil.now() is a function' );
same( +d, +now, 'dateutil.now()' );
});
test("dateutil._", function () {
ok( typeof dateutil._ === 'function', 'dateutil._() is a function' );
// provide translations for icelandic:
dateutil.lang.is = {
"January":"janúar", "February":"febrúar", "March":"mars", "April":"apríl", "May":"maí", "June":"júní", "July":"júlí", "August":"ágúst", "September":"september", "October":"október", "November":"nóvember", "December":"desember",
"Sunday":"sunnudagur", "Monday":"mánudagur", "Tuesday":"þriðjudagur", "Wednesday":"miðvikudagur", "Thursday":"fimmtudagur", "Friday":"föstudagur", "Saturday":"laugardagur",
"Jan":"jan", "Feb":"feb", "Mar":"mar", "Apr":"apr", "Jun":"jún", "Jul":"júl", "Aug":"ágú", "Sep":"sep", "Oct":"okt", "Nov":"nóv", "Dec":"des",
"Sun":"sun", "Mon":"mán", "Tue":"þri", "Wed":"mið", "Thu":"fim", "Fri":"fös", "Sat":"lau"
};
var d = new Date( Date.UTC( 2006, 7, 10, 15, 5, 8, 123 ) );
equals( dateutil.format( d, 'D', 'is' ), 'fim', 'format: D' );
equals( dateutil.format( d, 'l', 'is' ), 'fimmtudagur', 'format: l' );
equals( dateutil.format( d, 'F', 'is' ), 'ágúst', 'format: F' );
equals( dateutil.format( d, 'M', 'is' ), 'ágú', 'format: M' );
dateutil._ = function () { return 'BORK'; };
equals( dateutil.format( d, 'D', 'is' ), 'BORK', 'format: D' );
equals( dateutil.format( d, 'l', 'is' ), 'BORK', 'format: l' );
equals( dateutil.format( d, 'F', 'is' ), 'BORK', 'format: F' );
equals( dateutil.format( d, 'M', 'is' ), 'BORK', 'format: M' );
});
+164
View File
@@ -0,0 +1,164 @@
/*
* This file implements a subset of the QUnit functions/features to allow
* running simple QUnit tests in Node.JS.
*
* Copyright (c) 2009 Borgar Þorsteinsson
* Licensed under the terms of the MIT (LICENSE.txt) software license.
*
*/
// the files to load for the test, test itself last:
var files = [
'dateutil.js',
'dateutil.testdates.js',
'dateutil.test.js'
];
/* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- */
var sys = require('sys'),
fs = require('fs'),
Script = process.binding('evals').Script,
assert = require('assert')
;
var tests = [];
var current_test;
var sandbox = {
expect: function ( num ) {
current_test.expected = num;
},
ok: function ( value, message ) {
current_test.results.push([ !!value, message ]);
},
equals: function ( rest, exp, msg ) {
try {
assert.equal(rest, exp);
}
catch ( err ) {
current_test.results.push([ false, msg + ', expected: '+exp+' result: ' + rest ]);
return;
}
current_test.results.push([ true, msg ]);
},
same: function ( rest, exp, msg ) {
try {
assert.deepEqual(rest, exp);
}
catch ( err ) {
current_test.results.push([ false, msg + ', expected: '+exp+' result: ' + rest ]);
return;
}
current_test.results.push([ true, msg ]);
},
test: function ( testName /*, expected, callback, async */ ) {
var tmap = {
'object':'environment',
'boolean':'async',
'function':'callback',
'number':'expected'
};
current_test = {
name: testName,
expected: null,
callback: null,
context: {},
async: false, // TODO: async doesn't actually do anything :-/
error: false,
results: []
};
tests.push( current_test );
for (var i=1,l=arguments.length; i<l; i++) {
current_test[ tmap[ typeof arguments[i] ] || 'error' ] = arguments[i];
}
try {
current_test.callback.call( current_test.context );
}
catch ( err ) {
current_test.results.push([ false, err ]);
}
if ( current_test.expected ) {
if ( current_test.expected != current_test.results.length ) {
current_test.results.push([ false, 'Expected '+current_test.expected+' assertions, but '+current_test.results.length+' were run' ]);
}
}
}
}
function pad ( s, l ) {
s = l + s;
return s.substr( s.length - l.length, l.length );
}
// ------------
// -- runner --
// ------------
var test_script = files.map(function ( fn ) {
try {
return fs.readFileSync( fn );
}
catch ( err ) {
console.log('Critical failure: Cannot load ' + fn );
process.exit(1);
}
}).join('\n;\n');
console.log('');
// time it...
console.time('Tests total time');
// run the tests
Script.runInNewContext( test_script, sandbox );
// reports
var total_ok = 0, total_fail = 0, total_tests = 0;
for (var t=0,tl=tests.length; t<tl; t++) {
var test = tests[t];
var report = [];
var ok = 0, fail = 0;
for (var r=0,rl=test.results.length; r<rl; r++) {
var res = test.results[r];
if ( res[0] ) {
ok++;
total_ok++;
}
else {
fail++;
total_fail++;
}
report.push( ' ' + pad(r,' ') + '. ' + (res[0]?'':'[FAIL]: ') + res[1] );
}
total_tests += test.results.length;
console.log( '' + pad(t,' ') + '. ' + test.name + ' ('+fail+', '+ok+', '+(test.results.length)+')' );
if ( fail ) {
console.log( report.join('\n') );
console.log('');
}
}
console.log('');
console.timeEnd('Tests total time');
console.log(total_ok+' tests of '+total_tests+' passed, '+total_fail+' failed');
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
{
"name": "dateutil",
"version": "0.1.0",
"author": "Borgar Þorsteinsson <borgar@borgar.net> (http://borgar.net/)",
"description": "A date parsing and formatting library. Complete ISO 8061 date/time parsing support including quarters and years. Full PHP style date formatting.",
"repository": {
"type": "git",
"url": "git://github.com/borgar/dateutil.git"
},
"main": "./dateutil.js",
"engines": {
"node": "*"
},
"tags": [ "date", "time", "format", "PHP", "parse", "ISO 8601" ],
"bugs": {
"url": "https://github.com/borgar/dateutil/issues"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "webapp",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dateutil": "^0.1.0"
}
},
"node_modules/dateutil": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/dateutil/-/dateutil-0.1.0.tgz",
"integrity": "sha512-bI8Lm50mageQER89AvYr+bAxXn4MCAkjSg3M58QY2evrIypUk2aCQcMZ86f31tjmt2ceMrPkrHkhItYqOJsObw==",
"engines": {
"node": "*"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"dateutil": "^0.1.0"
}
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+5
View File
@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
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.
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).
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "vite-project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"ant-design-vue": "^4.2.6",
"axios": "^1.11.0",
"konva": "^9.3.22",
"querystring": "^0.2.1",
"vue": "^3.5.17",
"vue-konva": "^3.2.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3",
"vite": "^7.0.4",
"vue-tsc": "^2.2.12"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+55
View File
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { ref } from 'vue';
import index from './components/check/index.vue'
import manage from './pages/manage.vue'
const selectedKeys1 = ref (['1']);
let activeItem = ref(1);
const handleSelectd= (e: any) => {
console.log('click ', e);
activeItem.value = e.key;
};
</script>
<template>
<a-layout class="container">
<a-layout-header class="header">
<div class="logo" />
<a-menu
v-model:selectedKeys="selectedKeys1"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
@select="handleSelectd"
>
<a-menu-item key="1">发票查验</a-menu-item>
<a-menu-item key="2">发票管理</a-menu-item>
<a-menu-item key="3">发票复验</a-menu-item>
<a-menu-item key="4">发票统计</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout-content style="padding: 0 10px">
<a-layout style="padding: 24px 0; background: #fff">
<index v-if="activeItem == 1" />
<manage v-if="activeItem == 2" />
</a-layout>
</a-layout-content>
</a-layout>
</template>
<style scoped>
#components-layout-demo-top-side .logo {
float: left;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side .logo {
float: right;
margin: 16px 0 16px 24px;
}
.container {
height: 100%;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

@@ -0,0 +1,161 @@
<template>
<a-layout-content>
<a-row >
<a-col :mx="15" :sm="24">
<!-- <v-stage v-if="fileType === 'img'" :config="stageConfig">-->
<!-- <v-layer>-->
<!--&lt;!&ndash; <v-image :config="{image: image,x:0,y:0}" />&ndash;&gt;-->
<!-- <v-shape :config="{x: 100, y: 100, width: 100, height: 100, fill: 'red'}" />-->
<!-- </v-layer>-->
<!-- </v-stage>-->
<img v-if="fileType === 'img'" :src="fileUrl" alt="" class="pdf" />
<iframe v-if="fileType==='pdf'" :src="fileUrl" class="pdf">
</iframe>
</a-col>
<a-col :mx="8" :sm="24">
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item label="发票类型">
<a-input v-model:value="formState.invoiceType" />
</a-form-item>
<a-form-item label="发票号码">
<a-input v-model:value="formState.invoiceNumber" />
</a-form-item>
<a-form-item label="开票日期">
<a-input v-model:value="formState.invoiceDate" />
</a-form-item>
<a-form-item label="不含税金额">
<a-input v-model:value="formState.invoiceAmountPreTax" />
</a-form-item>
<a-form-item label="税额">
<a-input v-model:value="formState.invoiceTax" />
</a-form-item>
<a-form-item label="价税合计">
<a-input v-model:value="formState.totalAmount" />
</a-form-item>
<a-form-item label="销方名称">
<a-input v-model:value="formState.sellerName" />
</a-form-item>
<a-form-item label="销方税号">
<a-input v-model:value="formState.sellerTaxNumber" />
</a-form-item>
<a-form-item label="购方名称">
<a-input v-model:value="formState.purchaserName" />
</a-form-item>
<a-form-item label="购方税号">
<a-input v-model:value="formState.purchaserTaxNumber" />
</a-form-item>
<a-form-item label="发票代码">
<a-input v-model:value="formState.invoiceCode" />
</a-form-item>
<!-- <a-form-item >-->
<!-- <a-row>-->
<!-- <a-col :span="12" offset="24">-->
<!-- <a-space>-->
<!-- <a-button type="primary" html-type="submit">识别发票</a-button>-->
<!-- </a-space>-->
<!-- </a-col>-->
<!-- </a-row>-->
<!-- </a-form-item>-->
</a-form>
</a-col>
</a-row>
</a-layout-content>
</template>
<script lang="ts" setup>
import {ref,onMounted } from 'vue';
import request from "../../utils/request.ts";
import {baseURL} from "../../utils/baseurl.ts";
export type FormState = {
"invoiceCode": string,//发票代码
"invoiceNumber": string,//发票号码
"invoiceDate": string,//开票日期
"purchaserName": string,//购方名称
"purchaserTaxNumber": string,//购方税号
"invoiceAmountPreTax": number,//不含税金额
"invoiceTax": number,//税额
"totalAmount": number,//价税合计
"sellerName": string,//销方名称
"sellerTaxNumber": string,//销方税号
"sellerContactInfo": string,//销方联系方式
"sellerBankAccountInfo": string,
"drawer": string,//开票人
"remarks": string,//备注
"title": string,//标题
"invoiceType": string,//发票类型
"specialTag": string,//特殊标记
"invoiceDetails": [{
"itemName": string,//项目名称
"amount": number,//金额
"taxRate": number,//税率
"tax": number//税额
}]
}
const props = defineProps({ filePath: String,formState: String,});
const emit= defineEmits(['update:formState'])
const fileType = ref<string>('img')
onMounted(() => {
// alert(props.filePath)
if(props.filePath){
fileType.value = props.filePath.split('.').pop() ==='pdf'?'pdf':'img'
request.get('/recognize?filePath='+encodeURIComponent(props.filePath)).then((res) => {
console.log(res.data)
formState.value = res.data
emit('update:formState', JSON.stringify(res.data))
})
fileUrl.value = baseURL+"/"+props.filePath
}
})
//
const fileUrl = ref('')
let formState = ref<FormState>({
drawer: "",
invoiceAmountPreTax: 0,
invoiceCode: "",
invoiceDate: "",
invoiceDetails: [{amount: 0, itemName: "", tax: 0, taxRate: 0}],
invoiceNumber: "",
invoiceTax: 0,
invoiceType: "",
purchaserName: "",
purchaserTaxNumber: "",
remarks: "",
sellerBankAccountInfo: "",
sellerContactInfo: "",
sellerName: "",
sellerTaxNumber: "",
specialTag: "",
title: "",
totalAmount: 0
});
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
</script>
<style scoped>
.pdf{
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,75 @@
<script setup lang="ts">
import {h, ref} from 'vue';
import {
UploadOutlined,
FileDoneOutlined,
FileProtectOutlined,
// SmileOutlined,
} from '@ant-design/icons-vue'
import type {StepProps} from "ant-design-vue"
import Check from './Check.vue'
import upload from './upload.vue'
import Report from "./report.vue"
// 步骤
const items = [
{
title: '上传发票',
icon: h(UploadOutlined),
},
{
title: '识别发票',
icon: h(FileDoneOutlined),
},
{
title: '验证发票',
icon: h(FileProtectOutlined),
},
] as StepProps[];
// 文件路径
const filePath = ref('');
const current = ref<number>(0);
// 下一步
const nextPage = () => {
current.value ++;
};
// 上一步
const previousPage = () => {
current.value--;
};
// 保存
const saveReport = () => {
current.value = 0
}
// 表单数据
const formState = ref<string>('');
</script>
<template>
<a-steps :current="current" :items="items"></a-steps>
<a-layout>
<a-layout-content class="container">
<upload v-if="current == 0" v-model:filePath="filePath" />
<Check v-if="current == 1" v-model:filePath="filePath" v-model:formState="formState"/>
<report v-if="current == 2" v-model:filePath="filePath" v-model:formState="formState" />
</a-layout-content>
<a-layout-footer>
<a-row>
<a-col :span="12" offset="16">
<a-space>
<a-button type="default" v-if="current > 0" @click="previousPage">上一步</a-button>
<a-button type="primary" v-if="current < items.length-1 " :disabled="!filePath" @click="nextPage">下一步</a-button>
<a-button type="dashed" v-if="current == items.length-1" @click="saveReport">完成</a-button>
</a-space>
</a-col>
</a-row>
</a-layout-footer>
</a-layout>
</template>
<style scoped>
.ant-steps{
padding: 0 20px 20px 20px;
}
</style>
@@ -0,0 +1,52 @@
<template>
<a-layout-content>
<a-spin v-if="loadding">
</a-spin>
<a-result
:status="formState.status"
:title="formState.cyjgxx"
>
<template #subTitle >
<p v-if="formState.status === 'success'">查验次数: {{formState.inspectionAmount}}</p>
<p v-if="formState.status === 'success'">已存入发票库</p>
</template>
</a-result>
</a-layout-content>
</template>
<script lang="ts" setup>
import {ref,onMounted } from 'vue';
import request from "../../utils/request.ts";
const props = defineProps({ filePath: String });
import {baseURL} from "../../utils/baseurl.ts";
let loadding = ref(true)
const fileType = ref<string>('img')
onMounted(() => {
// alert(props.filePath)
if(props.filePath){
fileType.value = props.filePath.split('.').pop() ==='pdf'?'pdf':'img'
request.get('/verify?filePath='+encodeURIComponent(props.filePath)).then((res) => {
console.log(res)
formState.value = res.data
loadding.value = false
})
fileUrl.value = baseURL+"/"+props.filePath
}
})
//
const fileUrl = ref('')
let formState = ref({
status:"",
inspectionAmount: "",
cyjgxx: "",
verify_time: "",
});
</script>
<style scoped>
.pdf{
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,80 @@
<script setup lang="ts">
import type { UploadProps} from "ant-design-vue";
import {ref} from "vue";
import {
UploadOutlined
} from '@ant-design/icons-vue';
import {baseURL} from "../../utils/baseurl.ts";
const fileList1 = ref<UploadProps['fileList']>([]);
const props = defineProps({ filePath: String });
const emit= defineEmits(['update:filePath'])
const handleRemove = (file: UploadProps['fileList']) => {
console.log( file)
return false;
};
console.log(props.filePath)
const handleUpload = (options: any) => {
const { file, onSuccess, onError, onProgress } = options;
// 创建 FormData 对象
const formData = new FormData();
formData.append('file', file);
// 使用 XMLHttpRequest 进行上传
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress({ percent: (event.loaded / event.total) * 100 }, file);
}
};
// 上传成功回调
xhr.onload = () => {
if (xhr.status === 200 || xhr.status === 201) {
console.log(xhr.responseText);
onSuccess(xhr.responseText, file);
let res = JSON.parse(xhr.responseText)
emit('update:filePath', res.file_path)
} else {
onError(new Error(`Upload failed with status ${xhr.status}`));
}
};
// 上传错误回调
xhr.onerror = () => {
onError(new Error('Network error'));
};
// 发送请求到服务器
xhr.open('POST', baseURL+'/upload');
xhr.send(formData);
};
</script>
<template>
<a-layout-content class="container">
<a-upload
v-model:file-list="fileList1"
:customRequest="handleUpload"
list-type="picture"
class="upload-list-inline"
:max-count="1"
@remove="handleRemove"
>
<a-button>
<upload-outlined></upload-outlined>
上传发票
</a-button>
</a-upload>
</a-layout-content>
</template>
<style scoped>
.container {
margin-top: 50px;
height: 100%;
}
</style>
+11
View File
@@ -0,0 +1,11 @@
import { createApp } from 'vue'
import Antd from 'ant-design-vue';
import './style.css'
import App from './App.vue'
import 'ant-design-vue/dist/reset.css';
import VueKonva from 'vue-konva';
const app = createApp(App)
app.use(Antd);
app.use(VueKonva);
app.mount('#app')
+360
View File
@@ -0,0 +1,360 @@
<script setup lang="ts">
import {h, onMounted, ref} from "vue";
import {
SearchOutlined,
FileSearchOutlined,
PlusOutlined
} from '@ant-design/icons-vue'
import request from '../utils/request.ts';
import {baseURL} from "../utils/baseurl.ts";
import type {TableProps, UploadChangeParam, UploadProps} from "ant-design-vue";
import { message } from 'ant-design-vue';
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import type {UploadFile} from "ant-design-vue/es/upload/interface";
import type {Key} from "ant-design-vue/es/_util/type";
const [messageApi, contextHolder] = message.useMessage();
interface DataType {
id: string;
status: string;
desc: string;
desc_list: string;
}
const columns = [
// {
// title: '全选',
// dataIndex: 'status',
// key: 'status',
// showSelection: true,
// },
{
title: '发票号码',
dataIndex: 'invoiceNumber',
key: 'invoiceNumber',
// showSelection: true,
},
{
title: '销方名称',
dataIndex: 'sellerName',
key: 'sellerName',
},
{
title: '销方税号',
dataIndex: 'sellerTaxNumber',
key: 'sellerTaxNumber',
},
{
title: '不含税金额',
dataIndex: 'invoiceAmountPreTax',
key: 'invoiceAmountPreTax',
},
{
title: '税额',
dataIndex: 'invoiceTax',
key: 'invoiceTax',
},
{
title: '价税合计',
dataIndex: 'totalAmount',
key: 'totalAmount',
},
{
title: '开票日期',
dataIndex: 'invoiceDate',
key: 'invoiceDate',
},
{
title: '查验结果',
dataIndex: 'verify_status',
key: 'verify_status',
},
{
title: '查验次数',
dataIndex: 'inspectionAmount',
key: 'inspectionAmount',
},
{
title: '查验时间',
dataIndex: 'verify_time',
key: 'verify_time',
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
}
]
// 数据
let data = ref( [])
let searchParams = ref({
page: 1,
pageSize: 100,
total: 0,
params: {
verify_status: 'success',
value: null,
verify_time: null,
}
});
// 下拉框
const rowSelection: TableProps['rowSelection'] = {
onChange: (selectedRowKeys: Key[], selectedRows: DataType[]) => {
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
getCheckboxProps: (record: DataType) => ({
disabled: record.status === 'yes', // Column configuration not to be checked
name: record.status,
}),
};
// 弹窗
const openModal = ref(false);
const modalTitle = ref('')
const confirmLoading = ref(false);
const formData = ref({
id: '',
status: false,
desc: '',
desc_files: '',
fileIds: new Array<String>(),
});
const handleOk = () => {
confirmLoading.value = true;
request.post("/updateInvoice", {
invoiceId: formData.value.id,
status: formData.value.status?'yes':'no',
desc: formData.value.desc,
desc_files: formData.value.fileIds?.join(','),
}).then(( res ) => {
if(res.status == 200){
openModal.value = false;
confirmLoading.value = false;
messageApi.success("修改成功")
getList();//刷新
}
})
};
const handleOpen = (record:any) => {
if (record.status === 'yes') {
modalTitle.value = "备注"
}else {
modalTitle.value = "入账"
}
formData.value = Object.assign({}, record);
formData.value.status = (record.status === 'yes')
if (record.desc_files) {
formData.value.fileIds = record.desc_files.split(',');
let paths = record.desc_files.split(",")
fileList.value = []
for(let i = 0; i < paths.length; i++) {
fileList.value.push({
uid: paths[i],
name: paths[i].split("/")[2],
url: baseURL+"/"+paths[i],
});
}
}
openModal.value = true;
};
// 数据列表
const getList = () => {
request.post("/listInvoice", {
verify_status : searchParams.value.params.verify_status,
value : searchParams.value.params.value,
verify_time : searchParams.value.params.verify_time,
}).then(( res ) => {
console.log(res)
data.value = res.data
})
}
const fileList = ref<UploadProps['fileList']>([])
const previewVisible = ref(false);
const previewImage = ref('');
const previewTitle = ref('');
function getBase64(file: File) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
//
const handleCancelPreview = () => {
previewVisible.value = false;
previewTitle.value = '';
};
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = (await getBase64(file.originFileObj)) as string;
}
previewImage.value = file.url || file.preview;
previewVisible.value = true;
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
};
const handleFileChange = (info: UploadChangeParam) => {
if(info.file.status === 'done'){
info.file.uid = info.file.response.file_path
if (!formData.value.fileIds){
formData.value.fileIds = new Array<String>();
}
formData.value.fileIds.push(info.file.response.file_path)
}
}
const handleFileRemove = (file: UploadFile ) => {
let index = formData.value.fileIds.indexOf(file.uid)
formData.value.fileIds.splice(index, 1);
}
const handleUpload = (options: any) => {
const { file, onSuccess, onError, onProgress } = options;
// 创建 FormData 对象
const formData1 = new FormData();
formData1.append('file', file);
// 使用 XMLHttpRequest 进行上传
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress({ percent: (event.loaded / event.total) * 100 }, file);
}
};
// 上传成功回调
xhr.onload = () => {
if (xhr.status === 200 || xhr.status === 201) {
console.log(xhr.responseText);
onSuccess(xhr.responseText, file);
let res = JSON.parse(xhr.responseText)
if(formData.value.desc_files){
formData.value.desc_files += ",";
}else{
formData.value.desc_files = ""
}
formData.value.desc_files += res.file_path;
} else {
onError(new Error(`Upload failed with status ${xhr.status}`));
}
};
// 上传错误回调
xhr.onerror = () => {
onError(new Error('Network error'));
};
// 发送请求到服务器
xhr.open('POST', baseURL+'/upload');
xhr.send(formData1);
};
dayjs.locale('zh-cn')
onMounted(() => {
getList()
})
</script>
<template>
<div class="table-operations">
<a-row :gutter="20" >
<a-col :span="22" :align="'end'" offset="2">
<a-form v-model:value="searchParams" layout="inline">
<a-form-item label="验证时间">
<a-range-picker
v-model:value="searchParams.params.verify_time"
valueFormat="YYYY-MM-DD HH:mm:ss"
:locale="zhCN.DatePicker"
></a-range-picker>
</a-form-item>
<a-form-item label="关键字">
<a-input v-model:value="searchParams.params.value" allow-clear></a-input>
</a-form-item>
<a-form-item label="验证结果">
<a-radio-group v-model:value="searchParams.params.verify_status">
<a-radio-button value="success">正常</a-radio-button>
<a-radio-button value="faild">异常</a-radio-button>
<a-radio-button value="all">全部</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-button type="primary" :icon="h(SearchOutlined)" @click="getList">搜索</a-button>
</a-form-item>
</a-form>
</a-col>
</a-row>
</div>
<a-table :columns="columns" :data-source="data" :row-selection="rowSelection" rowKey="id" >
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'verify_status'">
<a-tag v-if="record.verify_status === 'success'" color="green">正常</a-tag>
<a-tag v-if="record.verify_status === 'fail'" color="red">异常</a-tag>
</template>
<template v-if="column.key === 'action'" >
<a-button type="primary" v-if="record.status ==='yes'" @click="handleOpen(record)">备注</a-button>
<a-button type="primary" v-if="record.status ==='no'" @click="handleOpen(record)">报销</a-button>
</template>
</template>
<template #expandedRowRender="{ record }">
<a-row >
<a-col :span="8">
<a-image-preview-group v-if="record.desc_files">
<a-image :width="200" v-for="file in record.desc_files.split(',')" :src="baseURL+'/'+file"/>
</a-image-preview-group>
</a-col>
<a-col :span="16">
<a-typography-title :level="4" >备注</a-typography-title>
<a-typography-paragraph>
<blockquote>{{ record.desc }}</blockquote>
</a-typography-paragraph>
</a-col>
</a-row>
</template>
<template #expandColumnTitle>
<FileSearchOutlined />
</template>
</a-table>
<a-modal v-model:open="openModal" :title="modalTitle" :confirm-loading="confirmLoading" @ok="handleOk">
<a-form v-model:value="formData" >
<a-form-item label="是否已报销">
<a-switch v-model:checked="formData.status"></a-switch>
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="formData.desc" :rows="6"></a-textarea>
</a-form-item>
<a-form-item label="附件">
<div class="clearfix">
<a-upload
v-model:file-list="fileList"
:customRequest="handleUpload"
@change="handleFileChange"
@remove="handleFileRemove"
list-type="picture-card"
@preview="handlePreview"
>
<div>
<PlusOutlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
<a-modal :open="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancelPreview">
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
</a-form-item>
</a-form>
</a-modal>
<context-holder />
</template>
<style scoped>
.table-operations {
margin-bottom: 16px;
text-align: right;
}
.table-operations > button {
margin-right: 8px;
}
</style>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>
+81
View File
@@ -0,0 +1,81 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
/*max-width: 1280px;*/
width: 100%;
height: 100%;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
+2
View File
@@ -0,0 +1,2 @@
// export const baseURL = "http://127.0.0.1:5555"
export const baseURL = "http://101.200.148.149:8555"
+12
View File
@@ -0,0 +1,12 @@
/* request.ts 这里博主用的是 TypeScript */
// 引入 Axios 库的功能,AxiosInstance 是 Axios 中自带的接口类型
import axios, {type AxiosInstance} from 'axios';
import {baseURL} from "./baseurl.ts";
const service: AxiosInstance = axios.create({
baseURL: baseURL,
timeout: 50000,
});
// 导出 axios 实例
export default service;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+26
View File
@@ -0,0 +1,26 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api/': {
changeOrigin: true,
}
}
},
resolve: {
alias: {
'#': '/src'
}
},
build: {
outDir: '../../alibabacloud_sample/templates',
emptyOutDir: true,
},
})