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):
|
class Record(BaseModel):
|
||||||
id: Optional[uuid.UUID] = Field(alias='_id')
|
id: Optional[uuid.UUID] = Field(alias='_id')
|
||||||
title: str = Field(description='عنوان در این فیلد قرار میگیرد')
|
title: str = Field(description='عنوان در این فیلد قرار میگیرد')
|
||||||
send: SubRecord = Field(description='اطلاعات ارسال در این فیلد ذخیره می شود')
|
send: Optional[SubRecord] = Field(description='اطلاعات ارسال در این فیلد ذخیره می شود')
|
||||||
receive: Optional[SubRecord] = Field(description='اطلاعات دریافت در این فیلد ذخیره می شود')
|
receive: Optional[SubRecord] = Field(description='اطلاعات دریافت در این فیلد ذخیره می شود')
|
||||||
|
|
||||||
class RecordModel(Record):
|
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="کد شناسایی پروژه در این فیلد قرار می گیرد")
|
project_id: Optional[uuid.UUID] = Field(description="کد شناسایی پروژه در این فیلد قرار می گیرد")
|
||||||
status:str
|
status:str
|
||||||
|
|
||||||
|
class ListWithIds(pydantic.BaseModel):
|
||||||
|
ids: list[uuid.UUID]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,5 @@ class RegisterStatusModel(BaseModel):
|
|||||||
amount: Optional[AmountStatus] = Field(description="مبالغ")
|
amount: Optional[AmountStatus] = Field(description="مبالغ")
|
||||||
related_to_number_start:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
|
related_to_number_start:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
|
||||||
related_to_number_end: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,6 +22,10 @@ class DelayDonutDetail(pydantic.BaseModel):
|
|||||||
project_id:uuid.UUID
|
project_id:uuid.UUID
|
||||||
select:Literal['financial','force_major','technical']
|
select:Literal['financial','force_major','technical']
|
||||||
|
|
||||||
|
class DonutReport(pydantic.BaseModel):
|
||||||
|
only_current: bool = Field(default=False, description='فقط پروژههای نظارت جاری؛ مقدار false برای همه پروژههای نظارت')
|
||||||
|
|
||||||
|
|
||||||
class DonutList(pydantic.BaseModel):
|
class DonutList(pydantic.BaseModel):
|
||||||
type: int = Field(description='مقدار وارد شده در گزارش قبل')
|
type: int = Field(description='مقدار وارد شده در گزارش قبل')
|
||||||
donut_select: str = Field(description='آیتم دونات انتخاب شده')
|
donut_select: str = Field(description='آیتم دونات انتخاب شده')
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class ContractRepository(BaseRepository):
|
|||||||
{'$match':query['query']},
|
{'$match':query['query']},
|
||||||
{'$group': {"_id": None, 'count': {'$sum': 1},'sum':{"$sum":"$amount"}}}
|
{'$group': {"_id": None, 'count': {'$sum': 1},'sum':{"$sum":"$amount"}}}
|
||||||
]).to_list(None)
|
]).to_list(None)
|
||||||
return count[0]
|
print(count)
|
||||||
else:
|
if len(count)>0:
|
||||||
return []
|
return count[0]
|
||||||
|
return []
|
||||||
|
|||||||
+740
-652
File diff suppressed because it is too large
Load Diff
@@ -139,6 +139,34 @@ class PhysicalProgressRepository(BaseRepository):
|
|||||||
else:
|
else:
|
||||||
await use_collection(COLLECTION).update_one({"_id": _i_model.id},{"$set": _update})
|
await use_collection(COLLECTION).update_one({"_id": _i_model.id},{"$set": _update})
|
||||||
return True
|
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 fastapi import HTTPException
|
||||||
|
|
||||||
from app.config import TaskEnv
|
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.repositories.base import BaseRepository
|
||||||
from core.service.mongo_db import use_collection
|
from core.service.mongo_db import use_collection
|
||||||
from core.utils.table_engine import TableEngineRequestType, TableEngineExport
|
from core.utils.table_engine import TableEngineRequestType, TableEngineExport
|
||||||
@@ -66,14 +66,14 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
|||||||
|
|
||||||
async def create(self, coding: ProjectModel):
|
async def create(self, coding: ProjectModel):
|
||||||
coding.counter = 1
|
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:
|
if last_project:
|
||||||
coding.counter = last_project[0]["counter"] + 1
|
coding.counter = last_project[0]["counter"] + 1
|
||||||
code = self.create_code(coding)
|
code = self.create_code(coding)
|
||||||
coding.design.code = 'D' + code
|
coding.design.code = code
|
||||||
coding.init_id().cu_at()
|
coding.init_id().cu_at()
|
||||||
if coding.monitor:
|
if coding.monitor:
|
||||||
coding.monitor.code = 'S' + code
|
coding.monitor.code = code
|
||||||
coding.has_monitor = True
|
coding.has_monitor = True
|
||||||
await self.create_report_price(coding)
|
await self.create_report_price(coding)
|
||||||
if coding.monitor.detail.date_create_project and coding.monitor.detail.first_time:
|
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)
|
old_project = ProjectModel(**old_project_dict)
|
||||||
if update.monitor and not old_project.monitor:
|
if update.monitor and not old_project.monitor:
|
||||||
code = self.create_code(update)
|
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:
|
if update.design.nature != old_project.design.nature or update.design.type != old_project.design.type:
|
||||||
code = self.create_code(update)
|
code = self.create_code(update)
|
||||||
update.design.code = 'D' + code
|
update.design.code = code
|
||||||
if update.monitor:
|
if update.monitor:
|
||||||
update.monitor.code = 'S' + code
|
update.monitor.code = code
|
||||||
if update.monitor:
|
if update.monitor:
|
||||||
update.has_monitor = True
|
update.has_monitor = True
|
||||||
await self.update_report_price(update)
|
await self.update_report_price(update)
|
||||||
@@ -175,7 +175,16 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def list_project_design(self):
|
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
|
return project
|
||||||
|
|
||||||
async def list_project_monitor(self):
|
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)
|
project = await use_collection(TaskEnv.CLN_PROJECT).find({}).to_list(None)
|
||||||
return project
|
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):
|
async def set_sts_two_for_monitor(self, project_id: uuid.UUID):
|
||||||
project = await use_collection(TaskEnv.CLN_PROJECT).find_one({"_id": project_id})
|
project = await use_collection(TaskEnv.CLN_PROJECT).find_one({"_id": project_id})
|
||||||
if not ('sts' in project['monitor'] and project['monitor']['sts'] > 1):
|
if not ('sts' in project['monitor'] and project['monitor']['sts'] > 1):
|
||||||
@@ -224,25 +240,69 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
|||||||
|
|
||||||
return projects
|
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]:
|
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),
|
query_class=use_collection(self.COLLECTION),
|
||||||
soft_deleted=True,
|
soft_deleted=True,
|
||||||
model=self.MODEL,
|
model=self.MODEL,
|
||||||
search_cols=['design.title'],
|
search_cols=['design.title'],
|
||||||
extra_filter=[{'sts': 2}],
|
extra_filter=extra_filter,
|
||||||
inputs=_input.te) \
|
inputs=_input.te) \
|
||||||
.to_dict()
|
.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]:
|
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),
|
query_class=use_collection(self.COLLECTION),
|
||||||
soft_deleted=True,
|
soft_deleted=True,
|
||||||
model=self.MODEL,
|
model=self.MODEL,
|
||||||
search_cols=['design.title'],
|
search_cols=['design.title'],
|
||||||
extra_filter=[{'sts': 1}],
|
extra_filter=extra_filter,
|
||||||
inputs=_input.te) \
|
inputs=_input.te) \
|
||||||
.to_dict()
|
.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):
|
async def get_all(self):
|
||||||
url = 'http://api.khanesaziceco.ir/api/api/project/table'
|
url = 'http://api.khanesaziceco.ir/api/api/project/table'
|
||||||
@@ -261,8 +321,10 @@ class ProjectRepository(BaseRepository[ProjectModel]):
|
|||||||
_set = ProjectModel(
|
_set = ProjectModel(
|
||||||
sts=1,
|
sts=1,
|
||||||
counter=i['id'],
|
counter=i['id'],
|
||||||
design=Design(**i)
|
design=Design(**i),
|
||||||
).init_id().cu_at()
|
created_at=i['created_at'],
|
||||||
|
updated_at=i['updated_at'],
|
||||||
|
).init_id()
|
||||||
obj.append(_set.mongo_dict())
|
obj.append(_set.mongo_dict())
|
||||||
|
|
||||||
await use_collection(TaskEnv.CLN_PROJECT).insert_many(obj)
|
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):
|
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)
|
list_status = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find({"project_id":project_id}).to_list(None)
|
||||||
return list_status
|
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.config import TaskEnv
|
||||||
from app.models.project import ProjectPriceReport
|
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
|
from core.service.mongo_db import use_collection
|
||||||
|
|
||||||
|
|
||||||
class ReportRepository():
|
class ReportRepository():
|
||||||
async def project_status(self, _inp: StatusReport, type):
|
async def project_status(self, _inp: StatusReport, type):
|
||||||
if _inp.state:
|
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:
|
else:
|
||||||
match = {f'{type}.sts': {'$exists': 1}}
|
match = {f'{type}.sts': {'$exists': 1},'sts':1}
|
||||||
if _inp.type_id:
|
if _inp.type_id:
|
||||||
match.update({'design.type_id': _inp.type_id})
|
match.update({'design.type_id': _inp.type_id})
|
||||||
if _inp.nature_id:
|
if _inp.nature_id:
|
||||||
match.update({'design.nature_id': _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:
|
if type == 'monitor' and _inp.monitor_type:
|
||||||
match.update({'monitor.base_information.type': _inp.monitor_type})
|
match.update({'monitor.base_information.type': _inp.monitor_type})
|
||||||
report = await use_collection(TaskEnv.CLN_PROJECT).aggregate([
|
report = await use_collection(TaskEnv.CLN_PROJECT).aggregate([
|
||||||
@@ -140,6 +150,64 @@ class ReportRepository():
|
|||||||
]).to_list(None)
|
]).to_list(None)
|
||||||
return report
|
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):
|
async def list_donut(self, _inp: DonutList):
|
||||||
_filter = None
|
_filter = None
|
||||||
if _inp.type == 1:
|
if _inp.type == 1:
|
||||||
@@ -220,6 +288,13 @@ class ReportRepository():
|
|||||||
async def categorize_deviation(self):
|
async def categorize_deviation(self):
|
||||||
report = await use_collection(TaskEnv.CLN_MONITOR_PYSICAL).aggregate(
|
report = await use_collection(TaskEnv.CLN_MONITOR_PYSICAL).aggregate(
|
||||||
[{"$match": {"deviation": {"$exists": True, "$ne": None}}},
|
[{"$match": {"deviation": {"$exists": True, "$ne": None}}},
|
||||||
|
{"$lookup": {
|
||||||
|
"from": TaskEnv.CLN_PROJECT,
|
||||||
|
"localField": "project_id",
|
||||||
|
"foreignField": "_id",
|
||||||
|
"as": "project"
|
||||||
|
}},
|
||||||
|
{"$match": {"project.monitor.sts": 2}},
|
||||||
{"$group": {
|
{"$group": {
|
||||||
"_id": {
|
"_id": {
|
||||||
"$cond": [
|
"$cond": [
|
||||||
@@ -257,6 +332,28 @@ class ReportRepository():
|
|||||||
]).to_list(None)
|
]).to_list(None)
|
||||||
return report
|
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):
|
async def list_delay_donut(self, _select: str):
|
||||||
get_list = []
|
get_list = []
|
||||||
_map = {'1': 'financial.total', '2': 'force_major.total', '3': 'technical.total', '4': 'extra_new_work',
|
_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)
|
to_date = datetime.combine(_inp.to_date.date(), time.max)
|
||||||
from_date = _inp.from_date.combine(_inp.from_date.date(), time.min)
|
from_date = _inp.from_date.combine(_inp.from_date.date(), time.min)
|
||||||
result = await use_collection(TaskEnv.CLN_MONITOR_PYSICAL).aggregate([
|
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"},
|
"$lookup": {
|
||||||
'avg': {'$avg': "$real"}},
|
"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)
|
]).to_list(None)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def design_progress_status(self):
|
async def design_progress_status(self):
|
||||||
result = await use_collection("design_progress").aggregate([
|
result = await use_collection("design_progress").aggregate([
|
||||||
{'$match':{'complete_at':{"$exists":0},'parent_id':{"$exists":0}}},
|
{'$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":
|
{"$group":
|
||||||
{'_id': '$type',
|
{'_id': '$type',
|
||||||
'count': {'$sum': 1},
|
'count': {'$sum': 1},
|
||||||
@@ -428,6 +560,13 @@ class ReportRepository():
|
|||||||
async def design_progress_status_state(self):
|
async def design_progress_status_state(self):
|
||||||
result = await use_collection("design_progress").aggregate([
|
result = await use_collection("design_progress").aggregate([
|
||||||
{'$match':{'complete_at':{"$exists":0},'parent_id':{"$exists":0}}},
|
{'$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":
|
{"$group":
|
||||||
{'_id': {'state':"$state",'type':'$type'},
|
{'_id': {'state':"$state",'type':'$type'},
|
||||||
'count': {'$sum': 1},
|
'count': {'$sum': 1},
|
||||||
@@ -532,6 +671,45 @@ class ReportRepository():
|
|||||||
res['audit_program'] = result
|
res['audit_program'] = result
|
||||||
return res
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ from .financial_adjust import *
|
|||||||
from .delivery import *
|
from .delivery import *
|
||||||
from .report 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():
|
def monitor():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@router.get('/progress1', tags=_tag_base)
|
@router.get('/progress/madarek', tags=_tag_base)
|
||||||
@shrink(repo.addProgress1)
|
@shrink(repo.madarek)
|
||||||
def addProgress1():
|
def madarek():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@router.get('/progress/physical', tags=_tag_base)
|
@router.get('/progress/physical', tags=_tag_base)
|
||||||
@@ -89,6 +89,11 @@ def addProgressTender():
|
|||||||
def changeStatus():
|
def changeStatus():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@router.get('/change/design/metri/sts', tags=_tag_base)
|
||||||
|
@shrink(repo.changeStatusWithMetri)
|
||||||
|
def changeStatusWithMetri():
|
||||||
|
pass
|
||||||
|
|
||||||
@router.get('/progress/monitor/physical', tags=_tag_base)
|
@router.get('/progress/monitor/physical', tags=_tag_base)
|
||||||
@shrink(repo.progressMonitorPhysical)
|
@shrink(repo.progressMonitorPhysical)
|
||||||
def progressMonitor():
|
def progressMonitor():
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ def create(coding: ThisModel = Body(...)):
|
|||||||
def last_record(last:LastRecord):
|
def last_record(last:LastRecord):
|
||||||
pass
|
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='پیشرفت فیزیکی')
|
@router.get('/list/{_id}', response_model=List[ThisModel], tags=_tag_base, description='پیشرفت فیزیکی')
|
||||||
@shrink(repo.list_by_project)
|
@shrink(repo.list_by_project)
|
||||||
def list_by_project(_id: UUID):
|
def list_by_project(_id: UUID):
|
||||||
|
|||||||
+10
-1
@@ -3,7 +3,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from fastapi import Body
|
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 app.repositories.project import ProjectRepository
|
||||||
from core.app import app
|
from core.app import app
|
||||||
from core.utils.router import generate_router, shrink
|
from core.utils.router import generate_router, shrink
|
||||||
@@ -32,6 +32,15 @@ def list_project_design():
|
|||||||
def list_project_monitor():
|
def list_project_monitor():
|
||||||
pass
|
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)
|
@router.get('/list/project', response_model=List[ThisModel], tags=_tag_base)
|
||||||
@shrink(repo.list_project)
|
@shrink(repo.list_project)
|
||||||
def list_project():
|
def list_project():
|
||||||
|
|||||||
+19
-3
@@ -2,7 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from fastapi import Body
|
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 app.repositories.report import ReportRepository
|
||||||
from core.app import app
|
from core.app import app
|
||||||
from core.utils.router import generate_router, shrink
|
from core.utils.router import generate_router, shrink
|
||||||
@@ -62,15 +62,25 @@ def sum_add_time_end_project():
|
|||||||
@shrink(repo.delay_donut_all)
|
@shrink(repo.delay_donut_all)
|
||||||
def delay_donut_all():
|
def delay_donut_all():
|
||||||
pass
|
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)
|
@router.get('/donut/delay/{_select}', tags=_tag_base)
|
||||||
@shrink(repo.list_delay_donut)
|
@shrink(repo.list_delay_donut)
|
||||||
def list_delay_donut(_select):
|
def list_delay_donut(_select):
|
||||||
pass
|
pass
|
||||||
#تفکیک پروژه ها بر حسب زیربنا و تفکیک ماهیت پروژه و تفکیک بر حسب نوع پروژه
|
#تفکیک پروژه ها بر حسب زیربنا و تفکیک ماهیت پروژه و تفکیک بر حسب نوع پروژه
|
||||||
@router.get('/donut/{_select}', tags=_tag_base)
|
@router.post('/donut/{_select}', tags=_tag_base)
|
||||||
@shrink(repo.donut)
|
@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
|
pass
|
||||||
|
|
||||||
@router.post('/list-donut', tags=_tag_base)
|
@router.post('/list-donut', tags=_tag_base)
|
||||||
@@ -139,4 +149,10 @@ def status_agreement_progress():
|
|||||||
def get_extra_project(project_id: uuid.UUID):
|
def get_extra_project(project_id: uuid.UUID):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# فرآیند طراحی گانت چارت
|
||||||
|
@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)
|
app.include_router(router)
|
||||||
+2
-2
@@ -5,8 +5,8 @@ from core.service.mongo_db import use_collection
|
|||||||
|
|
||||||
|
|
||||||
def get_auth_db():
|
def get_auth_db():
|
||||||
return conf.server.is_prod and conf.database.get('mongo').production.db or conf.database.get(
|
return conf.server.is_prod and conf.database.get('mongo_auth').production.db or conf.database.get(
|
||||||
'mongo').dev.db
|
'mongo_auth').dev.db
|
||||||
|
|
||||||
|
|
||||||
def use_auth_collection(collection: str) -> Collection:
|
def use_auth_collection(collection: str) -> Collection:
|
||||||
|
|||||||
+18
-3
@@ -4,7 +4,7 @@ auth:
|
|||||||
algorithm: 'HS256'
|
algorithm: 'HS256'
|
||||||
access_token_expire_minutes: 43201
|
access_token_expire_minutes: 43201
|
||||||
refresh_token_expire_minutes: 43200
|
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_prefix_key: 'auth_key_'
|
||||||
cache_ttl: 360
|
cache_ttl: 360
|
||||||
|
|
||||||
@@ -68,11 +68,11 @@ database:
|
|||||||
db: 'omde'
|
db: 'omde'
|
||||||
|
|
||||||
limiter_redis:
|
limiter_redis:
|
||||||
url: 'redis://192.168.15.76:6379'
|
url: 'redis://172.16.1.194:6379'
|
||||||
db: ''
|
db: ''
|
||||||
|
|
||||||
auth_redis:
|
auth_redis:
|
||||||
url: 'redis://192.168.15.231:6379'
|
url: 'redis://172.16.1.194:6379'
|
||||||
db: '1'
|
db: '1'
|
||||||
|
|
||||||
neo4j:
|
neo4j:
|
||||||
@@ -81,3 +81,18 @@ database:
|
|||||||
version: 4
|
version: 4
|
||||||
password: 'sample'
|
password: 'sample'
|
||||||
user: 'local'
|
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
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Union
|
from typing import Optional, List, Union, Dict, Any
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
import yaml
|
import yaml
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
|
|
||||||
def build_path(path: Union[Path, str], path_is_absolute: bool = False) -> Path:
|
def build_path(path: Union[Path, str], path_is_absolute: bool = False) -> Path:
|
||||||
@@ -73,6 +73,19 @@ class DatabaseModeConfig(BaseModel):
|
|||||||
production: Optional[DatabaseConfig]
|
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):
|
class DependencyConfig(BaseModel):
|
||||||
middleware: Optional[List[str]]
|
middleware: Optional[List[str]]
|
||||||
routes: Optional[List[str]]
|
routes: Optional[List[str]]
|
||||||
@@ -115,12 +128,33 @@ class CoreConfig(pydantic.BaseModel):
|
|||||||
server: Optional[ServerConfig]
|
server: Optional[ServerConfig]
|
||||||
auth: Optional[AuthConfig]
|
auth: Optional[AuthConfig]
|
||||||
dependency: Optional[DependencyConfig]
|
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()
|
static_files: dict[str, str] = dict()
|
||||||
celery: Optional[CeleryTask]
|
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:
|
class Config:
|
||||||
arbitrary_types_allowed = False
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
# def load_modules_config(self):
|
# def load_modules_config(self):
|
||||||
# # import dynamic routes in the modules
|
# # 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'))
|
_cache_auth = await redis_get(f'{conf.auth.cache_prefix_key}{_cache_key}', conf.database.get('auth_redis'))
|
||||||
if not _cache_auth:
|
if not _cache_auth:
|
||||||
client = httpx.AsyncClient()
|
client = httpx.AsyncClient()
|
||||||
headers = {'Content-Type': 'application/json', }
|
headers = {'Content-Type': 'application/json', 'Authorization':'Bearer '+_input.token}
|
||||||
|
result = await client.request('GET', conf.auth.url, json=_input.dict(), headers=headers)
|
||||||
result = await client.request('POST', conf.auth.url, json=_input.dict(), headers=headers)
|
|
||||||
export = result.text
|
export = result.text
|
||||||
if 'application/json' in result.headers['content-type']:
|
if 'application/json' in result.headers['content-type']:
|
||||||
export = result.json()
|
export = result.json()
|
||||||
@@ -35,6 +34,7 @@ async def validate_token(_input: ValidateTokenInput):
|
|||||||
|
|
||||||
if 'v2' in conf.auth.url:
|
if 'v2' in conf.auth.url:
|
||||||
export = export.get('data')
|
export = export.get('data')
|
||||||
|
print(export)
|
||||||
|
|
||||||
await redis_set(f'{conf.auth.cache_prefix_key}{_cache_key}', export, conf.database.get('auth_redis'),
|
await redis_set(f'{conf.auth.cache_prefix_key}{_cache_key}', export, conf.database.get('auth_redis'),
|
||||||
ex=conf.auth.cache_ttl)
|
ex=conf.auth.cache_ttl)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from core.models.base import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class UserModel(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]
|
username: Optional[str]
|
||||||
mobile: Optional[str]
|
mobile: Optional[str]
|
||||||
name: Optional[str]
|
name: Optional[str]
|
||||||
first_name: Optional[str]
|
fname: Optional[str]
|
||||||
last_name: Optional[str]
|
lname: Optional[str]
|
||||||
is_su: Optional[bool]
|
is_su: Optional[bool]
|
||||||
unit: Optional[str]
|
unit: Optional[str]
|
||||||
avatar: 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] = {}
|
o: Optional[dict] = {}
|
||||||
p: Optional[dict] = {}
|
p: Optional[dict] = {}
|
||||||
s: Optional[dict] = {}
|
s: Optional[dict] = {}
|
||||||
|
e: Optional[dict] = {}
|
||||||
|
|
||||||
|
|
||||||
class TableEngineExport(GenericModel, Generic[T]):
|
class TableEngineExport(GenericModel, Generic[T]):
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ from .utils.Models import TableEngineExport
|
|||||||
|
|
||||||
class NewTableEngine:
|
class NewTableEngine:
|
||||||
def __init__(self, query_class, model, inputs, mode, engine: AbstractEngine, extra_filter,
|
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.model = model
|
||||||
self.query_class = query_class
|
self.query_class = query_class
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
@@ -29,6 +30,7 @@ class NewTableEngine:
|
|||||||
self.search_cols = search_cols
|
self.search_cols = search_cols
|
||||||
self.unselect = unselect
|
self.unselect = unselect
|
||||||
self.return_query = return_query
|
self.return_query = return_query
|
||||||
|
self.collation = collation
|
||||||
self.engine = engine(self)
|
self.engine = engine(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -38,10 +40,12 @@ class NewTableEngine:
|
|||||||
mode: WorkingMode = WorkingMode.Development,
|
mode: WorkingMode = WorkingMode.Development,
|
||||||
extra_filter=None,
|
extra_filter=None,
|
||||||
soft_deleted=False,
|
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,
|
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,
|
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
|
@staticmethod
|
||||||
def mssql(query_class, model, inputs, mode: WorkingMode = WorkingMode.Development, extra_filter=None,
|
def mssql(query_class, model, inputs, mode: WorkingMode = WorkingMode.Development, extra_filter=None,
|
||||||
@@ -62,6 +66,9 @@ class NewTableEngine:
|
|||||||
if self.return_query:
|
if self.return_query:
|
||||||
query = await self.exe()
|
query = await self.exe()
|
||||||
return query
|
return query
|
||||||
|
if self.input.e and self.input.e.ot:
|
||||||
|
query = await self.exe()
|
||||||
|
return query
|
||||||
rows, count = await self.exe()
|
rows, count = await self.exe()
|
||||||
self.input.extra_filter = None
|
self.input.extra_filter = None
|
||||||
del self.engine
|
del self.engine
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class AbstractEngineClass(abc.ABC):
|
|||||||
self.soft_deleted = table_engine.soft_deleted
|
self.soft_deleted = table_engine.soft_deleted
|
||||||
self.unselect = table_engine.unselect
|
self.unselect = table_engine.unselect
|
||||||
self.return_query = table_engine.return_query
|
self.return_query = table_engine.return_query
|
||||||
|
self.collation = {'locale': 'fa'}
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def exe(self):
|
def exe(self):
|
||||||
|
|||||||
+190
-16
@@ -2,6 +2,13 @@ import datetime
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Union, List
|
from typing import Union, List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
import os
|
||||||
|
import xlsxwriter
|
||||||
|
import tempfile
|
||||||
|
import string
|
||||||
|
import copy
|
||||||
|
import jdatetime
|
||||||
|
|
||||||
|
|
||||||
# import dateutil.parser
|
# import dateutil.parser
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
@@ -9,6 +16,8 @@ from celery import shared_task
|
|||||||
|
|
||||||
from .AbstractEngine import AbstractEngineClass
|
from .AbstractEngine import AbstractEngineClass
|
||||||
from ..utils.Models import FilterItem, FilterScope, OutPutType
|
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):
|
# def zero_time_data(date, is_max=False):
|
||||||
@@ -31,25 +40,30 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
rows = []
|
rows = []
|
||||||
stack = []
|
stack = []
|
||||||
filters = {}
|
filters = {}
|
||||||
|
_const_cache: dict = None # کش const ها از MySQL
|
||||||
|
|
||||||
async def exe(self):
|
async def exe(self):
|
||||||
_f = self.filters = self.set_filters()
|
_f = self.filters = self.set_filters()
|
||||||
print("_f ====>", _f)
|
|
||||||
if self.unselect:
|
if self.unselect:
|
||||||
tmp = self.query_class.find(_f, self.unselect)
|
tmp = self.query_class.find(_f, self.unselect)
|
||||||
else:
|
else:
|
||||||
tmp = self.query_class.find(_f)
|
tmp = self.query_class.find(_f)
|
||||||
|
|
||||||
sort_by = self.set_sort()
|
sort_by = self.set_sort()
|
||||||
if isinstance(sort_by, dict):
|
if isinstance(sort_by, dict):
|
||||||
tmp = tmp.sort(**sort_by)
|
tmp = tmp.sort(**sort_by)
|
||||||
else:
|
else:
|
||||||
tmp = tmp.sort(sort_by)
|
tmp = tmp.sort(sort_by)
|
||||||
|
|
||||||
self.rows = tmp.limit(
|
is_natural = isinstance(sort_by, dict) and sort_by.get('key_or_list') == '$natural'
|
||||||
self.input.get_limit()).skip(self.input.skip())
|
if getattr(self, 'collation', None) and not is_natural:
|
||||||
|
tmp = tmp.collation(self.collation)
|
||||||
|
|
||||||
if self.input.e and self.input.e.ot:
|
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:
|
if self.return_query:
|
||||||
return {'query':_f, 'sort':self.set_sort(), 'limit':self.input.get_limit(), 'page':self.input.skip()}
|
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()
|
return [self.model(**document) async for document in self.rows], await self.count()
|
||||||
@@ -249,15 +263,11 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
def set_joins(self):
|
def set_joins(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def generate_output(self, query, _f):
|
async def generate_output(self, _f):
|
||||||
print(_f.__dict__)
|
print(self.input.e.ot)
|
||||||
return ''
|
|
||||||
if self.input.e.ot == OutPutType.EXEL:
|
if self.input.e.ot == OutPutType.EXEL:
|
||||||
file_name = str(uuid.uuid4().hex) + '.xlsx'
|
file_name = str(uuid.uuid4().hex) + '.xlsx'
|
||||||
return _f.dict()
|
return await self.output_generator(_f, file_name=file_name, cols=self.input.e.c)
|
||||||
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
|
|
||||||
elif self.input.e.ot == OutPutType.JSON:
|
elif self.input.e.ot == OutPutType.JSON:
|
||||||
pass
|
pass
|
||||||
elif self.input.e.ot == OutPutType.PDF:
|
elif self.input.e.ot == OutPutType.PDF:
|
||||||
@@ -265,7 +275,171 @@ class MongoEngine(AbstractEngineClass):
|
|||||||
else:
|
else:
|
||||||
raise Exception("Output type not supported.")
|
raise Exception("Output type not supported.")
|
||||||
|
|
||||||
@staticmethod
|
def _excel_temp_dir(self):
|
||||||
@shared_task(name='output_generator_task_mongo')
|
"""پوشه temp پروژه برای ذخیره فایلهای اکسل (همان مسیری که برای دانلود سرو میشود)."""
|
||||||
def output_generator(query, params, url, file_name, cols):
|
temp_dir = os.path.join(os.getcwd(), 'temp')
|
||||||
pass
|
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):
|
class OutPutType(str, Enum):
|
||||||
JSON = 'json'
|
JSON = 'json'
|
||||||
EXEL = 'exel'
|
EXEL = 'excel'
|
||||||
PDF = 'pdf'
|
PDF = 'pdf'
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +81,7 @@ class ExportModel(BaseModel):
|
|||||||
ot: Optional[OutPutType]
|
ot: Optional[OutPutType]
|
||||||
c: Optional[dict]
|
c: Optional[dict]
|
||||||
d: Optional[str]
|
d: Optional[str]
|
||||||
|
vk: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
class SortModel(BaseModel):
|
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
|
aiofiles==22.1.0
|
||||||
|
aiomysql==0.2.0
|
||||||
aioredis==2.0.1
|
aioredis==2.0.1
|
||||||
|
asyncmy==0.2.9
|
||||||
amqp==5.1.1
|
amqp==5.1.1
|
||||||
annotated-types==0.6.0
|
annotated-types==0.6.0
|
||||||
anyio==3.6.2
|
anyio==3.6.2
|
||||||
|
|||||||
Reference in New Issue
Block a user