Compare commits
27 Commits
75a71953f1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 39b0cb2418 | |||
| 241785ab91 | |||
| ba7000f3d9 | |||
| 489d470c7b | |||
| 485e6eacd2 | |||
| 4ebb7ce0fc | |||
| 9c904ae138 | |||
| 6ca3a9372a | |||
| 39087fc223 | |||
| 47ac3908f6 | |||
| d6dc39ef61 | |||
| 4d7e5ded4d | |||
| 7512b3347e | |||
| 7e853c48e7 | |||
| c8a234e27b | |||
| b302feec1d | |||
| 6dea56e3ee | |||
| dc8d9d242b | |||
| 34be5a198d | |||
| c508a3ee33 | |||
| f0b1c030f3 | |||
| 43cd50d5cd | |||
| 347eb9730c | |||
| 40abf70430 | |||
| 430cfad277 | |||
| f1edc1b0b4 | |||
| 8e0cddab4c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,5 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
test.*
|
test.*
|
||||||
test/
|
test/
|
||||||
apks/
|
apks/
|
||||||
|
*.log
|
||||||
4
app.py
4
app.py
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
from database.models import *
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
from .models import *
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
database/models/__init__.py
Normal file
2
database/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .basic import *
|
||||||
|
from .balance import *
|
||||||
40
database/models/balance.py
Normal file
40
database/models/balance.py
Normal 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)
|
||||||
@@ -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):
|
||||||
0
queries/balance/__init__.py
Normal file
0
queries/balance/__init__.py
Normal file
4
queries/balance/api/__init__.py
Normal file
4
queries/balance/api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .requests import (
|
||||||
|
get_balance_transactions,
|
||||||
|
get_balance_info
|
||||||
|
)
|
||||||
29
queries/balance/api/requests.py
Normal file
29
queries/balance/api/requests.py
Normal 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()
|
||||||
0
queries/balance/schemas/__init__.py
Normal file
0
queries/balance/schemas/__init__.py
Normal file
13
queries/balance/schemas/entities.py
Normal file
13
queries/balance/schemas/entities.py
Normal 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
|
||||||
10
queries/balance/schemas/requests.py
Normal file
10
queries/balance/schemas/requests.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from schemas.base import CommonModel
|
||||||
|
|
||||||
|
|
||||||
|
class GetBalanceTransactionsRequest(CommonModel):
|
||||||
|
page: int
|
||||||
|
user_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class GetBalanceInfoRequest(CommonModel):
|
||||||
|
user_id: int
|
||||||
12
queries/balance/schemas/responses.py
Normal file
12
queries/balance/schemas/responses.py
Normal 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
|
||||||
@@ -14,4 +14,5 @@ mariadb
|
|||||||
|
|
||||||
# Other stuff
|
# Other stuff
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
pydantic
|
||||||
@@ -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
114
routes/admin.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
23
routes/balance.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
124
routes/sipro.py
124
routes/sipro.py
@@ -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(',')))})
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
0
schemas/__init__.py
Normal file
20
schemas/base.py
Normal file
20
schemas/base.py
Normal 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)
|
||||||
@@ -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
|
||||||
@@ -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
23
utils/balance.py
Normal 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()
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user