Compare commits

...

27 Commits

Author SHA1 Message Date
39b0cb2418 Revert "fix: update order cancellation response check to use 'result' key"
This reverts commit 241785ab91.
2025-09-01 07:08:13 +03:00
241785ab91 fix: update order cancellation response check to use 'result' key 2025-09-01 07:07:48 +03:00
ba7000f3d9 feat: implement order cancellation for assemblies and handle error responses 2025-09-01 07:07:28 +03:00
489d470c7b feat: add response for users with existing active assemblies 2025-08-15 04:22:03 +03:00
485e6eacd2 feat: add application name filter to version query 2025-06-09 12:06:43 +03:00
4ebb7ce0fc feat: implement close_assembly_by_id function and confirm_current_assembly endpoint 2025-05-29 15:38:40 +03:00
9c904ae138 feat: add support for multiple label formats in get_label function 2025-05-26 22:48:51 +03:00
6ca3a9372a feat: add support for multiple label formats in get_label function 2025-05-26 22:43:52 +03:00
39087fc223 refactor: remove confirmed state filter from assembly queries 2025-05-24 00:05:23 +03:00
47ac3908f6 feat: ozon supplies 2025-04-02 03:44:34 +03:00
d6dc39ef61 crpt 2025-01-13 05:10:55 +03:00
4d7e5ded4d crpt 2024-10-12 03:55:00 +03:00
7512b3347e feat: assembly additional statistics 2024-03-05 07:16:24 +03:00
7e853c48e7 feat: balance and reward 2024-02-24 15:38:48 +03:00
c8a234e27b feat: get users sipro 2024-02-22 07:51:17 +03:00
b302feec1d Merge remote-tracking branch 'origin/master' 2023-12-20 01:06:26 +03:00
6dea56e3ee feat: cancel assembly by id 2023-12-20 01:04:20 +03:00
dc8d9d242b assembly statistics 2023-12-11 01:53:30 +04:00
34be5a198d assembly statistics 2023-12-11 01:50:49 +04:00
c508a3ee33 feat: user create and delete 2023-11-26 06:12:26 +03:00
f0b1c030f3 feat: admin panel 2023-11-25 09:01:55 +03:00
43cd50d5cd feat: admin panel 2023-11-25 08:56:44 +03:00
347eb9730c feat: admin panel 2023-11-25 08:46:02 +03:00
40abf70430 Merge remote-tracking branch 'origin/master' 2023-11-25 08:38:12 +03:00
430cfad277 feat: admin panel 2023-11-25 08:38:04 +03:00
f1edc1b0b4 Merge remote-tracking branch 'origin/master' 2023-11-25 03:52:37 +04:00
8e0cddab4c patch sipro 2023-11-25 03:52:31 +04:00
35 changed files with 625 additions and 86 deletions

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ __pycache__/
.env .env
test.* test.*
test/ test/
apks/ apks/
*.log

4
app.py
View File

