Compare commits
19
Commits
b63ca268b9
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee3a5025c1 | ||
|
|
0cbf543940 | ||
|
|
b74b0067a6 | ||
|
|
6cd225f395 | ||
|
|
4ddac54ab7 | ||
|
|
439f7b3c15 | ||
|
|
9fd5481461 | ||
|
|
198784f144 | ||
|
|
759c2216c6 | ||
|
|
d1f6c1830f | ||
|
|
c7b1929fdc | ||
|
|
189df529d9 | ||
|
|
abb1c01753 | ||
|
|
7e25cd6674 | ||
|
|
db0cc4007c | ||
|
|
3061b86524 | ||
|
|
cc7cd9876c | ||
|
|
f7426fb6ea | ||
|
|
eae9248dc5 |
@@ -50,7 +50,7 @@ class SubRecord(pydantic.BaseModel):
|
||||
class Record(BaseModel):
|
||||
id: Optional[uuid.UUID] = Field(alias='_id')
|
||||
title: str = Field(description='عنوان در این فیلد قرار میگیرد')
|
||||
send: SubRecord = Field(description='اطلاعات ارسال در این فیلد ذخیره می شود')
|
||||
send: Optional[SubRecord] = Field(description='اطلاعات ارسال در این فیلد ذخیره می شود')
|
||||
receive: Optional[SubRecord] = Field(description='اطلاعات دریافت در این فیلد ذخیره می شود')
|
||||
|
||||
class RecordModel(Record):
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
مدل ORM برای جدول const در MySQL.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text
|
||||
from core.service.mysql_orm import Base
|
||||
|
||||
|
||||
class Const(Base):
|
||||
"""مدل جدول const"""
|
||||
__tablename__ = "const"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(255), nullable=False, index=True)
|
||||
title_fa = Column(String(255), nullable=True)
|
||||
value = Column(Text, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Const(id={self.id}, key='{self.key}', title_fa='{self.title_fa}')>"
|
||||
|
||||
def to_dict(self):
|
||||
"""تبدیل به dict برای JSON response"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"key": self.key,
|
||||
"title_fa": self.title_fa,
|
||||
"value": self.value,
|
||||
}
|
||||
@@ -151,5 +151,8 @@ class ProjectStatusAgreement(BaseModel):
|
||||
project_id: Optional[uuid.UUID] = Field(description="کد شناسایی پروژه در این فیلد قرار می گیرد")
|
||||
status:str
|
||||
|
||||
class ListWithIds(pydantic.BaseModel):
|
||||
ids: list[uuid.UUID]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,5 @@ class RegisterStatusModel(BaseModel):
|
||||
amount: Optional[AmountStatus] = Field(description="مبالغ")
|
||||
related_to_number_start:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
|
||||
related_to_number_end:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
|
||||
date_at: Optional[datetime.datetime] = Field(description='تاریخ پرداخت')
|
||||
amount_ّfinancial: Optional[int] = Field(description='مبلغ صورت وضعیت در قسمت مالی')
|
||||
|
||||
@@ -22,10 +22,14 @@ class DelayDonutDetail(pydantic.BaseModel):
|
||||
project_id:uuid.UUID
|
||||
select:Literal['financial','force_major','technical']
|
||||
|
||||
class DonutReport(pydantic.BaseModel):
|
||||
only_current: bool = Field(default=False, description='فقط پروژههای نظارت جاری؛ مقدار false برای همه پروژههای نظارت')
|
||||
|
||||
|
||||
class DonutList(pydantic.BaseModel):
|
||||
type: int = Field(description='مقدار وارد شده در گزارش قبل')
|
||||
donut_select: str = Field(description='آیتم دونات انتخاب شده')
|
||||
|
||||
class PhysicalMonth(pydantic.BaseModel):
|
||||
from_date:datetime.datetime
|
||||
to_date:datetime.datetime
|
||||
to_date:datetime.datetime
|
||||
|
||||
@@ -87,6 +87,7 @@ class ContractRepository(BaseRepository):
|
||||
{'$match':query['query']},
|
||||
{'$group': {"_id": None, 'count': {'$sum': 1},'sum':{"$sum":"$amount"}}}
|
||||
]).to_list(None)
|
||||
return count[0]
|
||||
else:
|
||||
return []
|
||||
print(count)
|
||||
if len(count)>0:
|
||||
return count[0]
|
||||
return []
|
||||
|
||||
+740
-652
File diff suppressed because it is too large
Load Diff
@@ -139,6 +139,34 @@ class PhysicalProgressRepository(BaseRepository):
|
||||
else:
|
||||
await use_collection(COLLECTION).update_one({"_id": _i_model.id},{"$set": _update})
|
||||
return True
|
||||
# for recalc medium progress in database
|
||||
# async def recalc_medium_progress(self):
|
||||
# print('111111111111111111111')
|
||||
# coll = use_collection(TaskEnv.CLN_MONITOR_PYSICAL)
|
||||
# project_ids = await coll.distinct(
|
||||
# "project_id",
|
||||
# {"deleted_at": {"$exists": False}},
|
||||
# )
|
||||
# print('222222222222222222222')
|
||||
#
|
||||
# updated = 0
|
||||
# for pid in project_ids:
|
||||
# print('piiiiiidf',pid)
|
||||
# records = await coll.find({
|
||||
# "project_id": pid,
|
||||
# "deleted_at": {"$exists": False},
|
||||
# }).sort([("date_at", 1), ("_id", 1)]).to_list(None)
|
||||
#
|
||||
# total = 0
|
||||
# for i, rec in enumerate(records, start=1):
|
||||
# total += rec.get("real") or 0
|
||||
# await coll.update_one(
|
||||
# {"_id": rec["_id"]},
|
||||
# {"$set": {"medium_progress": round(total / i, 1)}},
|
||||
# )
|
||||
# updated += 1
|
||||
#
|
||||
# return {"projects": len(project_ids), "updated": updated}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Repository برای جدول const در MySQL - برای استفاده در خروجی اکسل.
|
||||
"""
|
||||
from typing import List, Dict, Optional
|
||||
from sqlalchemy import select
|
||||
from core.service.mysql_orm import get_mysql_session
|
||||
from app.models.mysql_const import Const
|
||||
|
||||
|
||||
class MySQLConstRepository:
|
||||
"""Repository برای خواندن دیتا از جدول const"""
|
||||
|
||||
async def get_by_keys(self, keys: List[str]) -> Dict[int, str]:
|
||||
"""
|
||||
گرفتن const ها با لیست key ها و برگرداندن dict به صورت {id: title_fa}
|
||||
مثال: get_by_keys(['nature', 'type']) -> {2: 'طبیعت 2', 9: 'نوع 9'}
|
||||
"""
|
||||
async with get_mysql_session() as session:
|
||||
result = await session.execute(
|
||||
select(Const).where(Const.key.in_(keys))
|
||||
)
|
||||
consts = result.scalars().all()
|
||||
return {c.id: c.title_fa or c.value or str(c.id) for c in consts}
|
||||
|
||||
async def get_by_key(self, key: str) -> Dict[int, str]:
|
||||
"""گرفتن const ها با یک key و برگرداندن dict به صورت {id: title_fa}"""
|
||||
return await self.get_by_keys([key])
|
||||
|
||||
async def get_all_consts_dict(self) -> Dict[int, str]:
|
||||
"""گرفتن همه const ها به صورت {id: title_fa} - برای کش کردن"""
|
||||
async with get_mysql_session() as session:
|
||||
result = await session.execute(select(Const))
|
||||
consts = result.scalars().all()
|
||||
return {c.id: c.title_fa or c.value or str(c.id) for c in consts}
|
||||
+76
-14
@@ -7,7 +7,7 @@ import requests
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.config import TaskEnv
|
||||
from app.models.project import ProjectModel, UpdateSts, ProjectSts, Design, ProjectStatusAgreement
|
||||
from app.models.project import ProjectModel, UpdateSts, ProjectSts, Design, ProjectStatusAgreement, ListWithIds
|
||||
from core.repositories.base import BaseRepository
|
||||
from core.service.mongo_db import use_collection
|
||||
from core.utils.table_engine import TableEngineRequestType, TableEngineExport
|
||||
@@ -66,14 +66,14 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
|
||||
async def create(self, coding: ProjectModel):
|
||||
coding.counter = 1
|
||||
last_project = await use_collection(COLLECTION).find({}).sort("$natural", -1).to_list(None)
|
||||
last_project = await use_collection(COLLECTION).find({}).sort("counter", -1).to_list(None)
|
||||
if last_project:
|
||||
coding.counter = last_project[0]["counter"] + 1
|
||||
code = self.create_code(coding)
|
||||
coding.design.code = 'D' + code
|
||||
coding.design.code = code
|
||||
coding.init_id().cu_at()
|
||||
if coding.monitor:
|
||||
coding.monitor.code = 'S' + code
|
||||
coding.monitor.code = code
|
||||
coding.has_monitor = True
|
||||
await self.create_report_price(coding)
|
||||
if coding.monitor.detail.date_create_project and coding.monitor.detail.first_time:
|
||||
@@ -95,12 +95,12 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
old_project = ProjectModel(**old_project_dict)
|
||||
if update.monitor and not old_project.monitor:
|
||||
code = self.create_code(update)
|
||||
update.monitor.code = 'S' + code
|
||||
update.monitor.code = code
|
||||
if update.design.nature != old_project.design.nature or update.design.type != old_project.design.type:
|
||||
code = self.create_code(update)
|
||||
update.design.code = 'D' + code
|
||||
update.design.code = code
|
||||
if update.monitor:
|
||||
update.monitor.code = 'S' + code
|
||||
update.monitor.code = code
|
||||
if update.monitor:
|
||||
update.has_monitor = True
|
||||
await self.update_report_price(update)
|
||||
@@ -175,7 +175,16 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
return True
|
||||
|
||||
async def list_project_design(self):
|
||||
project = await use_collection(COLLECTION).find({'monitor': {"$exists": 0}}).to_list(None)
|
||||
project = await use_collection(COLLECTION).find({
|
||||
'$or': [
|
||||
{'has_monitor': False},
|
||||
{'monitor': {'$exists': 0}},
|
||||
{
|
||||
'has_monitor': True,
|
||||
'monitor.has_design': True
|
||||
}
|
||||
]
|
||||
}).to_list(None)
|
||||
return project
|
||||
|
||||
async def list_project_monitor(self):
|
||||
@@ -200,6 +209,13 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
project = await use_collection(TaskEnv.CLN_PROJECT).find({}).to_list(None)
|
||||
return project
|
||||
|
||||
async def list_project_with_select(self):
|
||||
project = await use_collection(TaskEnv.CLN_PROJECT).find({},{"_id":1,"design.title":1,"design.code":1}).to_list(None)
|
||||
return project
|
||||
async def list_project_with_ids(self,inp:ListWithIds):
|
||||
projects = await use_collection(TaskEnv.CLN_PROJECT).find({"_id":{"$in":inp.ids}}).to_list(None)
|
||||
return projects
|
||||
|
||||
async def set_sts_two_for_monitor(self, project_id: uuid.UUID):
|
||||
project = await use_collection(TaskEnv.CLN_PROJECT).find_one({"_id": project_id})
|
||||
if not ('sts' in project['monitor'] and project['monitor']['sts'] > 1):
|
||||
@@ -224,25 +240,69 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
|
||||
return projects
|
||||
|
||||
@staticmethod
|
||||
def _has_design_sts_filter(te_input) -> bool:
|
||||
if not te_input:
|
||||
return False
|
||||
f_list = getattr(te_input, 'f', None) if not isinstance(te_input, dict) else te_input.get('f')
|
||||
if not f_list:
|
||||
return False
|
||||
for scope in f_list:
|
||||
items = scope.get('items', []) if isinstance(scope, dict) else getattr(scope, 'items', [])
|
||||
if items:
|
||||
for item in items:
|
||||
col = item.get('col') if isinstance(item, dict) else getattr(item, 'col', '')
|
||||
if col == 'design.sts':
|
||||
return True
|
||||
return False
|
||||
|
||||
async def fetch_delete(self, _input: TableEngineRequestType) -> TableEngineExport[T]:
|
||||
return await NewTableEngine.mongo(
|
||||
extra_filter = [{'sts': 2}]
|
||||
if self._has_design_sts_filter(_input.te if _input else None):
|
||||
extra_filter.append({
|
||||
'$or': [
|
||||
{'has_monitor': False},
|
||||
{'monitor': {'$exists': False}},
|
||||
{'has_monitor': True, 'monitor.has_design': True}
|
||||
]
|
||||
})
|
||||
result = await NewTableEngine.mongo(
|
||||
query_class=use_collection(self.COLLECTION),
|
||||
soft_deleted=True,
|
||||
model=self.MODEL,
|
||||
search_cols=['design.title'],
|
||||
extra_filter=[{'sts': 2}],
|
||||
extra_filter=extra_filter,
|
||||
inputs=_input.te) \
|
||||
.to_dict()
|
||||
if result and result.rows:
|
||||
for row in result.rows:
|
||||
if row.monitor and row.monitor.has_design is False:
|
||||
row.design.sts = None
|
||||
return result
|
||||
|
||||
async def fetch(self, _input: TableEngineRequestType) -> TableEngineExport[T]:
|
||||
return await NewTableEngine.mongo(
|
||||
extra_filter = [{'sts': 1}]
|
||||
if self._has_design_sts_filter(_input.te if _input else None):
|
||||
extra_filter.append({
|
||||
'$or': [
|
||||
{'has_monitor': False},
|
||||
{'monitor': {'$exists': False}},
|
||||
{'has_monitor': True, 'monitor.has_design': True}
|
||||
]
|
||||
})
|
||||
result = await NewTableEngine.mongo(
|
||||
query_class=use_collection(self.COLLECTION),
|
||||
soft_deleted=True,
|
||||
model=self.MODEL,
|
||||
search_cols=['design.title'],
|
||||
extra_filter=[{'sts': 1}],
|
||||
extra_filter=extra_filter,
|
||||
inputs=_input.te) \
|
||||
.to_dict()
|
||||
if result and result.rows:
|
||||
for row in result.rows:
|
||||
if row.monitor and row.monitor.has_design is False:
|
||||
row.design.sts = None
|
||||
return result
|
||||
|
||||
async def get_all(self):
|
||||
url = 'http://api.khanesaziceco.ir/api/api/project/table'
|
||||
@@ -261,8 +321,10 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
||||
_set = ProjectModel(
|
||||
sts=1,
|
||||
counter=i['id'],
|
||||
design=Design(**i)
|
||||
).init_id().cu_at()
|
||||
design=Design(**i),
|
||||
created_at=i['created_at'],
|
||||
updated_at=i['updated_at'],
|
||||
).init_id()
|
||||
obj.append(_set.mongo_dict())
|
||||
|
||||
await use_collection(TaskEnv.CLN_PROJECT).insert_many(obj)
|
||||
|
||||
@@ -19,3 +19,57 @@ class RegisterStatusRepository(BaseRepository):
|
||||
async def get_list(self,project_id:uuid.UUID):
|
||||
list_status = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find({"project_id":project_id}).to_list(None)
|
||||
return list_status
|
||||
async def create(self, coding: RegisterStatusModel):
|
||||
result = await super().create(coding)
|
||||
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': coding.project_id})
|
||||
if price and coding.amount_ّfinancial:
|
||||
if coding.sts ==1:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': coding.project_id},
|
||||
{"$inc": {'amount_status': coding.amount_ّfinancial}}
|
||||
)
|
||||
elif coding.sts == 2:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': coding.project_id},
|
||||
{"$inc": {'amount_adjust': coding.amount_ّfinancial}}
|
||||
)
|
||||
return result
|
||||
|
||||
async def update(self, _id: uuid.UUID, _update: RegisterStatusModel):
|
||||
_old_dic = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find_one({'_id':_id})
|
||||
change = 0
|
||||
result = await super().update(_id, _update)
|
||||
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': _update.project_id})
|
||||
if price:
|
||||
_old = RegisterStatusModel(**_old_dic)
|
||||
if 'amount_ّfinancial' not in _old_dic and _update.amount_ّfinancial:
|
||||
change = _update.amount_ّfinancial
|
||||
if _old.amount_ّfinancial != _update.amount_ّfinancial:
|
||||
change = _update.amount_ّfinancial - _old.amount_ّfinancial
|
||||
if change != 0 :
|
||||
if _old.sts == 1:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _update.project_id},
|
||||
{"$inc": {'amount_status': change}}
|
||||
)
|
||||
elif _old.sts == 2:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _update.project_id},
|
||||
{"$inc": {'amount_adjust': change}}
|
||||
)
|
||||
return result
|
||||
async def delete(self, _id: uuid.UUID):
|
||||
_old_dic = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find_one({'_id': _id})
|
||||
await super().delete(_id)
|
||||
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': _old_dic['project_id']})
|
||||
if price and _old_dic['sts'] == 1 and 'amount_status' in price:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _old_dic['project_id']},
|
||||
{"$inc": {'amount_status': -(_old_dic['amount_ّfinancial'])}}
|
||||
)
|
||||
if price and _old_dic['sts'] == 2 and 'amount_adjust' in price:
|
||||
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
|
||||
{'project_id': _old_dic['project_id']},
|
||||
{"$inc": {'amount_adjust': -(_old_dic['amount_ّfinancial'])}}
|
||||
)
|
||||
return True
|
||||
|
||||
+187
-9
@@ -3,20 +3,30 @@ from datetime import time
|
||||
|
||||
from app.config import TaskEnv
|
||||
from app.models.project import ProjectPriceReport
|
||||
from app.models.report import StatusReport, ReportCountry, DonutList, DelayDonutDetail, PhysicalMonth
|
||||
from app.models.report import StatusReport, ReportCountry, DonutList, DonutReport, DelayDonutDetail, PhysicalMonth
|
||||
from core.service.mongo_db import use_collection
|
||||
|
||||
|
||||
class ReportRepository():
|
||||
async def project_status(self, _inp: StatusReport, type):
|
||||
if _inp.state:
|
||||
match = {f'{type}.sts': {'$exists': 1}, 'design.state': _inp.state}
|
||||
match = {f'{type}.sts': {'$exists': 1}, 'design.state': _inp.state,'sts':1}
|
||||
else:
|
||||
match = {f'{type}.sts': {'$exists': 1}}
|
||||
match = {f'{type}.sts': {'$exists': 1},'sts':1}
|
||||
if _inp.type_id:
|
||||
match.update({'design.type_id': _inp.type_id})
|
||||
if _inp.nature_id:
|
||||
match.update({'design.nature_id': _inp.nature_id})
|
||||
if type == 'design':
|
||||
match.update({
|
||||
'$or': [
|
||||
{'has_monitor': False},
|
||||
{
|
||||
'monitor.has_design': True,
|
||||
'has_monitor': True
|
||||
}
|
||||
]
|
||||
})
|
||||
if type == 'monitor' and _inp.monitor_type:
|
||||
match.update({'monitor.base_information.type': _inp.monitor_type})
|
||||
report = await use_collection(TaskEnv.CLN_PROJECT).aggregate([
|
||||
@@ -140,6 +150,64 @@ class ReportRepository():
|
||||
]).to_list(None)
|
||||
return report
|
||||
|
||||
async def donut_v2(self, _select: int, _inp: DonutReport):
|
||||
project_filter = {"has_monitor": True}
|
||||
if _inp.only_current:
|
||||
project_filter["monitor.sts"] = 2
|
||||
|
||||
report = None
|
||||
if _select == 1:
|
||||
report = await use_collection(TaskEnv.CLN_PROJECT).aggregate([
|
||||
{"$match": project_filter},
|
||||
{"$group": {"_id": "$design.nature_id", 'count': {'$sum': 1}}}
|
||||
]).to_list(None)
|
||||
elif _select == 2:
|
||||
report = await use_collection(TaskEnv.CLN_PROJECT).aggregate([
|
||||
{"$match": project_filter},
|
||||
{
|
||||
"$group": {
|
||||
"_id": {
|
||||
"$cond": [
|
||||
{"$lt": ["$design.building_infrastructure", 1000]}, # Check if a < 10
|
||||
"less_than_1000",
|
||||
{
|
||||
"$cond": [
|
||||
{"$and": [{"$gte": ["$design.building_infrastructure", 1000]},
|
||||
{"$lt": ["$design.building_infrastructure", 5000]}]},
|
||||
# Check if 10 <= a < 20
|
||||
"between_1000_and_5000",
|
||||
{
|
||||
"$cond": [
|
||||
{"$and": [{"$gte": ["$design.building_infrastructure", 5000]},
|
||||
{"$lt": ["$design.building_infrastructure", 15000]}]},
|
||||
"between_5000_and_15000",
|
||||
{
|
||||
"$cond": [
|
||||
{"$and": [{"$gte": ["$design.building_infrastructure", 15000]},
|
||||
{"$lt": ["$design.building_infrastructure", 30000]}]},
|
||||
"between_15000_and_30000",
|
||||
"greater_than_or_equal_30000" # Otherwise, a >= 30
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"total": {"$sum": 1},
|
||||
}
|
||||
}
|
||||
]).to_list(None)
|
||||
|
||||
elif _select == 3:
|
||||
report = await use_collection(TaskEnv.CLN_PROJECT).aggregate([
|
||||
{"$match": project_filter},
|
||||
{"$group": {"_id": "$design.type_id", 'count': {'$sum': 1}}}
|
||||
]).to_list(None)
|
||||
return report
|
||||
|
||||
|
||||
async def list_donut(self, _inp: DonutList):
|
||||
_filter = None
|
||||
if _inp.type == 1:
|
||||
@@ -220,6 +288,13 @@ class ReportRepository():
|
||||
async def categorize_deviation(self):
|
||||
report = await use_collection(TaskEnv.CLN_MONITOR_PYSICAL).aggregate(
|
||||
[{"$match": {"deviation": {"$exists": True, "$ne": None}}},
|
||||
{"$lookup": {
|
||||
"from": TaskEnv.CLN_PROJECT,
|
||||
"localField": "project_id",
|
||||
"foreignField": "_id",
|
||||
"as": "project"
|
||||
}},
|
||||
{"$match": {"project.monitor.sts": 2}},
|
||||
{"$group": {
|
||||
"_id": {
|
||||
"$cond": [
|
||||
@@ -257,6 +332,28 @@ class ReportRepository():
|
||||
]).to_list(None)
|
||||
return report
|
||||
|
||||
async def delay_donut_all_v2(self, _inp: DonutReport):
|
||||
project_filter = {"project.has_monitor": True}
|
||||
if _inp.only_current:
|
||||
project_filter["project.monitor.sts"] = 2
|
||||
|
||||
report = await use_collection(TaskEnv.CLN_BILL_DELAY).aggregate([
|
||||
{"$lookup": {
|
||||
"from": TaskEnv.CLN_PROJECT,
|
||||
"localField": "project_id",
|
||||
"foreignField": "_id",
|
||||
"as": "project"
|
||||
}},
|
||||
{"$match": project_filter},
|
||||
{'$group': {"_id": None, "total_financial": {"$sum": "$financial.total"},
|
||||
"total_force_major": {"$sum": "$force_major.total"},
|
||||
"total_technical": {"$sum": "$technical.total"},
|
||||
"total_extra_new_work": {"$sum": "$extra_new_work"},
|
||||
"total_other": {"$sum": "$other"},
|
||||
}}
|
||||
]).to_list(None)
|
||||
return report
|
||||
|
||||
async def list_delay_donut(self, _select: str):
|
||||
get_list = []
|
||||
_map = {'1': 'financial.total', '2': 'force_major.total', '3': 'technical.total', '4': 'extra_new_work',
|
||||
@@ -402,20 +499,55 @@ class ReportRepository():
|
||||
to_date = datetime.combine(_inp.to_date.date(), time.max)
|
||||
from_date = _inp.from_date.combine(_inp.from_date.date(), time.min)
|
||||
result = await use_collection(TaskEnv.CLN_MONITOR_PYSICAL).aggregate([
|
||||
{"$match":{"date_at":{"$lte":to_date,"$gte":from_date}}},
|
||||
{
|
||||
"$sort": {'date_at': -1}
|
||||
"$match": {
|
||||
"date_at": {
|
||||
"$lte": to_date,
|
||||
"$gte": from_date
|
||||
}
|
||||
}
|
||||
},
|
||||
{"$group":
|
||||
{'_id': {"year": "$year", "month": "$month"},
|
||||
'avg': {'$avg': "$real"}},
|
||||
}
|
||||
{
|
||||
"$lookup": {
|
||||
"from": TaskEnv.CLN_PROJECT,
|
||||
"localField": "project_id",
|
||||
"foreignField": "_id",
|
||||
"as": "project"
|
||||
}
|
||||
},
|
||||
{"$match": {"project.monitor.sts": 2}},
|
||||
{
|
||||
"$group": {
|
||||
"_id": {
|
||||
"year": "$year",
|
||||
"month": "$month"
|
||||
},
|
||||
"avg": {
|
||||
"$avg": "$real"
|
||||
},
|
||||
"sort_date": {
|
||||
"$max": "$date_at"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$sort": {
|
||||
"sort_date": -1
|
||||
}
|
||||
}
|
||||
]).to_list(None)
|
||||
return result
|
||||
|
||||
async def design_progress_status(self):
|
||||
result = await use_collection("design_progress").aggregate([
|
||||
{'$match':{'complete_at':{"$exists":0},'parent_id':{"$exists":0}}},
|
||||
{"$lookup": {
|
||||
"from": TaskEnv.CLN_PROJECT,
|
||||
"localField": "project_id",
|
||||
"foreignField": "_id",
|
||||
"as": "project"
|
||||
}},
|
||||
{"$match": {"project.design.sts": 2}},
|
||||
{"$group":
|
||||
{'_id': '$type',
|
||||
'count': {'$sum': 1},
|
||||
@@ -428,6 +560,13 @@ class ReportRepository():
|
||||
async def design_progress_status_state(self):
|
||||
result = await use_collection("design_progress").aggregate([
|
||||
{'$match':{'complete_at':{"$exists":0},'parent_id':{"$exists":0}}},
|
||||
{"$lookup": {
|
||||
"from": TaskEnv.CLN_PROJECT,
|
||||
"localField": "project_id",
|
||||
"foreignField": "_id",
|
||||
"as": "project"
|
||||
}},
|
||||
{"$match": {"project.design.sts": 2}},
|
||||
{"$group":
|
||||
{'_id': {'state':"$state",'type':'$type'},
|
||||
'count': {'$sum': 1},
|
||||
@@ -532,6 +671,45 @@ class ReportRepository():
|
||||
res['audit_program'] = result
|
||||
return res
|
||||
|
||||
# فرآیند طراحی گانت چارت
|
||||
async def design_chart(self, project_id: uuid):
|
||||
list_design_progress = await use_collection('design_progress').find({"project_id": project_id}).sort('type', 1).to_list(None)
|
||||
if not list_design_progress:
|
||||
return {}
|
||||
list_design_progress_ids = [doc["_id"] for doc in list_design_progress]
|
||||
list_design_progress_record = await use_collection('design_progress_record').find({"design_progress_id": {"$in":list_design_progress_ids}}).to_list(None)
|
||||
grouped_records = {}
|
||||
for i in list_design_progress:
|
||||
t = i["type"]
|
||||
st = i.get("sub_type")
|
||||
if st:
|
||||
grouped_records.setdefault(t, {})[st] = {
|
||||
"progress": i,
|
||||
"records": []
|
||||
}
|
||||
else:
|
||||
grouped_records.setdefault(t, {
|
||||
"progress": i,
|
||||
"records": []
|
||||
})
|
||||
|
||||
print(grouped_records)
|
||||
for item in list_design_progress:
|
||||
for record in list_design_progress_record:
|
||||
if record["design_progress_id"] == item['_id']:
|
||||
if 'sub_type' not in item:
|
||||
grouped_records[item['type']]["records"].append(record)
|
||||
else:
|
||||
grouped_records[item['type']][item['sub_type']]["records"].append(record)
|
||||
return grouped_records
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,4 +18,5 @@ from .financial_status import *
|
||||
from .financial_adjust import *
|
||||
from .delivery import *
|
||||
from .report import *
|
||||
from .excel import *
|
||||
from .excel import *
|
||||
from .download import *
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
دانلود فایل اکسل تولیدشده از خروجی جدول (Table Engine).
|
||||
فایلها در پوشه temp پروژه ذخیره میشوند و از طریق این endpoint قابل دانلود هستند.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
from config import conf
|
||||
from core.app import app
|
||||
from core.utils.router import generate_router
|
||||
|
||||
router = generate_router("download", auth_dependencies=False, version="v2")
|
||||
_tag_base = ["download"]
|
||||
_pre = conf.server.prefix_url.replace("v1", "v2")
|
||||
|
||||
|
||||
def _excel_temp_dir():
|
||||
return os.path.join(os.getcwd(), "temp")
|
||||
|
||||
|
||||
def _safe_excel_filename(name: str) -> bool:
|
||||
"""فقط نام فایل اکسلی با فرمت uuid.xlsx (۳۲ کاراکتر hex + .xlsx) مجاز است."""
|
||||
return bool(re.match(r"^[a-f0-9]{32}\.xlsx$", name))
|
||||
|
||||
|
||||
@router.get("/excel/{filename}", tags=_tag_base)
|
||||
async def download_excel(filename: str):
|
||||
"""
|
||||
دانلود فایل اکسل با نام بازگرداندهشده از API لیست (فیلد path).
|
||||
مثال: بعد از درخواست خروجی اکسل، در پاسخ path مثلاً «abc123...xlsx» میآید؛
|
||||
برای دانلود: GET /v2/download/excel/abc123....xlsx
|
||||
"""
|
||||
if not _safe_excel_filename(filename):
|
||||
raise HTTPException(status_code=400, detail="نام فایل نامعتبر است.")
|
||||
file_path = os.path.join(_excel_temp_dir(), filename)
|
||||
if not os.path.isfile(file_path):
|
||||
raise HTTPException(status_code=404, detail="فایل یافت نشد یا منقضی شده است.")
|
||||
return FileResponse(
|
||||
file_path,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
+8
-3
@@ -18,9 +18,9 @@ def design():
|
||||
def monitor():
|
||||
pass
|
||||
|
||||
@router.get('/progress1', tags=_tag_base)
|
||||
@shrink(repo.addProgress1)
|
||||
def addProgress1():
|
||||
@router.get('/progress/madarek', tags=_tag_base)
|
||||
@shrink(repo.madarek)
|
||||
def madarek():
|
||||
pass
|
||||
|
||||
@router.get('/progress/physical', tags=_tag_base)
|
||||
@@ -89,6 +89,11 @@ def addProgressTender():
|
||||
def changeStatus():
|
||||
pass
|
||||
|
||||
@router.get('/change/design/metri/sts', tags=_tag_base)
|
||||
@shrink(repo.changeStatusWithMetri)
|
||||
def changeStatusWithMetri():
|
||||
pass
|
||||
|
||||
@router.get('/progress/monitor/physical', tags=_tag_base)
|
||||
@shrink(repo.progressMonitorPhysical)
|
||||
def progressMonitor():
|
||||
|
||||
@@ -25,6 +25,11 @@ def create(coding: ThisModel = Body(...)):
|
||||
def last_record(last:LastRecord):
|
||||
pass
|
||||
|
||||
# @router.get('/progress/rediv', response_model=list[dict], tags=_tag_base, description='پیشرفت فیزیکی')
|
||||
# @shrink(repo.recalc_medium_progress)
|
||||
# def recalc_medium_progress():
|
||||
# pass
|
||||
|
||||
@router.get('/list/{_id}', response_model=List[ThisModel], tags=_tag_base, description='پیشرفت فیزیکی')
|
||||
@shrink(repo.list_by_project)
|
||||
def list_by_project(_id: UUID):
|
||||
|
||||
+10
-1
@@ -3,7 +3,7 @@ from uuid import UUID
|
||||
|
||||
from fastapi import Body
|
||||
|
||||
from app.models.project import ProjectModel, UpdateSts
|
||||
from app.models.project import ProjectModel, UpdateSts, ListWithIds
|
||||
from app.repositories.project import ProjectRepository
|
||||
from core.app import app
|
||||
from core.utils.router import generate_router, shrink
|
||||
@@ -32,6 +32,15 @@ def list_project_design():
|
||||
def list_project_monitor():
|
||||
pass
|
||||
|
||||
@router.post('/list/ids', response_model=ThisModel, tags=_tag_base)
|
||||
@shrink(repo.list_project_with_ids)
|
||||
def list_project_with_ids(inp:ListWithIds):
|
||||
pass
|
||||
|
||||
@router.get('/list/all', response_model=List[ThisModel], tags=_tag_base)
|
||||
@shrink(repo.list_project_with_select)
|
||||
def list_project_with_select():
|
||||
pass
|
||||
@router.get('/list/project', response_model=List[ThisModel], tags=_tag_base)
|
||||
@shrink(repo.list_project)
|
||||
def list_project():
|
||||
|
||||
+20
-4
@@ -2,7 +2,7 @@ import uuid
|
||||
|
||||
from fastapi import Body
|
||||
|
||||
from app.models.report import StatusReport, ReportCountry, DonutList, DelayDonutDetail, PhysicalMonth
|
||||
from app.models.report import StatusReport, ReportCountry, DonutList, DonutReport, DelayDonutDetail, PhysicalMonth
|
||||
from app.repositories.report import ReportRepository
|
||||
from core.app import app
|
||||
from core.utils.router import generate_router, shrink
|
||||
@@ -62,15 +62,25 @@ def sum_add_time_end_project():
|
||||
@shrink(repo.delay_donut_all)
|
||||
def delay_donut_all():
|
||||
pass
|
||||
|
||||
@router.post('/donut-v2/delay/all', tags=_tag_base)
|
||||
@shrink(repo.delay_donut_all_v2)
|
||||
def delay_donut_all_v2(_inp: DonutReport = Body(...)):
|
||||
pass
|
||||
#لیست پروژه ها را نمایش میده
|
||||
@router.get('/donut/delay/{_select}', tags=_tag_base)
|
||||
@shrink(repo.list_delay_donut)
|
||||
def list_delay_donut(_select):
|
||||
pass
|
||||
#تفکیک پروژه ها بر حسب زیربنا و تفکیک ماهیت پروژه و تفکیک بر حسب نوع پروژه
|
||||
@router.get('/donut/{_select}', tags=_tag_base)
|
||||
@router.post('/donut/{_select}', tags=_tag_base)
|
||||
@shrink(repo.donut)
|
||||
def donut(_select:int):
|
||||
def donut(_select: int, _inp: DonutReport = Body(...)):
|
||||
pass
|
||||
|
||||
@router.post('/donut-v2/{_select}', tags=_tag_base)
|
||||
@shrink(repo.donut_v2)
|
||||
def donut_v2(_select: int, _inp: DonutReport = Body(...)):
|
||||
pass
|
||||
|
||||
@router.post('/list-donut', tags=_tag_base)
|
||||
@@ -139,4 +149,10 @@ def status_agreement_progress():
|
||||
def get_extra_project(project_id: uuid.UUID):
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
# فرآیند طراحی گانت چارت
|
||||
@router.get('/design/chart/{project_id}', tags=_tag_base)
|
||||
@shrink(repo.design_chart)
|
||||
def design_chart(project_id: uuid.UUID):
|
||||
pass
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ from core.service.mongo_db import use_collection
|
||||
|
||||
|
||||
def get_auth_db():
|
||||
return conf.server.is_prod and conf.database.get('mongo').production.db or conf.database.get(
|
||||
'mongo').dev.db
|
||||
return conf.server.is_prod and conf.database.get('mongo_auth').production.db or conf.database.get(
|
||||
'mongo_auth').dev.db
|
||||
|
||||
|
||||
def use_auth_collection(collection: str) -> Collection:
|
||||
|
||||
+18
-3
@@ -4,7 +4,7 @@ auth:
|
||||
algorithm: 'HS256'
|
||||
access_token_expire_minutes: 43201
|
||||
refresh_token_expire_minutes: 43200
|
||||
url: 'https://auth.hacoupian.net/v2/auth/validate-token'
|
||||
url: 'http://pm.khanesaziceco.ir/api/api/me'
|
||||
cache_prefix_key: 'auth_key_'
|
||||
cache_ttl: 360
|
||||
|
||||
@@ -68,11 +68,11 @@ database:
|
||||
db: 'omde'
|
||||
|
||||
limiter_redis:
|
||||
url: 'redis://192.168.15.76:6379'
|
||||
url: 'redis://172.16.1.194:6379'
|
||||
db: ''
|
||||
|
||||
auth_redis:
|
||||
url: 'redis://192.168.15.231:6379'
|
||||
url: 'redis://172.16.1.194:6379'
|
||||
db: '1'
|
||||
|
||||
neo4j:
|
||||
@@ -81,3 +81,18 @@ database:
|
||||
version: 4
|
||||
password: 'sample'
|
||||
user: 'local'
|
||||
|
||||
# MySQL (read-only) - جدا از database تا مونگو و بقیه بدون تغییر بمانند
|
||||
mysql:
|
||||
dev:
|
||||
host: '172.16.1.194'
|
||||
port: 3309
|
||||
user: 'root'
|
||||
password: '2TjwtsnYAs'
|
||||
db: 'doc'
|
||||
production:
|
||||
host: '172.16.1.194'
|
||||
port: 3309
|
||||
user: 'root'
|
||||
password: '2TjwtsnYAs'
|
||||
db: 'doc'
|
||||
|
||||
+38
-4
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, List, Union, Dict, Any
|
||||
|
||||
import pydantic
|
||||
import yaml
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
||||
def build_path(path: Union[Path, str], path_is_absolute: bool = False) -> Path:
|
||||
@@ -73,6 +73,19 @@ class DatabaseModeConfig(BaseModel):
|
||||
production: Optional[DatabaseConfig]
|
||||
|
||||
|
||||
class MySQLConfig(BaseModel):
|
||||
host: str
|
||||
port: int
|
||||
user: str
|
||||
password: str
|
||||
db: str
|
||||
|
||||
|
||||
class MySQLModeConfig(BaseModel):
|
||||
dev: Optional[MySQLConfig]
|
||||
production: Optional[MySQLConfig]
|
||||
|
||||
|
||||
class DependencyConfig(BaseModel):
|
||||
middleware: Optional[List[str]]
|
||||
routes: Optional[List[str]]
|
||||
@@ -115,12 +128,33 @@ class CoreConfig(pydantic.BaseModel):
|
||||
server: Optional[ServerConfig]
|
||||
auth: Optional[AuthConfig]
|
||||
dependency: Optional[DependencyConfig]
|
||||
database: dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
|
||||
database: Dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
|
||||
mysql: Optional[MySQLModeConfig] = None
|
||||
static_files: dict[str, str] = dict()
|
||||
celery: Optional[CeleryTask]
|
||||
|
||||
@validator('database', pre=True)
|
||||
def parse_database(cls, v: Any) -> Dict[str, Union[DatabaseConfig, DatabaseModeConfig]]:
|
||||
if not isinstance(v, dict):
|
||||
return v
|
||||
result: Dict[str, Union[DatabaseConfig, DatabaseModeConfig]] = {}
|
||||
for key, val in v.items():
|
||||
if not isinstance(val, dict):
|
||||
result[key] = val
|
||||
continue
|
||||
if 'dev' in val or 'production' in val:
|
||||
dev = val.get('dev')
|
||||
prod = val.get('production')
|
||||
result[key] = DatabaseModeConfig(
|
||||
dev=DatabaseConfig(**(dev or {})) if dev else None,
|
||||
production=DatabaseConfig(**(prod or {})) if prod else None,
|
||||
)
|
||||
else:
|
||||
result[key] = DatabaseConfig(**val)
|
||||
return result
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = False
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
# def load_modules_config(self):
|
||||
# # import dynamic routes in the modules
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
اتصال MySQL فقط-خواندنی (read-only).
|
||||
از پول کانکشن استفاده میکند و در هر کانکشن SESSION را به حالت فقط-خواندنی تنظیم میکند.
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
import aiomysql
|
||||
|
||||
from core.utils.mysql import get_mysql_config
|
||||
|
||||
_pool: Optional[aiomysql.Pool] = None
|
||||
_pool_connection_name: Optional[str] = None
|
||||
|
||||
|
||||
async def get_mysql_pool(connection_name: str = None) -> aiomysql.Pool:
|
||||
"""پول کانکشن MySQL را برمیگرداند؛ در اولین فراخوانی ساخته میشود."""
|
||||
global _pool, _pool_connection_name
|
||||
name = connection_name or "mysql"
|
||||
if _pool is None or _pool_connection_name != name:
|
||||
if _pool is not None:
|
||||
_pool.close()
|
||||
await _pool.wait_closed()
|
||||
cfg = get_mysql_config(name)
|
||||
_pool = await aiomysql.create_pool(
|
||||
host=cfg["host"],
|
||||
port=cfg["port"],
|
||||
user=cfg["user"],
|
||||
password=cfg["password"],
|
||||
db=cfg["db"],
|
||||
charset="utf8mb4",
|
||||
autocommit=True,
|
||||
minsize=1,
|
||||
maxsize=10,
|
||||
)
|
||||
_pool_connection_name = name
|
||||
return _pool
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_mysql_connection(connection_name: str = None):
|
||||
"""
|
||||
کانکشن فقط-خواندنی از پول میگیرد.
|
||||
در ابتدا SESSION را به حالت فقط-خواندنی تنظیم میکند (برای اطمینان بیشتر).
|
||||
استفاده:
|
||||
async with get_mysql_connection() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cur:
|
||||
await cur.execute("SELECT * FROM t LIMIT 10")
|
||||
rows = await cur.fetchall()
|
||||
"""
|
||||
pool = await get_mysql_pool(connection_name)
|
||||
conn = await pool.acquire()
|
||||
try:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute("SET SESSION TRANSACTION READ ONLY")
|
||||
yield conn
|
||||
finally:
|
||||
pool.release(conn)
|
||||
|
||||
|
||||
async def close_mysql_pool():
|
||||
"""بستن پول (مثلاً در shutdown اپ)."""
|
||||
global _pool
|
||||
if _pool is not None:
|
||||
_pool.close()
|
||||
await _pool.wait_closed()
|
||||
_pool = None
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
SQLAlchemy async setup برای MySQL (فقط خواندنی).
|
||||
از asyncmy به عنوان async driver استفاده میکند.
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
|
||||
from core.utils.mysql import get_mysql_config
|
||||
|
||||
# Base class برای تمام مدلهای ORM
|
||||
Base = declarative_base()
|
||||
|
||||
_engine = None
|
||||
_async_session_maker = None
|
||||
|
||||
|
||||
def get_mysql_url(connection_name: str = None) -> str:
|
||||
"""ساخت URL برای SQLAlchemy از تنظیمات MySQL"""
|
||||
cfg = get_mysql_config(connection_name)
|
||||
return f"mysql+asyncmy://{cfg['user']}:{cfg['password']}@{cfg['host']}:{cfg['port']}/{cfg['db']}?charset=utf8mb4"
|
||||
|
||||
|
||||
def get_async_engine(connection_name: str = None):
|
||||
"""برگرداندن async engine (در اولین فراخوانی ساخته میشود)"""
|
||||
global _engine
|
||||
if _engine is None:
|
||||
url = get_mysql_url(connection_name)
|
||||
_engine = create_async_engine(
|
||||
url,
|
||||
echo=False, # برای debug میتونی True کنی
|
||||
pool_pre_ping=True, # چک کردن کانکشن قبل از استفاده
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
)
|
||||
return _engine
|
||||
|
||||
|
||||
def get_async_session_maker(connection_name: str = None):
|
||||
"""برگرداندن async sessionmaker"""
|
||||
global _async_session_maker
|
||||
if _async_session_maker is None:
|
||||
engine = get_async_engine(connection_name)
|
||||
_async_session_maker = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
return _async_session_maker
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_mysql_session(connection_name: str = None):
|
||||
"""
|
||||
Context manager برای گرفتن async session (فقط خواندنی).
|
||||
استفاده:
|
||||
async with get_mysql_session() as session:
|
||||
result = await session.execute(select(User).where(User.id == 1))
|
||||
user = result.scalar_one_or_none()
|
||||
"""
|
||||
session_maker = get_async_session_maker(connection_name)
|
||||
async with session_maker() as session:
|
||||
# تنظیم session به حالت فقط-خواندنی
|
||||
await session.execute("SET SESSION TRANSACTION READ ONLY")
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.rollback() # برای read-only همیشه rollback میکنیم
|
||||
|
||||
|
||||
async def close_mysql_engine():
|
||||
"""بستن engine (مثلاً در shutdown اپ)."""
|
||||
global _engine, _async_session_maker
|
||||
if _engine is not None:
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_async_session_maker = None
|
||||
@@ -23,9 +23,8 @@ async def validate_token(_input: ValidateTokenInput):
|
||||
_cache_auth = await redis_get(f'{conf.auth.cache_prefix_key}{_cache_key}', conf.database.get('auth_redis'))
|
||||
if not _cache_auth:
|
||||
client = httpx.AsyncClient()
|
||||
headers = {'Content-Type': 'application/json', }
|
||||
|
||||
result = await client.request('POST', conf.auth.url, json=_input.dict(), headers=headers)
|
||||
headers = {'Content-Type': 'application/json', 'Authorization':'Bearer '+_input.token}
|
||||
result = await client.request('GET', conf.auth.url, json=_input.dict(), headers=headers)
|
||||
export = result.text
|
||||
if 'application/json' in result.headers['content-type']:
|
||||
export = result.json()
|
||||
@@ -35,6 +34,7 @@ async def validate_token(_input: ValidateTokenInput):
|
||||
|
||||
if 'v2' in conf.auth.url:
|
||||
export = export.get('data')
|
||||
print(export)
|
||||
|
||||
await redis_set(f'{conf.auth.cache_prefix_key}{_cache_key}', export, conf.database.get('auth_redis'),
|
||||
ex=conf.auth.cache_ttl)
|
||||
|
||||
@@ -8,12 +8,12 @@ from core.models.base import BaseModel
|
||||
|
||||
|
||||
class UserModel(BaseModel):
|
||||
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
|
||||
id: Optional[Union[uuid.UUID, str, int]] = Field(alias='_id')
|
||||
username: Optional[str]
|
||||
mobile: Optional[str]
|
||||
name: Optional[str]
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
fname: Optional[str]
|
||||
lname: Optional[str]
|
||||
is_su: Optional[bool]
|
||||
unit: Optional[str]
|
||||
avatar: Optional[str]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
تنظیمات اتصال MySQL (فقط خواندنی).
|
||||
از بخش جداگانه mysql در config.yml (همسطح با database) استفاده میکند.
|
||||
"""
|
||||
from config import conf
|
||||
|
||||
|
||||
def get_mysql_config(connection_name: str = None):
|
||||
"""
|
||||
بر اساس محیط (dev/production) تنظیمات MySQL را برمیگرداند.
|
||||
Returns: dict با کلیدهای host, port, user, password, db
|
||||
"""
|
||||
if not conf.mysql:
|
||||
raise ValueError("تنظیمات mysql در config.yml یافت نشد.")
|
||||
env = "production" if conf.server.is_prod else "dev"
|
||||
mode = getattr(conf.mysql, env, None) or getattr(conf.mysql, "dev", None)
|
||||
if not mode:
|
||||
raise ValueError(f"تنظیمات mysql.{env} در config.yml یافت نشد.")
|
||||
return {
|
||||
"host": getattr(mode, "host", "127.0.0.1"),
|
||||
"port": int(getattr(mode, "port", 3306)),
|
||||
"user": getattr(mode, "user", ""),
|
||||
"password": getattr(mode, "password", ""),
|
||||
"db": getattr(mode, "db", ""),
|
||||
}
|
||||
@@ -10,6 +10,7 @@ class TableEngineInputType(GenericModel, Generic[T]):
|
||||
o: Optional[dict] = {}
|
||||
p: Optional[dict] = {}
|
||||
s: Optional[dict] = {}
|
||||
e: Optional[dict] = {}
|
||||
|
||||
|
||||
class TableEngineExport(GenericModel, Generic[T]):
|
||||
|
||||
@@ -10,7 +10,8 @@ from .utils.Models import TableEngineExport
|
||||
|
||||
class NewTableEngine:
|
||||
def __init__(self, query_class, model, inputs, mode, engine: AbstractEngine, extra_filter,
|
||||
default_sort_by, joins, search_cols, options=None, soft_deleted=False, unselect={}, return_query=False):
|
||||
default_sort_by, joins, search_cols, options=None, soft_deleted=False, unselect={}, return_query=False,
|
||||
collation={'locale': 'fa'}):
|
||||
self.model = model
|
||||
self.query_class = query_class
|
||||
self.mode = mode
|
||||
@@ -29,6 +30,7 @@ class NewTableEngine:
|
||||
self.search_cols = search_cols
|
||||
self.unselect = unselect
|
||||
self.return_query = return_query
|
||||
self.collation = collation
|
||||
self.engine = engine(self)
|
||||
|
||||
@staticmethod
|
||||
@@ -38,10 +40,12 @@ class NewTableEngine:
|
||||
mode: WorkingMode = WorkingMode.Development,
|
||||
extra_filter=None,
|
||||
soft_deleted=False,
|
||||
default_sort_by=None, joins=None, search_cols=[] ,unselect={}, return_query=False):
|
||||
default_sort_by=None, joins=None, search_cols=[] ,unselect={}, return_query=False,
|
||||
collation={'locale': 'fa'}):
|
||||
return NewTableEngine(query_class, model, inputs, mode, engine=MongoEngine, extra_filter=extra_filter,
|
||||
default_sort_by=default_sort_by, joins=joins, search_cols=search_cols,
|
||||
soft_deleted=soft_deleted,unselect=unselect, return_query=return_query)
|
||||
soft_deleted=soft_deleted,unselect=unselect, return_query=return_query,
|
||||
collation=collation)
|
||||
|
||||
@staticmethod
|
||||
def mssql(query_class, model, inputs, mode: WorkingMode = WorkingMode.Development, extra_filter=None,
|
||||
@@ -62,6 +66,9 @@ class NewTableEngine:
|
||||
if self.return_query:
|
||||
query = await self.exe()
|
||||
return query
|
||||
if self.input.e and self.input.e.ot:
|
||||
query = await self.exe()
|
||||
return query
|
||||
rows, count = await self.exe()
|
||||
self.input.extra_filter = None
|
||||
del self.engine
|
||||
|
||||
@@ -17,6 +17,7 @@ class AbstractEngineClass(abc.ABC):
|
||||
self.soft_deleted = table_engine.soft_deleted
|
||||
self.unselect = table_engine.unselect
|
||||
self.return_query = table_engine.return_query
|
||||
self.collation = {'locale': 'fa'}
|
||||
|
||||
@abc.abstractmethod
|
||||
def exe(self):
|
||||
|
||||
+190
-16
@@ -2,6 +2,13 @@ import datetime
|
||||
import uuid
|
||||
from typing import Union, List
|
||||
from uuid import UUID
|
||||
import os
|
||||
import xlsxwriter
|
||||
import tempfile
|
||||
import string
|
||||
import copy
|
||||
import jdatetime
|
||||
|
||||
|
||||
# import dateutil.parser
|
||||
from celery import shared_task
|
||||
@@ -9,6 +16,8 @@ from celery import shared_task
|
||||
|
||||
from .AbstractEngine import AbstractEngineClass
|
||||
from ..utils.Models import FilterItem, FilterScope, OutPutType
|
||||
from ..utils.excel_value_maps import EXCEL_VALUE_MAP_REGISTRY
|
||||
from app.repositories.mysql_const_repo import MySQLConstRepository
|
||||
|
||||
|
||||
# def zero_time_data(date, is_max=False):
|
||||
@@ -31,25 +40,30 @@ class MongoEngine(AbstractEngineClass):
|
||||
rows = []
|
||||
stack = []
|
||||
filters = {}
|
||||
_const_cache: dict = None # کش const ها از MySQL
|
||||
|
||||
async def exe(self):
|
||||
_f = self.filters = self.set_filters()
|
||||
print("_f ====>", _f)
|
||||
if self.unselect:
|
||||
tmp = self.query_class.find(_f, self.unselect)
|
||||
else:
|
||||
tmp = self.query_class.find(_f)
|
||||
|
||||
sort_by = self.set_sort()
|
||||
if isinstance(sort_by, dict):
|
||||
tmp = tmp.sort(**sort_by)
|
||||
else:
|
||||
tmp = tmp.sort(sort_by)
|
||||
|
||||
self.rows = tmp.limit(
|
||||
self.input.get_limit()).skip(self.input.skip())
|
||||
is_natural = isinstance(sort_by, dict) and sort_by.get('key_or_list') == '$natural'
|
||||
if getattr(self, 'collation', None) and not is_natural:
|
||||
tmp = tmp.collation(self.collation)
|
||||
|
||||
if self.input.e and self.input.e.ot:
|
||||
self.input.e.d = self.generate_output(self.query_class, _f)
|
||||
res = await tmp.to_list(None)
|
||||
self.input.e.d = await self.generate_output(res)
|
||||
return {'query': _f, 'sort': self.set_sort(), 'path': self.input.e.d}
|
||||
|
||||
self.rows = tmp.limit(self.input.get_limit()).skip(self.input.skip())
|
||||
if self.return_query:
|
||||
return {'query':_f, 'sort':self.set_sort(), 'limit':self.input.get_limit(), 'page':self.input.skip()}
|
||||
return [self.model(**document) async for document in self.rows], await self.count()
|
||||
@@ -249,15 +263,11 @@ class MongoEngine(AbstractEngineClass):
|
||||
def set_joins(self):
|
||||
pass
|
||||
|
||||
def generate_output(self, query, _f):
|
||||
print(_f.__dict__)
|
||||
return ''
|
||||
async def generate_output(self, _f):
|
||||
print(self.input.e.ot)
|
||||
if self.input.e.ot == OutPutType.EXEL:
|
||||
file_name = str(uuid.uuid4().hex) + '.xlsx'
|
||||
return _f.dict()
|
||||
MongoEngine.output_generator(str(query).replace('false', '0'), params,
|
||||
str(self.query_class.bind.url), file_name=file_name, cols=self.input.e.c)
|
||||
return file_name
|
||||
return await self.output_generator(_f, file_name=file_name, cols=self.input.e.c)
|
||||
elif self.input.e.ot == OutPutType.JSON:
|
||||
pass
|
||||
elif self.input.e.ot == OutPutType.PDF:
|
||||
@@ -265,7 +275,171 @@ class MongoEngine(AbstractEngineClass):
|
||||
else:
|
||||
raise Exception("Output type not supported.")
|
||||
|
||||
@staticmethod
|
||||
@shared_task(name='output_generator_task_mongo')
|
||||
def output_generator(query, params, url, file_name, cols):
|
||||
pass
|
||||
def _excel_temp_dir(self):
|
||||
"""پوشه temp پروژه برای ذخیره فایلهای اکسل (همان مسیری که برای دانلود سرو میشود)."""
|
||||
temp_dir = os.path.join(os.getcwd(), 'temp')
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
return temp_dir
|
||||
|
||||
def _expand_headers_with_id_fields(self, cols: dict) -> list:
|
||||
"""
|
||||
اگر design.nature در فیلدها باشد، design.nature_id هم اضافه میشود.
|
||||
اگر design.budget در فیلدها باشد، design.budget_id هم اضافه میشود.
|
||||
"""
|
||||
headers = list(cols.keys())
|
||||
expanded = []
|
||||
pair_rules = [
|
||||
('design.nature', 'design.nature_id', 'نام ماهیت'),
|
||||
('design.budget', 'design.budget_id', 'نام دسته بودجه'),
|
||||
]
|
||||
for h in headers:
|
||||
expanded.append(h)
|
||||
for field, id_field, default_title in pair_rules:
|
||||
if h == field and id_field not in headers and id_field not in expanded:
|
||||
expanded.append(id_field)
|
||||
cols[id_field] = cols.get(field, default_title) + ' (نام)'
|
||||
break
|
||||
return expanded
|
||||
|
||||
async def output_generator(self, query, file_name, cols):
|
||||
datas = query
|
||||
file_dir = os.path.join(self._excel_temp_dir(), file_name)
|
||||
head_cols = list(str(string.ascii_uppercase))
|
||||
headers = self._expand_headers_with_id_fields(cols)
|
||||
|
||||
workbook = xlsxwriter.Workbook(file_dir)
|
||||
worksheet = workbook.add_worksheet()
|
||||
|
||||
# نوشتن هدرها
|
||||
bold_format = workbook.add_format({'bold': True})
|
||||
|
||||
# نوشتن هدرها
|
||||
for col_index, header in enumerate(headers):
|
||||
worksheet.write(0, col_index, cols[header], bold_format)
|
||||
|
||||
# ساختن value_maps از vk (نام مپ) با استفاده از رجیستری
|
||||
value_maps = self._build_value_maps()
|
||||
|
||||
# بارگذاری const ها برای فیلدهای خاص
|
||||
const_maps = await self._load_const_maps(headers)
|
||||
|
||||
for row_index, item in enumerate(datas, start=1):
|
||||
for col_index, col in enumerate(headers):
|
||||
val = self.get_nested_value(item, col)
|
||||
val = self._apply_value_map(col, val, value_maps)
|
||||
val = await self._apply_const_map(col, val, const_maps)
|
||||
worksheet.write(row_index, col_index, val)
|
||||
workbook.close()
|
||||
return file_name
|
||||
|
||||
def _build_value_maps(self):
|
||||
"""اگر کاربر در e.vk برای ستونی کلید مپ داده (مثلاً design_sts)، مپ از رجیستری برمیگردد."""
|
||||
value_maps = {}
|
||||
vk = getattr(self.input.e, "vk", None) or {}
|
||||
if not isinstance(vk, dict):
|
||||
return value_maps
|
||||
for col_key, map_name in vk.items():
|
||||
if map_name and map_name in EXCEL_VALUE_MAP_REGISTRY:
|
||||
value_maps[col_key] = EXCEL_VALUE_MAP_REGISTRY[map_name]
|
||||
return value_maps
|
||||
|
||||
def _apply_value_map(self, col_key: str, val, value_maps: dict):
|
||||
"""اگر برای این ستون مپ تعریف شده، مقدار را به متن متناظر تبدیل میکند."""
|
||||
if not value_maps or col_key not in value_maps:
|
||||
return val
|
||||
mapping = value_maps[col_key]
|
||||
if not isinstance(mapping, dict):
|
||||
return val
|
||||
if val is None:
|
||||
return mapping.get(None, val)
|
||||
key = val if isinstance(val, (int, float)) else (int(val) if isinstance(val, str) and str(val).strip().isdigit() else val)
|
||||
if key in mapping:
|
||||
return mapping[key]
|
||||
if isinstance(key, int) and str(key) in mapping:
|
||||
return mapping[str(key)]
|
||||
return val
|
||||
|
||||
async def _load_const_maps(self, headers: List[str]) -> dict:
|
||||
"""
|
||||
شناسایی فیلدهای خاص (مثلاً nature_id, type_id, budget_id) و بارگذاری const ها از MySQL.
|
||||
Returns: dict به صورت {col_key: {id: title_fa}} مثلاً {"design.nature_id": {2: "طبیعت 2", ...}}
|
||||
"""
|
||||
# لیست فیلدهای خاص که باید از const خوانده شوند
|
||||
const_field_names = ['nature_id', 'type_id', 'budget_id'] # میتونی اضافه کنی
|
||||
|
||||
const_maps = {}
|
||||
const_keys_to_load = set()
|
||||
const_column_map = {} # {col_key: const_key} مثلاً {"design.nature_id": "nature"}
|
||||
|
||||
# شناسایی ستونهایی که باید const بخونن
|
||||
for col_key in headers:
|
||||
col_name = col_key.split('.')[-1] # آخرین بخش (nature_id)
|
||||
if col_name in const_field_names:
|
||||
const_key = col_name.replace('_id', '') # nature_id -> nature
|
||||
const_keys_to_load.add(const_key)
|
||||
const_column_map[col_key] = const_key
|
||||
|
||||
# بارگذاری const ها از MySQL
|
||||
if const_keys_to_load:
|
||||
repo = MySQLConstRepository()
|
||||
for col_key, const_key in const_column_map.items():
|
||||
consts_for_key = await repo.get_by_key(const_key)
|
||||
if consts_for_key:
|
||||
const_maps[col_key] = consts_for_key
|
||||
|
||||
return const_maps
|
||||
|
||||
async def _apply_const_map(self, col_key: str, val, const_maps: dict):
|
||||
"""اگر این ستون در const_maps باشد، id را به title_fa تبدیل میکند."""
|
||||
if not const_maps or col_key not in const_maps:
|
||||
return val
|
||||
|
||||
mapping = const_maps[col_key]
|
||||
if not isinstance(mapping, dict):
|
||||
return val
|
||||
|
||||
if val is None:
|
||||
return mapping.get(None, val)
|
||||
|
||||
# تبدیل val به int برای جستجو در mapping
|
||||
key = val if isinstance(val, (int, float)) else (int(val) if isinstance(val, str) and str(val).strip().isdigit() else val)
|
||||
|
||||
if key in mapping:
|
||||
return mapping[key]
|
||||
if isinstance(key, int) and str(key) in mapping:
|
||||
return mapping[str(key)]
|
||||
|
||||
return val
|
||||
|
||||
def get_nested_value(self, data, path, default='-'):
|
||||
"""
|
||||
دریافت مقدار از دیکشنری تو در تو با مسیر dot notation
|
||||
مثال: path = "design.title"
|
||||
"""
|
||||
if not isinstance(path, str):
|
||||
return default
|
||||
|
||||
keys = path.split('.')
|
||||
val = data
|
||||
|
||||
for key in keys:
|
||||
if isinstance(val, dict):
|
||||
val = val.get(key, default)
|
||||
else:
|
||||
return default
|
||||
|
||||
if val is None:
|
||||
return default
|
||||
|
||||
# تبدیل datetime و UUID به رشته برای اکسل
|
||||
if isinstance(val, datetime.datetime):
|
||||
return self.create_date(val)
|
||||
if isinstance(val, uuid.UUID):
|
||||
return str(val)
|
||||
|
||||
return val
|
||||
|
||||
def create_date(self, date):
|
||||
shamsi_dt = jdatetime.datetime.fromgregorian(datetime=date)
|
||||
return shamsi_dt.strftime("%Y/%m/%d - %H:%M:%S")
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class Cr(str, Enum):
|
||||
|
||||
class OutPutType(str, Enum):
|
||||
JSON = 'json'
|
||||
EXEL = 'exel'
|
||||
EXEL = 'excel'
|
||||
PDF = 'pdf'
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ class ExportModel(BaseModel):
|
||||
ot: Optional[OutPutType]
|
||||
c: Optional[dict]
|
||||
d: Optional[str]
|
||||
vk: Optional[dict] = None
|
||||
|
||||
|
||||
class SortModel(BaseModel):
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
رجیستری مپهای ازپیشتعریفشده برای خروجی اکسل.
|
||||
کلاینت فقط کلید (مثلاً design_sts) را در e.vk میفرستد تا بهجای عدد، متن در اکسل بیاید.
|
||||
"""
|
||||
|
||||
EXCEL_VALUE_MAP_REGISTRY = {
|
||||
"design_sts": {
|
||||
1: "شروع نشده",
|
||||
2: "جاری",
|
||||
3: "متوقف",
|
||||
4: "خاتمه یافته",
|
||||
},
|
||||
"monitor_sts": {
|
||||
1: "شروع نشده",
|
||||
2: "جاری",
|
||||
3: "متوقف",
|
||||
4: "تحویل موقت",
|
||||
5: "خاتمه یافته",
|
||||
},
|
||||
|
||||
"sts": {
|
||||
1: "جاری",
|
||||
2: "حذف شده",
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
aiofiles==22.1.0
|
||||
aiomysql==0.2.0
|
||||
aioredis==2.0.1
|
||||
asyncmy==0.2.9
|
||||
amqp==5.1.1
|
||||
annotated-types==0.6.0
|
||||
anyio==3.6.2
|
||||
|
||||
Reference in New Issue
Block a user