diff --git a/kassa/atol.py b/kassa/atol.py new file mode 100644 index 0000000..89e7aa2 --- /dev/null +++ b/kassa/atol.py @@ -0,0 +1,47 @@ +import requests +import json + + +class Atol: + def __init__(self, token): + self.token = token + # Вызовы функций + self.load_info() + # self.get_token() + + def load_info(self): + self.url = "https://online.atol.ru/possystem/v4/" + self.group_id = 'jkhsakha-ru_3289' + + def get_headers(self): + headers_dict = { + "Content-type": "application/json", + "charset": "utf-8", + "Token": self.token, } + return headers_dict + + def get_request(self, method, url, data): + if method == "post": + r = requests.post(self.url+url, data=json.dumps(data), + headers=self.get_headers()) + else: + r = requests.get(self.url+url, headers=self.get_headers()) + r.encoding = "utf-8" + return json.loads(r.text) + + def get_token(self, login, password): + self.login = login + self.password = password + d = {"login": self.login, "pass": self.password} + self.token, _, self.timestamp = self.get_request( + "post", "getToken", d).values() + return self.token + + def set_sell(self, reciept, is_refund): + func = '/sell' if is_refund == 0 else '/sell_refund' + s = self.get_request('post', self.group_id+func, reciept) + return s + + def get_reciepts(self, uuid): + r = self.get_request('get', self.group_id+'/' + 'report/'+uuid, None) + return r diff --git a/kassa/cruds/correction.py b/kassa/cruds/correction.py new file mode 100644 index 0000000..e69de29 diff --git a/kassa/cruds/doc.py b/kassa/cruds/doc.py new file mode 100644 index 0000000..3a50cdb --- /dev/null +++ b/kassa/cruds/doc.py @@ -0,0 +1,53 @@ +from ast import Dict +from sqlalchemy.orm import Session +import schemas +import models + + +def create_doc(db: Session, doc: schemas.Doc, external_id: str = None): + d = doc.dict() + external_id = external_id.lower() + doc_query = db.query(models.Doc).filter( + models.Doc.external_id == external_id) + error = d.pop('error', None) + payload = d.pop('payload', None) + warnings = d.pop('warnings', None) + if error: + create_error(db, error, external_id) + if payload: + create_payload(db, payload, external_id) + if doc_query.first(): + doc_query.update(values=d) + else: + doc_query = models.Doc(**d) + db.add(doc_query) + db.commit() + + +def create_error(db: Session, error: schemas.Error, external_id: str = None): + err = error + err['external_id'] = external_id + err_query = db.query(models.Error).filter( + models.Error.external_id == external_id) + if err_query.first(): + + err_query.update(values=err) + else: + err_query = models.Error(**err, synchronize_session=False) + db.add(err_query) + db.commit() + + +def create_payload(db: Session, payload: schemas.Payload, external_id: str = None): + pay = payload + + payload_query = db.query(models.Payload).filter( + models.Payload.external_id == external_id) + if payload_query.first(): + payload_query.update(pay, synchronize_session=False) + + else: + pay['external_id'] = external_id + model = models.Payload(**pay) + db.add(model) + db.commit() diff --git a/kassa/cruds/sell.py b/kassa/cruds/sell.py new file mode 100644 index 0000000..e69de29 diff --git a/kassa/databases.py b/kassa/databases.py new file mode 100644 index 0000000..6866019 --- /dev/null +++ b/kassa/databases.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine + +Base = declarative_base() +SQL_ALCHEMY_DATABASE_URL_MSSQL = f'mssql+pyodbc://sa:159357@Sanctuary/Atol?driver=SQL+Server' +engine = create_engine( + SQL_ALCHEMY_DATABASE_URL_MSSQL, connect_args={'check_same_thread': False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/kassa/kassa.py b/kassa/kassa.py new file mode 100644 index 0000000..b8dddb2 --- /dev/null +++ b/kassa/kassa.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +import databases +import cruds.doc as doc +import schemas +import models + +models.Base.metadata.create_all(databases.engine) +router = APIRouter() + +get_db = databases.get_db + + +@router.post('/{external_id}') +def insert_doc(request: schemas.Doc, external_id: str, db=Depends(get_db)): + return doc.create_doc(db, request, external_id) diff --git a/kassa/models.py b/kassa/models.py new file mode 100644 index 0000000..441648a --- /dev/null +++ b/kassa/models.py @@ -0,0 +1,80 @@ +from sqlalchemy.sql.schema import ForeignKey +from sqlalchemy import Column, Integer, String, Numeric, DateTime, Boolean +from sqlalchemy.orm import relationship +from databases import Base + + +class Payment(Base): + __tablename__ = 'payments' + + id = Column('id', Integer, primary_key=True, autoincrement=True) + external_id = Column('external_id', String(length=128)) + type = Column('type', Integer) + sum = Column('sum', Numeric(12, 2)) + + +class CorrectionInfoTable(Base): + __tablename__ = 'correction_info' + + external_id = Column('external_id', String(length=128), primary_key=True) + type = Column('type', String(length=10)) + base_date = Column('base_date', String(length=128)) + base_number = Column('base_number', String(length=128)) + + +class Error(Base): + __tablename__ = 'errors' + + external_id = Column('external_id', String(length=128), primary_key=True) + error_id = Column('error_id', String(length=128)) + code = Column('code', Integer) + text = Column('text', String(length=250)) + type = Column('type', String(length=10)) + + +class Doc(Base): + __tablename__ = 'docs' + uuid = Column('uuid', String(length=128), primary_key=True) + timestamp = Column('timestamp', String(length=128)) + group_code = Column('group_code', String(length=128)) + daemon_code = Column('daemon_code', String(length=128)) + device_code = Column('device_code', String(length=128)) + external_id = Column('external_id', String(length=128)) + callback_url = Column('callback_url', String(length=128)) + status = Column('status', String(length=128)) + + +class Atol(Base): + __tablename__ = 'atol_receipts' + uuid = Column('uuid', String(length=128), primary_key=True) + timestamp = Column('timestamp', String(length=128)) + external_id = Column('external_id', String(length=128)) + status = Column('status', String(length=128)) + + +class Payload(Base): + __tablename__ = 'payloads' + + external_id = Column('external_id', String(128), primary_key=True) + fiscal_receipt_number = Column('fiscal_receipt_number', Integer) + shift_number = Column('shift_number', Integer) + receipt_datetime = Column('receipt_datetime', String(length=128)) + total = Column('total', Numeric(12, 2)) + fn_number = Column('fn_number', String(length=128)) + ecr_registration_number = Column( + 'ecr_registration_number', String(length=128)) + fiscal_document_number = Column('fiscal_document_number', Integer) + fiscal_document_attribute = Column('fiscal_document_attribute', Integer) + fns_site = Column('fns_site', String(length=128)) + + +""" +class Sell(Base): + __tablename__ = 'sells' + + id = Column(Integer, primary_key=True, autoincrement=True) + external_id = Column('external_id',String(128)) + is_refund = Column('is_refund', Boolean) + service = Column('service', String(length=16)) + timestamp = Column('timestamp', DateTime) +""" diff --git a/kassa/new.py b/kassa/new.py new file mode 100644 index 0000000..f613136 --- /dev/null +++ b/kassa/new.py @@ -0,0 +1,319 @@ +from time import sleep +from typing import Dict, List, Tuple +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import create_engine, Table, Column, engine +from sqlalchemy.orm.session import Session +from sqlalchemy.orm import query, sessionmaker +from sqlalchemy.types import BigInteger, Integer, String, Numeric, Boolean, DateTime +from sqlalchemy.sql.sqltypes import DateTime +from sqlalchemy import desc, cast, case, func +import schemas +import models +import datetime + +Base = declarative_base() +metadata = Base.metadata + + +class PaymentDetails(Base): + __tablename__ = "payment_details" + + id = Column(Integer, primary_key=True, autoincrement=True) + external_id = Column(String(36), nullable=False, index=True) + is_refund = Column(Boolean, nullable=False, index=True) + is_taken = Column(Boolean, nullable=False, index=True) + added_date = Column(DateTime, nullable=True, + default=datetime.datetime.now()) +# class PaymentDetailsBaseModel(BaseModel): + + +class DBEngine: + def __init__(self, server, dbname, user, password): + self.dbname = dbname + self.user = user + self.password = password + self.server = server + self.get_mssql_engine() + + def get_mssql_engine(self): + query = f'mssql+pyodbc://{self.user}:{self.password}@{self.server}/{self.dbname}?driver=SQL+Server' + self.engine = create_engine( + query, connect_args={'check_same_thread': False}) + + def get_table(self, tablename: str) -> Table: + self.metadata = Base.metadata + self.metadata.reflect(bind=self.engine) + return Table(tablename, self.metadata, schema=self.dbname+'.dbo', autoload=True, autoload_with=self.engine) + + def get_columns(self, tablename: str, columns: List[str] = None, labels: Dict = None) -> List[Column]: + table = self.get_table(tablename) + if not labels: + labels = {} + if columns: + + lst = [(table.c.get(column)).label(labels.get(column, column) or column) + for column in columns] + return lst + return table.c.items() + + def get_db(self): + db = Session(autocommit=False, autoflush=False, bind=self.engine) + try: + yield db + finally: + db.close() + + +def get_payment_details(db: Session, engine_class: DBEngine): + pd = engine_class.get_table('payment_details') + external_id, is_refund, is_taken = engine_class.get_columns( + 'payment_details', ['external_id', 'is_refund', 'is_taken']) + query = db.query(pd).filter(is_taken == 0).with_entities( + external_id, is_refund, is_taken).distinct().order_by(external_id, desc(is_refund)) + return query + + +def update_payment_details(db: Session, engine_class: DBEngine, payment_details: Dict): + external_id_value = payment_details.get("external_id") + is_refund_value = payment_details.get("is_refund") + is_taken_value = payment_details.get("is_taken") + pd = engine_class.get_table('payment_details') + external_id, is_refund, is_taken = engine_class.get_columns( + 'payment_details', ['external_id', 'is_refund', 'is_taken']) + query = db.query(pd).filter(external_id == external_id_value, + is_taken == is_taken_value, is_refund == is_refund_value) + query = query.update({'is_taken': True}, synchronize_session='fetch') + db.commit() + return query + + +def get_payment(db: Session, engine_class: DBEngine, payment_details: Tuple): + external_id_value, is_refund_value, is_taken_value = payment_details + pd = engine_class.get_table('payment_details') + external_id, is_refund, is_taken = engine_class.get_columns( + 'payment_details', ['external_id', 'is_refund', 'is_taken']) + query = db.query(pd).filter(external_id == external_id_value, + is_taken == is_taken_value, is_refund == is_refund_value) + return query + + +""" +#Место сбора запроса на items с заполнением по справочникам +""" + + +def get_payment_details_items(db: Session, engine_class: DBEngine, dict_engine: DBEngine, payment_details: Tuple): + payment = get_payment( + db, engine_class, payment_details).subquery('payment') + services = dict_engine.get_table('services') + units = dict_engine.get_table('units') + payment_method = dict_engine.get_table('payment_method') + payment_object = dict_engine.get_table('payment_object') + vats = dict_engine.get_table('payment_object_vat_type') + agents = dict_engine.get_table('agents') + company = dict_engine.get_table('providers') + query = db.query( + payment.c.external_id.label('external_id'), + services.c.sname.label('name'), + units.c["sname"].label('measurement_unit'), + payment.c.price.label('price'), + payment.c.quantity.label('quantity'), + payment.c.date_operation.label('date_operation'), + payment.c.phone.label('phone'), + company.c.inn, + payment.c.summa.label('sum'), + case( + (payment.c.payment_method.in_([5, 6, 7]), 3), + (payment.c.payment_method.in_([1, 2, 3]), 2), + (payment.c.payment_method == 4, 1) + ).label("payment_group"), + payment_method.c["name"].label('payment_method'), + payment_object.c["name"].label('payment_object'), + vats.c["name"].label("vat"), + payment.c.agent_type.label('agent_info'), + case((payment.c.agent_type != None, payment.c.supplier_info), + else_=None).label('supplier_info'), + agents.c["inn"].label("supplier_inn"), + agents.c["name"].label("supplier_name") + ).select_from(payment)\ + .join(services, services.c["id_service"] == payment.c.id_item)\ + .join(units, services.c["id_unit"] == units.c["id_unit"])\ + .join(payment_method, payment.c.payment_method == payment_method.c["id"])\ + .join(payment_object, payment.c.payment_object == payment_object.c["id"])\ + .join(vats, payment.c.vat == vats.c["id"])\ + .join(company, payment.c.id_company == company.c["id_provider"])\ + .join(agents, payment.c.supplier_info == agents.c["id_agent"]) + return query + + +""" +#Функция для заполнения payments +""" + + +def get_payments(data: List): + d = {} + for row in data: + type_id = row.get("payment_group") + cur_sum = row.get("sum") + d[type_id] = cur_sum + d.get(type_id, 0.0) + res = [{'type': i[0], 'sum': i[1]} for i in d.items()] + return res + + +def get_token(db: Session, db_dict: DBEngine): + token_dict = db_dict.get_table("vAtolToken") + return token_dict.get("token") + + +""" +#Функция для заполнения total +""" + + +def get_total(data: List): + total = 0.0 + for i in data: + total += i.get("sum") + return total + + +""" +#Функция для заполнения items в dicts +""" + + +def items_convert(data: query): + items = [] + external_id = 0 + total = 0.0 + d = {} + payments = [] + phone = "" + inn = 0 + for row in data.all(): + item = dict(row) + external_id = item.get("external_id") + item["vat"] = get_vat(item.get("vat")) + agent_info = item.pop("agent_info", None) + total += item.get("sum") + type_id = item.get("payment_group") + cur_sum = item.get("sum") + phone = item.get("phone", '+79111111111') + inn = int(item.get("inn")) + d[type_id] = cur_sum + d.get(type_id, 0.0) + if agent_info: + item["agent_info"] = {"type": agent_info} + item["supplier_info"] = { + "inn": item.pop("supplier_inn", None), + "name": item.pop("supplier_name", None) + } + else: + for i in ["supplier_info", 'supplier_inn', 'supplier_name']: + del item[i] + items.append(item) + payments = [{'type': i[0], 'sum': i[1]} for i in d.items()] + client = get_client(phone) + company = get_company(inn) + return external_id, items, payments, client, company, total + + +def get_company(inn: int): + company = {} + company['inn'] = inn + company['email'] = 'ocnkp@jkhsakha.ru' + company['payment_address'] = 'http://jkhsakha.ru/' + company['sno'] = 'osn' + return company + + +def get_receipt(data: query): + receipt = {} + external_id = 0 + external_id, items, payments, client, company, total = items_convert( + data) + receipt['client'] = client + receipt["payments"] = payments + receipt["company"] = company + receipt['items'] = items + receipt['total'] = total + return external_id, receipt + + +def get_sell(data: query): + sell = {} + current_datetime = datetime.datetime.now() + external_id, sell['receipt'] = get_receipt(data) + sell["timestamp"] = current_datetime.strftime( + '%d.%m.%Y %H:%M:%S') + sell["external_id"] = external_id + sell["service"] = { + 'callback_url': f"http://api.jkhsakha.ru/kassa/{external_id}" + } + return sell + + +def get_client(phone: str): + return { + 'phone': phone + } + + +def get_vat(vat: str): + return {'type': vat} + + +def add_atol(db: Session, engine: DBEngine, atol: Dict): + aa = engine.get_table("Atol") + a = aa.insert().values(atol) + db.execute(a) + db.commit() + + +def add_doc(sell: schemas.Sell, is_refund): + from databases import SessionLocal + from atol import Atol + atol_model = Atol + session = SessionLocal() + a = Atol.set_sell(atol_model, sell, is_refund) + check = models.Atol(**a) + session.add(check) + session.commit() + session.close() + + +def get_payment(): + server = 'Sanctuary' + user = 'sa' + password = '159357' + dbname = 'fz54_details' + db_dicts_name = 'fz54' + db = DBEngine(server, dbname, user, password) + db_dicts = DBEngine(server, db_dicts_name, user, password) + session = Session(autocommit=False, autoflush=False, bind=db.engine) + payment = get_payment_details(session, db).first() + if payment: + payments = get_payment_details_items(session, db, db_dicts, payment) + body = get_sell(payments) + sell = schemas.Sell(**body) + payment = dict(payment) + atol = { + "external_id": payment.get("external_id"), + "is_refund": payment.get("is_refund"), + "is_taken": payment.get("is_taken"), + "body": str(body) + } + add_atol(session, db, atol) + update_payment_details(session, db, payment) + return sell, payment.get("is_refund") + return False, False + + +if __name__ == "__main__": + while True: + payment, is_refund = get_payment() + if payment == False: + break + sleep(1) + add_doc(sell=payment, is_refund=is_refund) + break diff --git a/kassa/schemas.py b/kassa/schemas.py new file mode 100644 index 0000000..672e66e --- /dev/null +++ b/kassa/schemas.py @@ -0,0 +1,222 @@ +from ast import Try +from typing import List, Optional, Dict +from pydantic import BaseModel, Field, confloat, constr +from sqlalchemy.sql.sqltypes import Boolean, DateTime +from sqlalchemy.sql.sqltypes import DateTime + + +class SumNumberTwoFormat(BaseModel): + __root__: Optional[confloat(ge=0.0, le=100000000.0, multiple_of=0.01)] + + +class NumberPrice(BaseModel): + __root__: confloat(ge=0.0, le=42949673.0, multiple_of=0.01) + + +class PhoneNumber(BaseModel): + __root__: constr(regex=r'^([^\s\\]{0,17}|\+[^\s\\]{1,18})$') + + +class NumberTwoFormat(BaseModel): + __root__: confloat(ge=0.0, le=100000000.0, multiple_of=0.01) + + +class NumberThreeFormat(BaseModel): + __root__: confloat(ge=0.0, le=100000.0, multiple_of=0.001) + + +class Service(BaseModel): + callback_url: Optional[constr(max_length=256)] = None + + +class Warnings(BaseModel): + callback_url: str = None + + +class PayingAgent(BaseModel): + operation: Optional[str] = None + phones: Optional[List[PhoneNumber]] = None + + +class SupplierInfo(BaseModel): + phones: Optional[List[PhoneNumber]] = None + name: Optional[str] = None + inn: Optional[str] = None + + class Config: + orm_mode = True + + +class ReceivePaymentsOperator(BaseModel): + phones: Optional[List[PhoneNumber]] = None + + +class MoneyTransferOperator(BaseModel): + phones: Optional[List[PhoneNumber]] = None + name: Optional[str] = None + address: Optional[str] = None + inn: Optional[constr(regex=r'(^[0-9]{10}$)|(^[0-9]{12}$)')] = None + + +class Company(BaseModel): + email: Optional[constr(max_length=64)] = None + sno: Optional[str] = None + inn: constr(max_length=12) + payment_address: constr(max_length=256) + + class Config: + orm_mode = True + + +class ClientItem(BaseModel): + email: constr(max_length=64) = None + phone: Optional[constr(max_length=64)] = None + + +class AdditionalUserProps(BaseModel): + name: constr(max_length=64) + value: constr(max_length=256) + + +class AgentInfo(BaseModel): + type: Optional[str] = None + paying_agent: Optional[PayingAgent] = None + receive_payments_operator: Optional[ReceivePaymentsOperator] = None + money_transfer_operator: Optional[MoneyTransferOperator] = None + + class Config: + orm_mode = True + + +class Error(BaseModel): + error_id: Optional[str] = None + code: int + text: str + type: Optional[str] = None + + class Config: + orm_mode = True + + +class Payload(BaseModel): + fiscal_receipt_number: int + shift_number: int + receipt_datetime: str + total: float + fn_number: str + ecr_registration_number: str + fiscal_document_number: int + fiscal_document_attribute: int + fns_site: str + + +class Vat(BaseModel): + type: Optional[str] = None + + class Config: + orm_mode = True + + +class CorrectionInfo(BaseModel): + type: str + base_date: str + base_number: str + + +class Payment(BaseModel): + type: int + sum: SumNumberTwoFormat + + class Config: + orm_mode = True + + +class Item(BaseModel): + name: str + price: NumberPrice + quantity: NumberThreeFormat + sum: SumNumberTwoFormat + measurement_unit: Optional[constr(max_length=16)] = None + payment_method: Optional[str] = None + payment_object: Optional[str] = None + nomenclature_code: Optional[str] = None + vat: Optional[Vat] = None + agent_info: Optional[AgentInfo] = None + supplier_info: Optional[SupplierInfo] = None + user_data: Optional[constr(max_length=64)] = None + excise: Optional[confloat(ge=0.0)] = None + country_code: Optional[constr( + regex=r'^[0-9]*$', min_length=1, max_length=3)] = None + declaration_number: Optional[constr(min_length=1, max_length=32)] = None + + class Config: + orm_mode = True + + +class Receipt(BaseModel): + client: ClientItem + company: Company + agent_info: Optional[AgentInfo] = None + supplier_info: Optional[SupplierInfo] = None + items: List[Item] = Field(..., min_items=1) + payments: List[Payment] = Field(..., max_items=10, min_items=1) + vats: Optional[List[Vat]] = Field(None, max_items=6, min_items=1) + total: NumberTwoFormat + additional_check_props: Optional[constr(max_length=16)] = None + cashier: Optional[constr(max_length=64)] = None + additional_user_props: Optional[AdditionalUserProps] = None + + class Config: + orm_mode = True + + +class Correction(BaseModel): + company: Company + cashier: Optional[constr(max_length=64)] = None + correction_info: CorrectionInfo + payments: List[int] = Field(..., max_items=10, min_items=1) + vats: List[Vat] = Field(..., max_items=6, min_items=1) + + class Config: + orm_mode = True + + +class Doc(BaseModel): + uuid: str + timestamp: str + group_code: str + daemon_code: str + device_code: str + external_id: Optional[str] = None + callback_url: Optional[str] = None + status: Optional[str] = None + error: Error = None + warnings: Optional[Warnings] = None + payload: Payload = None + + class Config: + orm_mode = True + + +class Sell(BaseModel): + external_id: constr(max_length=128) + receipt: Receipt + service: Optional[Service] = None + timestamp: str = None + + class Config: + arbitrary_types_allowed = True + + +class SellCreate(Sell): + is_refund: bool + + +class SellShow(SellCreate): + id: int + + +class Correction(BaseModel): + timestamp: str + external_id: constr(max_length=128) + service: Optional[Service] = None diff --git a/main.py b/main.py index 2737cf5..bd61218 100644 --- a/main.py +++ b/main.py @@ -3,14 +3,15 @@ from fastapi.middleware import Middleware from fastapi.middleware.cors import CORSMiddleware from kv import kv from auth import auth +from kassa import kassa import uvicorn middleware = [Middleware( CORSMiddleware, - allow_origins = ['*'], - allow_credentials = True, - allow_methods = ['*'], - allow_headers = ['*'], + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], )] app = FastAPI(middleware=middleware) @@ -18,15 +19,26 @@ app = FastAPI(middleware=middleware) @app.get('/hello') async def say_hello(): - return { "text": "Hello!" } + return {"text": "Hello!"} app.include_router( - router = auth.router, - prefix= '/auth', + router=auth.router, + prefix='/auth', + tags=['Авторизация'], + responses={404: {"description": "Not found"}} +) +app.include_router( + router=kv.router, + prefix='/kv', + tags=['Кварплата'], + responses={404: {"description": "Not found"}} +) +app.include_router( + router=kassa.router, + prefix='/kassa', + tags=['Касса Атол'], responses={404: {"description": "Not found"}} ) -app.include_router(router = kv.router, prefix = '/kv',tags=['Кварплата'], responses={404: {"description": "Not found"}}) - if __name__ == "__main__": - uvicorn.run("main:app", host = "0.0.0.0", port = 5000) \ No newline at end of file + uvicorn.run("main:app", host="0.0.0.0", port=5000)