@@ -20,7 +20,7 @@ migrate = Migrate(app, database.db)
server_session = Session(app) server_session = Session(app)
# CORS config # CORS config
CORS(app, supports_credentials=True) CORS(app, expose_headers=["Content-Range", 'Authorization'], supports_credentials=True)
jwt = JWTManager(app) jwt = JWTManager(app)
blueprints = [ blueprints = [
(routes.auth_blueprint, '/auth'), (routes.auth_blueprint, '/auth'),
@@ -31,6 +31,8 @@ blueprints = [
(routes.general_blueprint, '/general'), (routes.general_blueprint, '/general'),
(routes.application_blueprint, '/application'), (routes.application_blueprint, '/application'),
(routes.sipro_blueprint, '/sipro'), (routes.sipro_blueprint, '/sipro'),
(routes.admin_blueprint, '/admin'),
(routes.balance_blueprint, '/balance'),
] ]
for blueprint, url_prefix in blueprints: for blueprint, url_prefix in blueprints:

View File

@@ -9,3 +9,9 @@ def to_nested_dict(row):
current_level = current_level[part] current_level = current_level[part]
current_level[keys[-1]] = value current_level[keys[-1]] = value
return result return result
def compile_query_to_plain_sql(query) -> str:
return query.statement.compile(compile_kwargs={
'literal_binds': True
})

View File

@@ -1 +1,4 @@
from database.models import * from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
from .models import *

View File

@@ -8,3 +8,9 @@ class AssemblyState(IntEnum):
ALL_PRODUCTS_ASSEMBLED = 2, ALL_PRODUCTS_ASSEMBLED = 2,
CONFIRMED = 3, CONFIRMED = 3,
ENDED = 4 ENDED = 4
@unique
class BalanceTransactionType(IntEnum):
TOP_UP = 0
WITHDRAW = 1

View File

@@ -0,0 +1,2 @@
from .basic import *
from .balance import *

View File

@@ -0,0 +1,40 @@
from sqlalchemy import Connection, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapper
import database
from database import db
class BalanceTransaction(db.Model):
__tablename__ = 'balance_transactions'
id = db.Column(db.Integer, primary_key=True, comment='ID транзакции')
type = db.Column(db.Integer, nullable=False, comment='Тип транзакции')
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
user = db.relationship('User', backref='transactions')
amount = db.Column(db.Double, nullable=False, comment='Сумма транзакции')
description = db.Column(db.Text, nullable=False, comment='Описание')
json_data = db.Column(JSONB, comment='JSON данные')
created_at = db.Column(db.DateTime, nullable=False, comment='Дата и время создания транзакции')
def recalculate_user_balance(user_id: int):
user_balance = (database.BalanceTransaction.query.filter_by(user_id=user_id)
.group_by(database.BalanceTransaction.user_id)
.with_entities(func.sum(database.BalanceTransaction.amount))
.scalar()) or 0
(db.session.query(database.User)
.filter_by(id=user_id)
.update({'balance': user_balance}, synchronize_session='fetch'))
@db.event.listens_for(BalanceTransaction, 'after_insert')
def after_balance_transaction_insert(mapper: Mapper, connection: Connection, target: BalanceTransaction):
recalculate_user_balance(target.user_id)
@db.event.listens_for(BalanceTransaction, 'after_delete')
def after_balance_transaction_delete(mapper: Mapper, connection: Connection, target: BalanceTransaction):
recalculate_user_balance(target.user_id)

View File

@@ -1,6 +1,4 @@
from flask_sqlalchemy import SQLAlchemy from database import db
db = SQLAlchemy()
class User(db.Model): class User(db.Model):
@@ -11,6 +9,10 @@ class User(db.Model):
password_hash = db.Column(db.String, nullable=False, comment='Пароль') password_hash = db.Column(db.String, nullable=False, comment='Пароль')
sipro_id = db.Column(db.Integer, nullable=True, comment='ID пользователя в SIPRO') sipro_id = db.Column(db.Integer, nullable=True, comment='ID пользователя в SIPRO')
is_admin = db.Column(db.Boolean, nullable=False, default=False, server_default='0', comment='Админ ли юзверь')
city_id = db.Column(db.Integer, nullable=False, default='1', server_default='1', comment='ID страны')
balance = db.Column(db.Double, nullable=False, default=0, server_default='0', comment='Баланс пользователя')
class Assembly(db.Model): class Assembly(db.Model):

View File

View File

@@ -0,0 +1,4 @@
from .requests import (
get_balance_transactions,
get_balance_info
)

View File

@@ -0,0 +1,29 @@
import database
from ..schemas.requests import *
from ..schemas.responses import *
from ..schemas.entities import *
def get_balance_transactions(data: dict):
items_per_page = 5
request = GetBalanceTransactionsRequest.model_validate(data)
page = max([request.page - 1, 0])
query = (database.BalanceTransaction.query
.filter(database.BalanceTransaction.user_id == request.user_id)
.order_by(database.BalanceTransaction.created_at.desc())
.offset(items_per_page * page)
.limit(items_per_page)
.all())
balance_transactions = [BalanceTransaction.model_validate(balance_transaction) for balance_transaction in query]
response = GetBalanceTransactionsResponse(balance_transactions=balance_transactions)
return response.to_dict()
def get_balance_info(data: dict):
request = GetBalanceInfoRequest.model_validate(data)
balance = (database.User.query
.filter_by(id=request.user_id)
.with_entities(database.User.balance)
.scalar())
response = GetBalanceInfoResponse(balance=balance)
return response.to_dict()

View File

View File

@@ -0,0 +1,13 @@
from datetime import datetime
from schemas.base import CommonModel
class BalanceTransaction(CommonModel):
id: int
type: int
user_id: int
amount: float
description: str
json_data: dict | None = None
created_at: datetime

View File

@@ -0,0 +1,10 @@
from schemas.base import CommonModel
class GetBalanceTransactionsRequest(CommonModel):
page: int
user_id: int
class GetBalanceInfoRequest(CommonModel):
user_id: int

View File

@@ -0,0 +1,12 @@
from typing import List
from schemas.base import CommonModel
from .entities import BalanceTransaction
class GetBalanceTransactionsResponse(CommonModel):
balance_transactions: List[BalanceTransaction]
class GetBalanceInfoResponse(CommonModel):
balance: float

View File

@@ -14,4 +14,5 @@ mariadb
# Other stuff # Other stuff
requests requests
python-dotenv python-dotenv
pydantic

View File

@@ -1,8 +1,10 @@
from routes.auth import auth_blueprint from .auth import auth_blueprint
from routes.orders import orders_blueprint from .orders import orders_blueprint
from routes.barcode import barcode_blueprint from .barcode import barcode_blueprint
from routes.printing import printing_blueprint from .printing import printing_blueprint
from routes.assembly import assembly_blueprint from .assembly import assembly_blueprint
from routes.general import general_blueprint from .general import general_blueprint
from routes.application import application_blueprint from .application import application_blueprint
from routes.sipro import sipro_blueprint from .sipro import sipro_blueprint
from .admin import admin_blueprint
from .balance import balance_blueprint

114
routes/admin.py Normal file
View File

@@ -0,0 +1,114 @@
import json
from flask import Blueprint, request, make_response, jsonify
from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
from werkzeug.security import generate_password_hash
import database
import sipro.api.general
admin_blueprint = Blueprint('admin', __name__)
@admin_blueprint.before_request
def admin_check():
if request.method == 'OPTIONS':
return
if not verify_jwt_in_request(optional=True):
return {'error': 'Unauthorized'}, 401
user_id = get_jwt_identity()
is_admin = database.db.session.get(database.User, user_id).is_admin
if not is_admin:
return {'error': 'Unauthorized'}, 401
def format_user(user: database.User):
return {
'id': user.id,
'login': user.login,
'city_id': user.city_id,
'is_admin': user.is_admin
}
@admin_blueprint.get('/ping')
def ping():
return {"response": "pong"}
@admin_blueprint.get('/user')
def get_users():
response = make_response(jsonify(
[format_user(user) for user in database.User.query.all()]
))
response.headers['Content-Range'] = 'user 0-1/1'
return response
@admin_blueprint.get('/user/<int:user_id>')
def get_user(user_id):
user = database.db.session.get(database.User, user_id)
return format_user(user)
@admin_blueprint.put('/user/<int:user_id>')
def put_user(user_id):
params: dict = request.json
password = params.get('password')
if password:
password = password.strip()
if password:
params['password_hash'] = generate_password_hash(password)
if 'password' in params:
del params['password']
database.db.session.bulk_update_mappings(database.User, [params])
database.db.session.commit()
user = database.db.session.get(database.User, user_id)
return format_user(user)
@admin_blueprint.post('/user')
def create_user():
args: dict = request.json
args['password_hash'] = generate_password_hash(args['password'])
del args['password']
new_user = database.User(**args)
database.db.session.add(new_user)
database.db.session.flush()
database.db.session.commit()
return format_user(new_user)
@admin_blueprint.delete('/user/<int:user_id>')
def delete_user(user_id):
user = database.db.session.get(database.User, user_id)
if user:
database.db.session.delete(user)
database.db.session.commit()
return '', 200
@admin_blueprint.get('/city')
def get_cities():
filters = request.args.get('filter')
if filters:
filters = json.loads(filters)
else:
filters = {}
cities = sipro.api.general.get_cities()
for key, value in filters.items():
match key:
case 'id':
cities = list(filter(lambda city: city['id'] in value, cities))
total_cities = len(cities)
range_start, range_end = 0, total_cities - 1
range_raw = request.args.get('range')
if range_raw:
range_start, range_end = json.loads(range_raw)
cities = cities[range_start:range_end + 1]
response = make_response(jsonify(cities))
response.headers['Content-Range'] = f'city {range_start}-{range_end}/{total_cities}'
return response

View File

@@ -55,7 +55,8 @@ def upload(application_name: str):
return {"error": "Invalid form data. There is no file or version field"}, 400 return {"error": "Invalid form data. There is no file or version field"}, 400
version = version.strip() version = version.strip()
application = (database.Application.query. application = (database.Application.query.
filter_by(version=version). filter_by(version=version,
name=application_name).
with_entities(database.Application.id).first()) with_entities(database.Application.id).first())
if application: if application:
return {"error": f"Specified version ({version}) already uploaded"}, 400 return {"error": f"Specified version ({version}) already uploaded"}, 400

View File

@@ -2,11 +2,13 @@ import datetime
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from flask_jwt_extended import get_jwt_identity from flask_jwt_extended import get_jwt_identity
from sqlalchemy.orm import joinedload
import database import database
from database.enums import AssemblyState from database.enums import AssemblyState
from routes.utils import jwt_protect_blueprint from routes.utils import jwt_protect_blueprint
import sipro.api.orders import sipro.api.orders
import utils.balance
assembly_blueprint = jwt_protect_blueprint(Blueprint('assembly', __name__)) assembly_blueprint = jwt_protect_blueprint(Blueprint('assembly', __name__))
@@ -17,15 +19,33 @@ def create_assembly():
data: dict = request.json data: dict = request.json
order_id: int = data.get('orderId') order_id: int = data.get('orderId')
user_id = get_jwt_identity() user_id = get_jwt_identity()
existing_assembly = database.Assembly.query.filter_by(order_id=order_id).first() existing_assembly = (database.Assembly.query.
options(joinedload(database.Assembly.user)).
filter_by(order_id=order_id).
first())
if existing_assembly: if existing_assembly:
response = { response = {
'ok': False, 'ok': False,
'message': 'Сборка этого товара уже была запущена', 'message': 'Сборка этого товара уже была запущена',
'assemblyId': existing_assembly.id, 'assemblyId': existing_assembly.id,
'statusCode': 'ASSEMBLY_ALREADY_EXISTS' 'statusCode': 'ASSEMBLY_ALREADY_EXISTS',
'userName': existing_assembly.user.login
} }
return jsonify(response) return jsonify(response)
need_crpt_for_order_response = sipro.api.orders.need_crpt_by_order_id(order_id)
need_crpt_value = need_crpt_for_order_response.get('needCrpt')
if need_crpt_value:
valid_app = request.headers.get('CrptAvailable')
if not valid_app:
response = {
'ok': False,
'message': 'Для сборки этого заказа необходимо приложение с поддержкой Честного Знака',
'assemblyId': -1,
'statusCode': 'USER_ALREADY_HAS_ACTIVE_ASSEMBLY',
}
return jsonify(response)
active_assembly = database.Assembly.query.filter(database.Assembly.user_id == user_id, active_assembly = database.Assembly.query.filter(database.Assembly.user_id == user_id,
database.Assembly.is_active == True).first() database.Assembly.is_active == True).first()
if active_assembly: if active_assembly:
@@ -36,6 +56,22 @@ def create_assembly():
'statusCode': 'USER_ALREADY_HAS_ACTIVE_ASSEMBLY' 'statusCode': 'USER_ALREADY_HAS_ACTIVE_ASSEMBLY'
} }
return jsonify(response) return jsonify(response)
assembled_assembly = (
database.Assembly.query
.filter(
database.Assembly.order_id == order_id,
database.Assembly.ended_at != None
)
.first()
)
if assembled_assembly:
response = {
'ok': False,
'message': 'Вы уже собирали этот товар',
'assemblyId': assembled_assembly.id,
'statusCode': 'USER_ALREADY_HAS_ACTIVE_ASSEMBLY',
}
return jsonify(response)
assembly = database.Assembly(user_id=user_id, assembly = database.Assembly(user_id=user_id,
order_id=order_id, order_id=order_id,
state=AssemblyState.NOT_STARTED, state=AssemblyState.NOT_STARTED,
@@ -60,10 +96,9 @@ def create_assembly():
return jsonify(response) return jsonify(response)
@assembly_blueprint.post('/close') def close_assembly_by_id(
def close_assembly(): assembly_id: int,
args = request.json ):
assembly_id = args.get('assemblyId')
if not assembly_id or (not isinstance(assembly_id, int)): if not assembly_id or (not isinstance(assembly_id, int)):
response = { response = {
'ok': False, 'ok': False,
@@ -81,7 +116,25 @@ def close_assembly():
assembly.ended_at = datetime.datetime.now() assembly.ended_at = datetime.datetime.now()
database.db.session.commit() database.db.session.commit()
order_id = assembly.order_id order_id = assembly.order_id
return sipro.api.orders.close_order(order_id) sipro_response = sipro.api.orders.close_order(order_id)
reward = sipro_response.get('reward')
ok = sipro_response.get('ok')
if ok:
utils.balance.add_top_up(user_id=assembly.user_id,
amount=reward,
description=f'Начисление за сборку заказа {order_id}',
json_data={'order_id': order_id},
commit=True)
return sipro_response
@assembly_blueprint.post('/close')
def close_assembly():
args = request.json
assembly_id = args.get('assemblyId')
sipro_response = close_assembly_by_id(int(assembly_id))
return sipro_response
@assembly_blueprint.post('/cancel') @assembly_blueprint.post('/cancel')
@@ -96,6 +149,14 @@ def cancel_assembly():
'message': 'У вас нет активных сборок' 'message': 'У вас нет активных сборок'
} }
return jsonify(response) return jsonify(response)
order_id = assembly.order_id
sipro_response = sipro.api.orders.cancel_order_assembly(order_id)
if not sipro_response.get('ok'):
response = {
'ok': False,
'message': f'Ошибка: {sipro_response.get("message")}'
}
return jsonify(response)
database.db.session.delete(assembly) database.db.session.delete(assembly)
database.db.session.commit() database.db.session.commit()
response = { response = {
@@ -111,6 +172,32 @@ def cancel_assembly():
return jsonify(response) return jsonify(response)
@assembly_blueprint.post('/cancelById')
def cancel_assembly_by_id():
try:
assembly_id = request.json.get('assemblyId')
assembly = database.db.session.get(database.Assembly, assembly_id)
if not assembly:
response = {
'ok': False,
'message': f'Сборка {assembly_id} не найдена'
}
return jsonify(response)
database.db.session.delete(assembly)
database.db.session.commit()
response = {
'ok': True,
'message': f'Сборка {assembly_id} успешно отменена!'
}
return jsonify(response)
except Exception as e:
response = {
'ok': False,
'message': f'Неудалось отменить сборку, ошибка: {e}'
}
return jsonify(response)
@assembly_blueprint.get('/hasActive') @assembly_blueprint.get('/hasActive')
def user_has_active_assembly(): def user_has_active_assembly():
user_id = get_jwt_identity() user_id = get_jwt_identity()
@@ -135,6 +222,26 @@ def get_active_assembly():
return jsonify(response) return jsonify(response)
@assembly_blueprint.post('/confirmCurrent')
def confirm_current_assembly():
# confirm current assembly for user
user_id = get_jwt_identity()
assembly = database.Assembly.query.filter(database.Assembly.user_id == user_id,
database.Assembly.is_active == True).first()
if not assembly:
response = {
'ok': False,
'message': 'У вас нет активных сборок'
}
return jsonify(response)
close_assembly_by_id(assembly.id)
response = {
'ok': True,
'message': 'Сборка успешно завершена!',
}
return response
@assembly_blueprint.post('/confirm') @assembly_blueprint.post('/confirm')
def confirm_assembly(): def confirm_assembly():
user_id = get_jwt_identity() user_id = get_jwt_identity()
@@ -179,3 +286,16 @@ def update_assembly_state():
except Exception as e: except Exception as e:
print('Error while updating') print('Error while updating')
return jsonify(ok=False) return jsonify(ok=False)
@assembly_blueprint.get('/needCrpt')
def need_crpt():
order_product_id = request.args.get('orderProductId')
return sipro.api.orders.need_crpt(order_product_id)
@assembly_blueprint.post('/attachCrpt')
def attach_crpt():
order_product_id = request.json.get('orderProductId')
crpt = request.json.get('crpt')
return sipro.api.orders.attach_crpt(order_product_id, crpt)

23
routes/balance.py Normal file
View File

@@ -0,0 +1,23 @@
from flask import Blueprint, request
from flask_jwt_extended import get_jwt_identity
from routes.utils import jwt_protect_blueprint
import queries.balance.api as api
balance_blueprint = jwt_protect_blueprint(Blueprint('balance', __name__))
@balance_blueprint.get('/transactions')
def get_transactions():
data = dict(request.args)
if 'user_id' not in data:
data['user_id'] = get_jwt_identity()
response = api.get_balance_transactions(dict(data))
return response
@balance_blueprint.get('/info')
def get_balance_info():
data = {'user_id': get_jwt_identity()}
response = api.get_balance_info(data)
return response

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, jsonify, request from flask import Blueprint
from routes.utils import jwt_protect_blueprint from routes.utils import jwt_protect_blueprint
import sipro.api.general import sipro.api.general

View File

@@ -1,4 +1,7 @@
import database
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from flask_jwt_extended import get_jwt_identity
from routes.utils import jwt_protect_blueprint from routes.utils import jwt_protect_blueprint
import sipro.api.orders import sipro.api.orders
@@ -30,8 +33,10 @@ def get_orders():
@orders_blueprint.get('/getByProductId') @orders_blueprint.get('/getByProductId')
def get_orders_by_supplier_product_id(): def get_orders_by_supplier_product_id():
user_id = get_jwt_identity()
city_id = database.db.session.get(database.User, user_id).city_id
params = dict(request.args) params = dict(request.args)
# product_id = args.get('productId') params['city'] = city_id
return sipro.api.orders.get_orders_from_barcode(params=params) return sipro.api.orders.get_orders_from_barcode(params=params)

View File

@@ -1,4 +1,6 @@
from flask import Blueprint, request, send_file import json
from flask import Blueprint, request, send_file, jsonify
from routes.utils import jwt_protect_blueprint from routes.utils import jwt_protect_blueprint
import sipro.api.printing import sipro.api.printing
@@ -9,9 +11,14 @@ printing_blueprint = jwt_protect_blueprint(Blueprint('printing', __name__))
def get_label(): def get_label():
args = request.args args = request.args
order_id = args.get('orderId') order_id = args.get('orderId')
data = sipro.api.printing.get_label(order_id) label_format = args.get('format', 'pdf')
data = sipro.api.printing.get_label(order_id, label_format=label_format)
data.seek(0) data.seek(0)
return send_file(data,
as_attachment=True, if label_format == 'pdf':
download_name='label.pdf', return send_file(data,
mimetype='application/pdf') as_attachment=True,
download_name='label.pdf',
mimetype='application/pdf')
data= json.loads(str(data.read(), 'ascii'))
return jsonify(data)

View File

@@ -1,10 +1,11 @@
import datetime
from enum import StrEnum from enum import StrEnum
from flask import Blueprint, request, jsonify
from flask import Blueprint, request from sqlalchemy import func, cast, String, Numeric, Float
from sqlalchemy import func
import auxiliary import auxiliary
import database import database
import database.enums
sipro_blueprint = Blueprint('sipro', __name__) sipro_blueprint = Blueprint('sipro', __name__)
@@ -26,7 +27,7 @@ def assembly_info():
expand_param = params.get('expand') expand_param = params.get('expand')
order_ids = params.get('orderIds') order_ids = params.get('orderIds')
expand_list = expand_param if expand_param else [] expand_list = expand_param if expand_param else []
datetime_format = 'YYYY-MM-DD, HH24:MI' datetime_format = 'YYYY.MM.DD, HH24:MI'
entity_list = [ entity_list = [
database.Assembly.id.label('id'), database.Assembly.id.label('id'),
database.Assembly.order_id.label('order_id'), database.Assembly.order_id.label('order_id'),
@@ -51,3 +52,118 @@ def assembly_info():
result = query.all() result = query.all()
json_result = [auxiliary.to_nested_dict(row) for row in result] json_result = [auxiliary.to_nested_dict(row) for row in result]
return json_result return json_result
@sipro_blueprint.post('/tableAssemblyStats')
def table_assembly_stats():
filters = request.json
date_from = filters.get('from')
date_to = filters.get('to')
dated_query = (
database.Assembly.query
.join(database.User)
.filter(
database.User.is_admin == False
)
.group_by(database.User.login, database.Assembly.user_id)
.with_entities(
database.User.login.label('user_login'),
func.count(database.Assembly.user_id).label('assembled'),
func.string_agg(func.cast(database.Assembly.order_id, database.db.String), ',').label('order_ids')
)
)
if len(date_from) > 0 and len(date_to) > 0:
dated_query = dated_query.filter(
database.Assembly.ended_at >= date_from,
database.Assembly.ended_at <= date_to
)
json_result = []
for row in dated_query.all():
json_result.append({
'user_login': row.user_login,
'assembled': row.assembled,
'order_ids': row.order_ids
})
return jsonify(json_result)
@sipro_blueprint.get('/dailyAssemblyData')
def daily_assembly_stats():
query = (
database.Assembly.query
.join(database.User)
.filter(
database.Assembly.ended_at >= func.current_date()
)
.group_by(database.User.login, database.Assembly.user_id)
.with_entities(
database.User.login.label('user_login'),
func.count(database.Assembly.user_id).label('assembled'),
func.string_agg(func.cast(database.Assembly.order_id, database.db.String), ',').label('order_ids')
)
)
json_result = []
for row in query.all():
json_result.append({
'user_login': row.user_login,
'assembled': row.assembled,
'order_ids': row.order_ids
})
return json_result
@sipro_blueprint.get('/users')
def get_users():
query = (database.User.query.
with_entities(database.User.id,
database.User.login)
.all())
response = [{'id': user.id, 'login': user.login} for user in query]
return jsonify(response)
@sipro_blueprint.get('/statistics')
def get_users_statistics():
data: dict = request.args
user_id = int(data['userId'])
date_from = datetime.datetime.fromisoformat(data['dateFrom'])
date_to = datetime.datetime.fromisoformat(data['dateTo']) + datetime.timedelta(hours=24, minutes=59, seconds=59)
query = (database.BalanceTransaction.query
.filter(database.BalanceTransaction.user_id == user_id,
database.BalanceTransaction.created_at.between(date_from, date_to))
.order_by(func.date_trunc('day', database.BalanceTransaction.created_at))
.group_by(func.date_trunc('day', database.BalanceTransaction.created_at))
.with_entities(func.date_trunc('day', database.BalanceTransaction.created_at).label('date'),
func.cast(func.round(func.cast(func.sum(database.BalanceTransaction.amount), Numeric), 2),
Float).label(
'value'),
func.string_agg(cast(database.BalanceTransaction.json_data['order_id'], String), ',').label(
'order_ids'))
.all())
result = [{'date': row.date.isoformat(), 'value': row.value, 'order_ids': row.order_ids} for row in query]
return jsonify(result)
@sipro_blueprint.get('/get-orders-by-date')
def get_orders_by_date():
data: dict = request.args
date = datetime.date.fromisoformat(data['date'])
query = (
database.Assembly.query
.filter(
func.date_trunc('day', database.Assembly.ended_at) == func.date_trunc('day', date),
)
.with_entities(
func.string_agg(func.cast(database.Assembly.order_id, database.db.String), ',').label('order_ids')
)
.scalar()
)
if not query:
return jsonify({'order_ids': []})
return jsonify({'order_ids': list(map(int, query.split(',')))})

View File

@@ -1,13 +0,0 @@
from enum import unique, IntEnum
@unique
class CodeType(IntEnum):
BARCODE = 0
QRCODE = 1
INVALID = 2
@unique
class SearchType(IntEnum):
PRODUCT = 0

View File

@@ -1,11 +0,0 @@
from scanner.enums import CodeType
from scanner.utils import guess_code_type
class ScannerSearch:
def __init__(self, string_value: str):
self.string_value = string_value
self._code_type = guess_code_type(self.string_value)
def get_code_type(self) -> CodeType:
return self._code_type

View File

@@ -1,9 +0,0 @@
from scanner.enums import CodeType
def guess_code_type(string_value: str) -> CodeType:
if string_value.isdigit():
return CodeType.BARCODE
if string_value:
return CodeType.QRCODE
return CodeType.INVALID

0
schemas/__init__.py Normal file
View File

20
schemas/base.py Normal file
View File

@@ -0,0 +1,20 @@
from pydantic import BaseModel
from pydantic.alias_generators import to_camel
class CommonConfig:
alias_generator = to_camel
populate_by_name = True
from_attributes = True
class CommonModel(BaseModel):
class Config(CommonConfig):
pass
@classmethod
def json_serialize(cls, obj):
return cls.model_validate(obj).model_dump(by_alias=True)
def to_dict(self, by_alias=True):
return self.model_dump(by_alias=by_alias)

View File

@@ -40,3 +40,29 @@ def get_orders_from_barcode(params: dict):
method = f'{router}/getByProductId' method = f'{router}/getByProductId'
response = client.method('GET', method, params=params) response = client.method('GET', method, params=params)
return response return response
def need_crpt(order_product_id):
method = f'{router}/needCrpt?orderProductId={order_product_id}'
response = client.method('GET', method)
return response
def attach_crpt(order_product_id, crpt):
method = f'{router}/attachCrpt'
data = {'orderProductId': order_product_id, 'crpt': crpt}
response = client.method('POST', method, data=data)
return response
def need_crpt_by_order_id(order_id):
method = f'{router}/needCrptByOrder?orderId={order_id}'
response = client.method('GET', method)
return response
def cancel_order_assembly(order_id):
method = f'{router}/cancelOrderAssembly'
data = {'orderId': order_id}
response = client.method('POST', method, data=data)
return response

View File

@@ -9,8 +9,8 @@ client = get_client()
router = '/printing' router = '/printing'
def get_label(order_product_id: str) -> BytesIO: def get_label(order_product_id: str, label_format:str='pdf') -> BytesIO:
method = f'{router}/getLabel?orderId={order_product_id}' method = f'{router}/getLabel?orderId={order_product_id}&format={label_format}'
response: Response = client.method('GET', method, raw=True) response: Response = client.method('GET', method, raw=True)
data = BytesIO(response.content) data = BytesIO(response.content)
return data return data

23
utils/balance.py Normal file
View File

@@ -0,0 +1,23 @@
import datetime
import database
import database.enums
def add_top_up(user_id: int,
amount: float,
description: str,
json_data: dict,
commit=False):
transaction = database.BalanceTransaction(
user_id=user_id,
type=database.enums.BalanceTransactionType.TOP_UP,
amount=amount,
description=description,
json_data=json_data,
created_at=datetime.datetime.now()
)
database.db.session.add(transaction)
if commit:
database.db.session.commit()

View File

@@ -1,16 +0,0 @@
import database
from database.mariadb import MariadbConnector
class BarcodesSynchronizer:
def __init__(self):
self.mariadb_connector = MariadbConnector()
def synchronize(self):
existing_barcodes = database.Barcode.query.all()
denco_articles = list(set([barcode.denco_article for barcode in existing_barcodes]))
query_string = 'SELECT product, barcode FROM modx_0_connections WHERE barcode != "" LIMIT 100'
for denco_article, barcodes_string in self.mariadb_connector.select(query_string):
barcodes = barcodes_string.split(',')
print(denco_article, barcodes)