This commit is contained in:
2025-05-03 17:16:53 +08:00
parent 1d0b84e2b0
commit badaa4b64e
20 changed files with 619 additions and 1 deletions

6
.flaskenv Normal file
View File

@ -0,0 +1,6 @@
FLASK_APP=inventory_check_lark
FLASK_ENV=development
SECRET_KEY=asdASS3434dfE5tgfg
LARK_APP_ID=cli_a7359bac85b9d01c
LARK_APP_SECRECT=vZdiNrSiszH9ODD2qzuI1fHRupOwY5wY
LARK_TB_TOKEN=EciWbKOEXa68HIsnh5Ac7vZgnff

View File

@ -1,2 +1,8 @@
# inventory_check_lark
# inventory_check
Flask
gunicorn
python-dotenv
lark-oapi
Flask-SQLAlchemy

BIN
data.db Normal file

Binary file not shown.

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""
:author: luffmims
:url: http://yum2.cc
:copyright: © 2025 luffmims <luffmims@hotmaill.com>
:license: MIT, see LICENSE for more details.
"""
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
application = Flask('inventory_check_lark')
application.config.from_pyfile('settings.py')
application.jinja_env.trim_blocks = True
application.jinja_env.lstrip_blocks = True
db = SQLAlchemy(application)
from inventory_check_lark import views, errors, commands

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
:author: luffmims
:url: http://yum2.cc
:copyright: © 2025 luffmims <luffmims@hotmaill.com>
:license: MIT, see LICENSE for more details.
"""
import click
from inventory_check_lark import application
@application.cli.command()
@click.option('--drop', is_flag=True, help='Create after drop.')
def initdb(drop):
"""Initialize the database."""
click.echo('Initialized database.')

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
:author: luffmims
:url: http://yum2.cc
:copyright: © 2025 luffmims <luffmims@hotmaill.com>
:license: MIT, see LICENSE for more details.
"""
from flask import render_template
from inventory_check_lark import application
@application.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html'), 404
@application.errorhandler(500)
def internal_server_error(e):
return render_template('errors/500.html'), 500

View File

@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
"""
:author: luffmims
:url: http://yum2.cc
:copyright: © 2025 luffmims <luffmims@hotmaill.com>
:license: MIT, see LICENSE for more details.
"""
# from datetime import datetime
from inventory_check_lark import application, db
import json
import time
import lark_oapi as lark
from lark_oapi.api.auth.v3 import *
from lark_oapi.api.bitable.v1 import *
class Inventory(db.Model):
id = db.Column(db.Integer, primary_key=True)
tag = db.Column(db.Text)
amount = db.Column(db.Integer)
checked = db.Column(db.Boolean, default=False)
recordid = db.Column(db.Text)
tableid = db.Column(db.Text)
# lasttime = db.Column(db.DateTime, default=datetime.now())
client = lark.Client.builder() \
.app_id(application.config['LARK_APP_ID']) \
.app_secret(application.config['LARK_APP_SECRECT']) \
.log_level(lark.LogLevel.DEBUG) \
.build()
TIMER = time.time()
def get_access_token():
# 构造请求对象
request: InternalAppAccessTokenRequest = InternalAppAccessTokenRequest.builder() \
.request_body(InternalAppAccessTokenRequestBody.builder()
.app_id(application.config['LARK_APP_ID'])
.app_secret(application.config['LARK_APP_SECRECT'])
.build()) \
.build()
# 发起请求
response: InternalAppAccessTokenResponse = client.auth.v3.app_access_token.internal(request)
# 处理失败返回
# if not response.success():
# lark.logger.error(
# f"client.auth.v3.app_access_token.internal failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
# return
if not response.success():
return False
return True
# # 处理业务结果
# res = eval(response.raw.content)
# return {"access_token": res["tenant_access_token"], "expire": res["expire"], "code": res["code"]}
def get_table_record(id, page_token=None):
global TIMER
if time.time() - TIMER > 7000:
get_access_token()
TIMER = time.time()
# 构造请求对象
if page_token:
request: SearchAppTableRecordRequest = SearchAppTableRecordRequest.builder() \
.app_token(application.config['LARK_TB_TOKEN']) \
.table_id(id['table_id']) \
.page_token(page_token) \
.page_size(50) \
.request_body(SearchAppTableRecordRequestBody.builder()
.view_id(id['view_id'])
.field_names(["规格", "外库数量", "排序", "已盘"])
.sort([Sort.builder()
.field_name("排序")
.desc(False)
.build()
])
.automatic_fields(True)
.build()) \
.build()
else:
request: SearchAppTableRecordRequest = SearchAppTableRecordRequest.builder() \
.app_token(application.config['LARK_TB_TOKEN']) \
.table_id(id['table_id']) \
.page_size(50) \
.request_body(SearchAppTableRecordRequestBody.builder()
.view_id(id['view_id'])
.field_names(["规格", "外库数量", "排序", "已盘"])
.sort([Sort.builder()
.field_name("排序")
.desc(False)
.build()
])
.automatic_fields(True)
.build()) \
.build()
# 发起请求
response: SearchAppTableRecordResponse = client.bitable.v1.app_table_record.search(request)
# 处理失败返回
if not response.success():
lark.logger.error(
f"client.bitable.v1.app_table_record.search failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
return
rep = json.loads(response.raw.content)
res = []
page_token = None
if rep['data']['has_more']:
page_token = rep['data']['page_token']
for i in rep['data']['items']:
if '已盘' in i['fields']:
cd = i['fields']['已盘']
else:
cd = False
res.append({
"sort": int(i['fields']['排序']),
"tag": i['fields']['规格'][0]['text'],
"amount": int(i['fields']['外库数量']) if '外库数量' in i['fields'] else 0,
"checked": cd,
"recordid": i['record_id'],
})
return res, page_token
def update_record(id, recordid, amount):
global TIMER
if time.time() - TIMER > 200:
get_access_token()
TIMER = time.time()
# 构造请求对象
request: UpdateAppTableRecordRequest = UpdateAppTableRecordRequest.builder() \
.app_token(application.config['LARK_TB_TOKEN']) \
.table_id(id['table_id']) \
.record_id(recordid) \
.request_body(AppTableRecord.builder()
.fields({"已盘":True,"外库数量":amount})
.build()) \
.build()
# 发起请求
response: UpdateAppTableRecordResponse = client.bitable.v1.app_table_record.update(request)
# 处理失败返回
# if not response.success():
# lark.logger.error(
# f"client.bitable.v1.app_table_record.update failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
# return
if not response.success():
return False
# 处理业务结果
# lark.logger.info(lark.JSON.marshal(response.raw.content, indent=4))
return True
def init_db():
get_access_token()
with application.app_context():
db.drop_all()
db.create_all()
youxin, page_token= get_table_record(application.config['YOUXIN_ID'])
for i in youxin:
inv = Inventory()
inv.id = i['sort']
inv.tag = i['tag']
inv.amount = i['amount']
inv.checked = i['checked']
inv.recordid = i['recordid']
inv.tableid = application.config['YOUXIN_ID']
db.session.add(inv)
while page_token:
youxin, page_token = get_table_record(application.config['YOUXIN_ID'], page_token)
if not youxin:
break
for i in youxin:
inv = Inventory()
inv.id = i['sort']
inv.tag = i['tag']
inv.amount = i['amount']
inv.checked = i['checked']
inv.recordid = i['recordid']
inv.tableid = application.config['YOUXIN_ID']
db.session.add(inv)
waimo, page_token= get_table_record(application.config['WAIMO_ID'])
for i in waimo:
inv = Inventory()
inv.id = i['sort']
inv.tag = i['tag']
inv.amount = i['amount']
inv.checked = i['checked']
inv.recordid = i['recordid']
inv.tableid = application.config['WAIMO_ID']
db.session.add(inv)
while page_token:
waimo, page_token = get_table_record(application.config['WAIMO_ID'], page_token)
if not waimo:
break
for i in waimo:
inv = Inventory()
inv.id = i['sort']
inv.tag = i['tag']
inv.amount = i['amount']
inv.checked = i['checked']
inv.recordid = i['recordid']
inv.tableid = application.config['WAIMO_ID']
db.session.add(inv)
db.session.commit()
return True

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""
:author: luffmims
:url: http://yum2.cc
:copyright: © 2025 luffmims <luffmims@hotmaill.com>
:license: MIT, see LICENSE for more details.
"""
import os
import sys
from inventory_check_lark import application
# sqlite URI compatible
WIN = sys.platform.startswith('win')
if WIN:
prefix = 'sqlite:///'
else:
prefix = 'sqlite:////'
dev_db = prefix + os.path.join(os.path.dirname(application.root_path), 'data.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URI', dev_db)
SECRET_KEY = os.getenv('SECRET_KEY', 'secret string')
application.config['LARK_APP_ID'] = os.getenv('LARK_APP_ID', 'lark_app_id')
application.config['LARK_APP_SECRECT'] = os.getenv('LARK_APP_SECRECT', 'lark_app_secrect')
application.config['LARK_TB_TOKEN'] = os.getenv('LARK_TB_TOKEN', 'lark_app_secrect')
application.config['WAIMO_ID'] = {
'table_id': 'tbluSufkcQm0noBz',
'view_id': 'vewLYvBFns',
'tag_id': 'fld2xdCqrs',
'amount_id': 'fldC9cnnAY',
'checked_id': 'fldJkHhQgQ'
}
application.config['YOUXIN_ID'] = {
'table_id': 'tbl9NPZQMl5IVHQ2',
'view_id': 'vewrOuK32q',
'tag_id': 'fldrxqL8uz',
'amount_id': 'fldsOIpOBk',
'checked_id': 'fldsuS6SuR'
}

