Compare commits

...

40 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
75a71953f1 Merge remote-tracking branch 'origin/master' 2023-11-25 02:39:39 +03:00
448ba351a7 index on order_id 2023-11-25 02:39:29 +03:00
474b9caf22 patch sipro 2023-11-25 03:29:40 +04:00
6683576a70 Merge remote-tracking branch 'origin/master' 2023-11-25 01:47:05 +03:00
98a9a2cd61 sipro blueprint 2023-11-25 01:44:52 +03:00
a5b6c572d2 deshitting 2023-11-25 02:33:12 +04:00
c5cd5ded3a Merge pull request 'separation' (#1) from separation into master
Reviewed-on: #1
2023-11-25 00:11:27 +03:00
4f325898f9 Removal of log 2023-11-25 01:09:56 +04:00
82c67434e2 Submodule update 2023-11-25 01:07:25 +04:00
f0fce37a0c Assembly separation 2023-11-25 01:06:11 +04:00
52f54d086f Assembly separation 2023-11-25 01:03:10 +04:00
07735b5e2c Changes 2023-11-25 00:36:54 +04:00
b5a3b3f1d1 Structural changes 2023-11-25 00:34:30 +04:00
40 changed files with 779 additions and 174 deletions

3
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "database"]
path = database
url = https://git.denco.store/agonex/Assemblr-Database.git

