Compare commits

...

15 Commits

31 changed files with 392 additions and 82 deletions

1
app.py
View File

@@ -32,6 +32,7 @@ blueprints = [
(routes.application_blueprint, '/application'), (routes.application_blueprint, '/application'),
(routes.sipro_blueprint, '/sipro'), (routes.sipro_blueprint, '/sipro'),
(routes.admin_blueprint, '/admin'), (routes.admin_blueprint, '/admin'),
(routes.balance_blueprint, '/balance'),
] ]
for blueprint, url_prefix in blueprints: for blueprint, url_prefix in blueprints:

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):
@@ -14,6 +12,7 @@ class User(db.Model):
is_admin = db.Column(db.Boolean, nullable=False, default=False, server_default='0', comment='Админ ли юзверь') 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 страны') 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

@@ -15,3 +15,4 @@ mariadb
# Other stuff # Other stuff
requests requests
python-dotenv python-dotenv
pydantic

View File

@@ -1,9 +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 routes.admin import admin_blueprint from .admin import admin_blueprint
from .balance import balance_blueprint

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

@@ -8,6 +8,7 @@ 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__))
@@ -31,6 +32,20 @@ def create_assembly():
'userName': existing_assembly.user.login '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:
@@ -41,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,
@@ -65,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,
@@ -86,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')
@@ -101,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 = {
@@ -142,7 +198,6 @@ def cancel_assembly_by_id():
return jsonify(response) 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()
@@ -167,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()
@@ -211,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,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)
if label_format == 'pdf':
return send_file(data, return send_file(data,
as_attachment=True, as_attachment=True,
download_name='label.pdf', download_name='label.pdf',
mimetype='application/pdf') mimetype='application/pdf')
data= json.loads(str(data.read(), 'ascii'))
return jsonify(data)

View File

@@ -1,6 +1,7 @@
import datetime
from enum import StrEnum from enum import StrEnum
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from sqlalchemy import func from sqlalchemy import func, cast, String, Numeric, Float
import auxiliary import auxiliary
import database import database
@@ -63,7 +64,6 @@ def table_assembly_stats():
database.Assembly.query database.Assembly.query
.join(database.User) .join(database.User)
.filter( .filter(
database.Assembly.state == database.enums.AssemblyState.CONFIRMED,
database.User.is_admin == False database.User.is_admin == False
) )
.group_by(database.User.login, database.Assembly.user_id) .group_by(database.User.login, database.Assembly.user_id)
@@ -97,7 +97,6 @@ def daily_assembly_stats():
database.Assembly.query database.Assembly.query
.join(database.User) .join(database.User)
.filter( .filter(
database.Assembly.state == database.enums.AssemblyState.CONFIRMED,
database.Assembly.ended_at >= func.current_date() database.Assembly.ended_at >= func.current_date()
) )
.group_by(database.User.login, database.Assembly.user_id) .group_by(database.User.login, database.Assembly.user_id)
@@ -117,3 +116,54 @@ def daily_assembly_stats():
}) })
return json_result 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)