View File

@ -0,0 +1,39 @@
body {
background-color: azure;
}
main {
text-align: center;
vertical-align: middle;
font-size: 18px;
}
input {
width: 4ch;
text-align: center;
}
hr {
width: 350px;
height: 3px;
}
.grail{
display: flex;
min-height: 100vh;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-evenly;
align-items: center;
}
.handler {
width: 350px;
display: flex;
flex-wrap: wrap;
align-content: space-between;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
button, input {
font-size: 18px;
}
form {
width:350px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@ -0,0 +1,51 @@
function increment() {
var amount = document.getElementById("inventory-amount");
amount.value = parseInt(amount.value) + 1;
}
function decrement() {
var amount = document.getElementById("inventory-amount");
if (amount.value > 0) {
amount.value = parseInt(amount.value) - 1;
}
}
function checknext() {
var amount = document.getElementById("inventory-amount");
// 获取ID为"forid"的元素
var idelement = document.getElementById("inventory-id");
// 获取该元素的文本内容,并解析为整数
var id = parseInt(idelement.innerText, 10); // 第二个参数10表示解析的基数这里是十进制
var tagelement = document.getElementById("inventory-tag");
var tag = tagelement.innerText;
var msg = document.getElementById("flash-msg");
var location = document.getElementById("inventory-location");
// var params = new URLSearchParams({
// checked: 1,
// id: id,
// amount: amount.value
// });
// var url = `/check?${params.toString()}`;
fetch('/get_inventory', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({id: id, amount: amount.value})
})
.then(response => response.json())
.then(data => {
// console.log('Success:', data);
amount.value = data.amount;
tagelement.innerText = data.tag;
idelement.innerText = data.id;
msg.innerText = data.checkedone + " 盘点数 " + data.checkedamount;
location.innerText = data.location;
})
.catch(error => console.error('Error:', error));
}
function backtoroot(msg="已完成,返回") {
alert(msg);
location.href="/";
}

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>盘库助手</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
</head>
<body>
<script type="text/javascript" src="{{ url_for('static', filename='js/script.js') }}"></script>
<script>
backtoroot( "{{ msg }}" );
</script>
</body>
</html>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>盘库助手</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" type="text/css">
</head>
<body>
<main class="grail">
<header>
<h1>盘点 </h1>
</header>
{% block content %}{% endblock %}
<footer>
{% block footer %}
<small> 2025 &copy; SDHLMH
</small>
{% endblock %}
</footer>
</main>
<script type="text/javascript" src="{{ url_for('static', filename='js/script.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block content %}
<div id="flash-msg">
开始盘点>>>
</div>
<div>
<div class="handler">
<a href="/"><button type="button">返回</button></a>
<div id="inventory-location"></div><div><p id="inventory-id" hidden>{{ inventory.id }}</p></div>
</div>
<h1 id="inventory-tag">{{ inventory.tag }}</h1>
<hr>
<div class="handler">
<button onclick="decrement()" type="button">减一</button>
<input type="number" id="inventory-amount" value="{{ inventory.amount }}" min="0">
<button onclick="increment()" type="button">加一</button>
</div>
<hr>
<div class="handler">
<div></div>
<div></div>
<button onclick="checknext()" type="button">确认,下一个</button>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}404 Error{% endblock %}
{% block content %}
<p class="text-center">Page Not Found</p>
{% endblock %}
{% block footer %}
<a href="{{ url_for('index') }}">&larr; Go Back</a>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}500 Error{% endblock %}
{% block content %}
<p class="text-center">Internal Server Error</p>
{% endblock %}
{% block footer %}
<a href="{{ url_for('index') }}">&larr; Go Back</a>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% block content %}
<form method=get action="/init">
<input style="width:150px;" type=submit value=初始化>
</form>
<form method=get action="/check">
<input style="width:150px;" type=submit value=继续盘点>
</form>
<!-- <form method=get action="/download-xlsx">
<input style="width:150px;" type=submit value=下载XLSX>
</form> -->
<div>
</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% block title %}上传Excel文件{% endblock %}
{% block content %}
<h1>初始化</h1>
<div class="handler">
<a href="/"><button type="button">返回</button></a>
<div></div><div></div>
</div>
<hr>
<form method=post enctype=multipart/form-data action="/import">
<div>初始化是从多维表格获取数据修改多维表格后务必运行花费约1-5分钟请耐心等待</div>
<div class="handler"><div></div><input style="width:100px;"type=submit value=初始化></div>
</form>
<div>
</div>
{% endblock %}

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
"""
:author: luffmims
:url: http://yum2.cc
:copyright: © 2025 luffmims <luffmims@hotmaill.com>
:license: MIT, see LICENSE for more details.
"""
from flask import jsonify, request, render_template
from inventory_check_lark import application, db
from inventory_check_lark.models import init_db, get_access_token, get_table_record, update_record, Inventory
from werkzeug.exceptions import BadRequest
@application.route('/')
def index():
return render_template('index.html')
@application.route('/init')
def init():
return 'ok' if init_db() else 'error'
@application.route('/check', methods=['GET'])
def check():
inventory = Inventory.query.filter_by(checked=False).first()
if not inventory:
return render_template('after.html',msg="未初始化或已盘完")
return render_template('check.html',inventory=inventory)
@application.route('/get_youxin_inventory', methods=['POST'])
def update_youxin_inventory():
try:
# 获取并验证表单数据
data = request.get_json()
inventory_id = data['id']
amount = data['amount']
if not inventory_id or not amount:
raise BadRequest('Missing inventory id or amount')
# 查询并更新库存对象
inventory = Inventory.query.get(inventory_id)
if not inventory:
raise BadRequest('Inventory not found')
checkedone = inventory.tag
inventory.amount = amount
inventory.checked = True
update_record(inventory.tableid, inventory.recordid, amount)
# 提交更改
db.session.commit()
# 显示成功消息
# flash(f'{inventory.tag} checked and updated')
id = inventory_id + 1
inventory = Inventory.query.get(id)
if not inventory:
raise BadRequest('Inventory not found')
# 返回更新后的库存信息
return jsonify({
'id': inventory.id,
'tag': inventory.tag,
'amount': inventory.amount,
'checkedone': checkedone,
'checkedamount': amount
})
except BadRequest as e:
# 处理错误情况
# flash(str(e))
return jsonify({'error': str(e)}), 400
except Exception as e:
# 处理其他可能的错误
db.session.rollback()
# flash('An error occurred while updating the inventory')
return jsonify({'error': 'Internal server error'}), 500

1
wsgi.py Normal file
View File

@ -0,0 +1 @@
from inventory_check_lark import application