7
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, request
from flask import Flask
from flask_cors import CORS
from flask_jwt_extended import JWTManager
from flask_migrate import Migrate
@@ -20,7 +20,7 @@ migrate = Migrate(app, database.db)
server_session = Session(app)
# CORS config
CORS(app, supports_credentials=True)
CORS(app, expose_headers=["Content-Range", 'Authorization'], supports_credentials=True)
jwt = JWTManager(app)
blueprints = [
(routes.auth_blueprint, '/auth'),
@@ -30,6 +30,9 @@ blueprints = [
(routes.assembly_blueprint, '/assembly'),
(routes.general_blueprint, '/general'),
(routes.application_blueprint, '/application'),
(routes.sipro_blueprint, '/sipro'),
(routes.admin_blueprint, '/admin'),
(routes.balance_blueprint, '/balance'),
]
for blueprint, url_prefix in blueprints:

View File

@@ -1,89 +0,0 @@
10-15 00:21 - assemblr - INFO - SiproClient successfully initialized
10-15 00:31 - assemblr - INFO - SiproClient successfully initialized
10-15 00:31 - assemblr - INFO - SiproClient successfully initialized
10-15 00:31 - assemblr - INFO - SiproClient successfully initialized
10-15 00:31 - assemblr - INFO - SiproClient successfully initialized
10-27 03:01 - assemblr - INFO - SiproClient successfully initialized
10-27 04:50 - assemblr - INFO - SiproClient successfully initialized
10-27 04:51 - assemblr - INFO - SiproClient successfully initialized
10-27 05:36 - assemblr - INFO - SiproClient successfully initialized
10-27 05:37 - assemblr - INFO - SiproClient successfully initialized
10-27 05:38 - assemblr - INFO - SiproClient successfully initialized
10-27 05:57 - assemblr - INFO - SiproClient successfully initialized
10-28 04:06 - assemblr - INFO - SiproClient successfully initialized
10-28 04:06 - assemblr - INFO - SiproClient successfully initialized
10-28 04:37 - assemblr - INFO - SiproClient successfully initialized
10-28 04:38 - assemblr - INFO - SiproClient successfully initialized
10-28 04:38 - assemblr - INFO - SiproClient successfully initialized
10-28 04:38 - assemblr - INFO - SiproClient successfully initialized
10-28 05:00 - assemblr - INFO - SiproClient successfully initialized
10-28 05:00 - assemblr - INFO - SiproClient successfully initialized
10-28 05:00 - assemblr - INFO - SiproClient successfully initialized
10-28 05:02 - assemblr - INFO - SiproClient successfully initialized
10-28 05:02 - assemblr - INFO - SiproClient successfully initialized
10-28 05:02 - assemblr - INFO - SiproClient successfully initialized
10-28 05:03 - assemblr - INFO - SiproClient successfully initialized
10-28 05:03 - assemblr - INFO - SiproClient successfully initialized
10-28 05:04 - assemblr - INFO - SiproClient successfully initialized
10-28 05:06 - assemblr - INFO - SiproClient successfully initialized
10-28 05:06 - assemblr - INFO - SiproClient successfully initialized
10-28 05:07 - assemblr - INFO - SiproClient successfully initialized
10-28 05:07 - assemblr - INFO - SiproClient successfully initialized
10-28 05:08 - assemblr - INFO - SiproClient successfully initialized
10-28 05:08 - assemblr - INFO - SiproClient successfully initialized
10-28 05:09 - assemblr - INFO - SiproClient successfully initialized
10-28 05:09 - assemblr - INFO - SiproClient successfully initialized
10-29 01:50 - assemblr - INFO - SiproClient successfully initialized
10-29 01:50 - assemblr - INFO - SiproClient successfully initialized
10-29 02:21 - assemblr - INFO - SiproClient successfully initialized
10-29 02:25 - assemblr - INFO - SiproClient successfully initialized
10-29 06:43 - assemblr - INFO - SiproClient successfully initialized
10-29 06:47 - assemblr - INFO - SiproClient successfully initialized
10-29 06:53 - assemblr - INFO - SiproClient successfully initialized
10-30 03:37 - assemblr - INFO - SiproClient successfully initialized
10-30 03:37 - assemblr - INFO - SiproClient successfully initialized
10-30 03:39 - assemblr - INFO - SiproClient successfully initialized
10-30 04:23 - assemblr - INFO - SiproClient successfully initialized
10-30 05:10 - assemblr - INFO - SiproClient successfully initialized
10-30 05:11 - assemblr - INFO - SiproClient successfully initialized
10-30 05:11 - assemblr - INFO - SiproClient successfully initialized
10-30 05:11 - assemblr - INFO - SiproClient successfully initialized
10-30 05:11 - assemblr - INFO - SiproClient successfully initialized
10-30 05:11 - assemblr - INFO - SiproClient successfully initialized
10-30 05:13 - assemblr - INFO - SiproClient successfully initialized
10-30 05:15 - assemblr - INFO - SiproClient successfully initialized
10-30 05:15 - assemblr - INFO - SiproClient successfully initialized
10-30 05:15 - assemblr - INFO - SiproClient successfully initialized
10-30 05:29 - assemblr - INFO - SiproClient successfully initialized
10-30 05:29 - assemblr - INFO - SiproClient successfully initialized
10-30 05:31 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:34 - assemblr - INFO - SiproClient successfully initialized
10-30 05:37 - assemblr - INFO - SiproClient successfully initialized
10-30 05:38 - assemblr - INFO - SiproClient successfully initialized
10-30 05:46 - assemblr - INFO - SiproClient successfully initialized
10-30 06:15 - assemblr - INFO - SiproClient successfully initialized
10-30 06:24 - assemblr - INFO - SiproClient successfully initialized
10-30 06:24 - assemblr - INFO - SiproClient successfully initialized
10-30 07:14 - assemblr - INFO - SiproClient successfully initialized
10-30 07:18 - assemblr - INFO - SiproClient successfully initialized
10-30 07:19 - assemblr - INFO - SiproClient successfully initialized
10-30 07:19 - assemblr - INFO - SiproClient successfully initialized
10-30 07:21 - assemblr - INFO - SiproClient successfully initialized
11-02 03:18 - assemblr - INFO - SiproClient successfully initialized
11-02 03:18 - assemblr - INFO - SiproClient successfully initialized
11-02 03:26 - assemblr - INFO - SiproClient successfully initialized
11-03 04:07 - assemblr - INFO - SiproClient successfully initialized
11-03 04:07 - assemblr - INFO - SiproClient successfully initialized
11-03 04:12 - assemblr - INFO - SiproClient successfully initialized
11-06 03:47 - assemblr - INFO - SiproClient successfully initialized
11-06 03:47 - assemblr - INFO - SiproClient successfully initialized
11-06 03:47 - assemblr - INFO - SiproClient successfully initialized
11-06 05:15 - assemblr - INFO - SiproClient successfully initialized
11-06 05:17 - assemblr - INFO - SiproClient successfully initialized
11-06 05:32 - assemblr - INFO - SiproClient successfully initialized

17
auxiliary.py Normal file
View File

@@ -0,0 +1,17 @@
def to_nested_dict(row):
result = {}
for key, value in row._mapping.items():
keys = key.split('.')
current_level = result
for part in keys[:-1]:
if part not in current_level:
current_level[part] = {}
current_level = current_level[part]
current_level[keys[-1]] = value
return result
def compile_query_to_plain_sql(query) -> str:
return query.statement.compile(compile_kwargs={
'literal_binds': True
})

Submodule database deleted from 785179e3dc

4
database/__init__.py Normal file
View File

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

16
database/enums.py Normal file
View File

@@ -0,0 +1,16 @@
from enum import unique, IntEnum
@unique
class AssemblyState(IntEnum):
NOT_STARTED = 0,
ASSEMBLING_PRODUCTS = 1,
ALL_PRODUCTS_ASSEMBLED = 2,
CONFIRMED = 3,
ENDED = 4
@unique
class BalanceTransactionType(IntEnum):
TOP_UP = 0
WITHDRAW = 1

40
database/mariadb.py Normal file
View File

@@ -0,0 +1,40 @@
import mariadb
import settings
class MariadbConnector:
def __init__(self):
# Settings
self.user = settings.MY_LOGIN
self.password = settings.MY_PASSWORD
self.host = settings.MY_HOST
self.port = int(settings.MY_PORT)
self.database = settings.MY_DATABASE
self.conn = self._get_connection()
self.cursor = self._get_cursor()
def _get_connection(self) -> mariadb.connections.Connection:
return mariadb.connect(user=self.user,
password=self.password,
host=self.host,
port=self.port,
database=self.database)
def _get_cursor(self) -> mariadb.cursors.Cursor:
return self.conn.cursor()
def select(self, query_string: str, as_list=True):
cursor = self.cursor
cursor.execute(query_string)
if not as_list:
return cursor.fetchone()
return cursor.fetchall()
def insert(self, query_string) -> int:
cursor = self.cursor
cursor.execute(query_string)
return cursor.insert_id

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)

46
database/models/basic.py Normal file
View File

@@ -0,0 +1,46 @@
from database import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, comment='ID пользователя')
login = 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')
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):
__tablename__ = 'assemblies'
id = db.Column(db.Integer, primary_key=True, comment='ID сборки')
created_at = db.Column(db.DateTime, nullable=True, comment='Дата и время начала сборки')
ended_at = db.Column(db.DateTime, nullable=True, comment='Дата и время конца сборки')
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
user = db.relationship('User', backref='assemblies')
order_id = db.Column(db.Integer, nullable=False, comment='ID заказа в базе данных', index=True)
is_active = db.Column(db.Boolean, nullable=False, comment='Активная ли сборка')
state = db.Column(db.Integer, nullable=False, comment='Состояние сборки')
class Barcode(db.Model):
__tablename__ = 'barcodes'
id = db.Column(db.Integer, primary_key=True, comment='ID пользователя')
denco_article = db.Column(db.Integer, nullable=False, comment='Артикул', index=True)
barcode = db.Column(db.String, nullable=False, comment='Баркод', index=True)
class Application(db.Model):
__tablename__ = 'applications'
id = db.Column(db.Integer, primary_key=True, comment='')
name = db.Column(db.String(40), nullable=False)
version = db.Column(db.String(10), nullable=False)
uploaded = db.Column(db.DateTime, nullable=False)
filename = db.Column(db.String, nullable=False)

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
requests
python-dotenv
python-dotenv
pydantic

View File

@@ -1,7 +1,10 @@
from routes.auth import auth_blueprint
from routes.orders import orders_blueprint
from routes.barcode import barcode_blueprint
from routes.printing import printing_blueprint
from routes.assembly import assembly_blueprint
from routes.general import general_blueprint
from routes.application import application_blueprint
from .auth import auth_blueprint
from .orders import orders_blueprint
from .barcode import barcode_blueprint
from .printing import printing_blueprint
from .assembly import assembly_blueprint
from .general import general_blueprint
from .application import application_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
version = version.strip()
application = (database.Application.query.
filter_by(version=version).
filter_by(version=version,
name=application_name).
with_entities(database.Application.id).first())
if application:
return {"error": f"Specified version ({version}) already uploaded"}, 400

View File

@@ -1,13 +1,14 @@
import datetime
import time
from flask import Blueprint, request, jsonify
from flask_jwt_extended import get_jwt_identity
from sqlalchemy.orm import joinedload
import database
from database.enums import AssemblyState
from routes.utils import jwt_protect_blueprint
import sipro.api.orders
import utils.balance
assembly_blueprint = jwt_protect_blueprint(Blueprint('assembly', __name__))
@@ -18,15 +19,33 @@ def create_assembly():
data: dict = request.json
order_id: int = data.get('orderId')
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:
response = {
'ok': False,
'message': 'Сборка этого товара уже была запущена',
'assemblyId': existing_assembly.id,
'statusCode': 'ASSEMBLY_ALREADY_EXISTS'
'statusCode': 'ASSEMBLY_ALREADY_EXISTS',
'userName': existing_assembly.user.login
}
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,
database.Assembly.is_active == True).first()
if active_assembly:
@@ -37,6 +56,22 @@ def create_assembly():
'statusCode': 'USER_ALREADY_HAS_ACTIVE_ASSEMBLY'
}
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,
order_id=order_id,
state=AssemblyState.NOT_STARTED,
@@ -61,10 +96,9 @@ def create_assembly():
return jsonify(response)
@assembly_blueprint.post('/close')
def close_assembly():
args = request.json
assembly_id = args.get('assemblyId')
def close_assembly_by_id(
assembly_id: int,
):
if not assembly_id or (not isinstance(assembly_id, int)):
response = {
'ok': False,
@@ -82,7 +116,25 @@ def close_assembly():
assembly.ended_at = datetime.datetime.now()
database.db.session.commit()
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')
@@ -97,6 +149,14 @@ def cancel_assembly():
'message': 'У вас нет активных сборок'
}
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.commit()
response = {
@@ -112,6 +172,32 @@ def cancel_assembly():
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')
def user_has_active_assembly():
user_id = get_jwt_identity()
@@ -136,6 +222,26 @@ def get_active_assembly():
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')
def confirm_assembly():
user_id = get_jwt_identity()
@@ -180,3 +286,16 @@ def update_assembly_state():
except Exception as e:
print('Error while updating')
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)

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from flask_jwt_extended import create_access_token
from werkzeug.security import generate_password_hash, check_password_hash
from database import User, db

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
import sipro.api.general

View File

@@ -1,4 +1,7 @@
import database
from flask import Blueprint, jsonify, request
from flask_jwt_extended import get_jwt_identity
from routes.utils import jwt_protect_blueprint
import sipro.api.orders
@@ -30,8 +33,10 @@ def get_orders():
@orders_blueprint.get('/getByProductId')
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)
# product_id = args.get('productId')
params['city'] = city_id
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
import sipro.api.printing
@@ -9,9 +11,14 @@ printing_blueprint = jwt_protect_blueprint(Blueprint('printing', __name__))
def get_label():
args = request.args
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)
return send_file(data,
as_attachment=True,
download_name='label.pdf',
mimetype='application/pdf')
if label_format == 'pdf':
return send_file(data,
as_attachment=True,
download_name='label.pdf',
mimetype='application/pdf')
data= json.loads(str(data.read(), 'ascii'))
return jsonify(data)

169
routes/sipro.py Normal file
View File

@@ -0,0 +1,169 @@
import datetime
from enum import StrEnum
from flask import Blueprint, request, jsonify
from sqlalchemy import func, cast, String, Numeric, Float
import auxiliary
import database
import database.enums
sipro_blueprint = Blueprint('sipro', __name__)
class ExpandParam(StrEnum):
USER = 'user'
@sipro_blueprint.before_request
def auth():
API_KEY = '5D809ED08080B5F204443B31374BD6A5'
if request.headers.get('Authorization') != API_KEY:
return {'error': 'Unauthorized'}, 401
@sipro_blueprint.post('/assemblyInfo')
def assembly_info():
params: dict = request.json
expand_param = params.get('expand')
order_ids = params.get('orderIds')
expand_list = expand_param if expand_param else []
datetime_format = 'YYYY.MM.DD, HH24:MI'
entity_list = [
database.Assembly.id.label('id'),
database.Assembly.order_id.label('order_id'),
func.to_char(database.Assembly.created_at, datetime_format).label('created_at'),
func.to_char(database.Assembly.ended_at, datetime_format).label('ended_at'),
database.Assembly.is_active.label('is_active'),
database.Assembly.state.label('state')
]
query = database.Assembly.query
for expand in expand_list:
match expand:
case ExpandParam.USER:
query = query.join(database.User)
entity_list.extend([
database.User.id.label('user.id'),
database.User.login.label('user.login'),
])
if order_ids:
query = query.filter(database.Assembly.order_id.in_(order_ids))
query = query.with_entities(*entity_list)
result = query.all()
json_result = [auxiliary.to_nested_dict(row) for row in 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'
response = client.method('GET', method, params=params)
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'
def get_label(order_product_id: str) -> BytesIO:
method = f'{router}/getLabel?orderId={order_product_id}'
def get_label(order_product_id: str, label_format:str='pdf') -> BytesIO:
method = f'{router}/getLabel?orderId={order_product_id}&format={label_format}'
response: Response = client.method('GET', method, raw=True)
data = BytesIO(response.content)
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,17 +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)