Merge pull request 'phase-directions: направления в фазах, год в зависимости от года формы или проекта, фикс с колонками для редактирования' (#66) from phase-directions into test
Reviewed-on: #66
This commit is contained in:
commit
b270c69fbd
82
api/alembic/versions/0014_direction.py
Normal file
82
api/alembic/versions/0014_direction.py
Normal file
@ -0,0 +1,82 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "0014"
|
||||
down_revision = "0013"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
||||
|
||||
|
||||
def _find_dollar_tag(line: str) -> str | None:
|
||||
m = _DOLLAR_TAG_RE.search(line.strip())
|
||||
return m.group(0) if m else None
|
||||
|
||||
|
||||
def _split_statements(sql: str) -> list[str]:
|
||||
statements: list[str] = []
|
||||
current: list[str] = []
|
||||
in_dollar = False
|
||||
dollar_tag: str | None = None
|
||||
|
||||
for line in sql.split("\n"):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("--"):
|
||||
continue
|
||||
|
||||
if not in_dollar:
|
||||
tag = _find_dollar_tag(stripped)
|
||||
if tag and tag.endswith("$") and tag.startswith("$"):
|
||||
dollar_tag = tag
|
||||
in_dollar = True
|
||||
current.append(line)
|
||||
continue
|
||||
|
||||
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
|
||||
after = stripped[len(dollar_tag):].strip()
|
||||
if after == ";" or after == "":
|
||||
in_dollar = False
|
||||
dollar_tag = None
|
||||
if after == ";":
|
||||
current.append(line)
|
||||
statements.append("\n".join(current))
|
||||
current = []
|
||||
continue
|
||||
|
||||
if not in_dollar and stripped.rstrip().endswith(";"):
|
||||
current.append(line)
|
||||
statements.append("\n".join(current))
|
||||
current = []
|
||||
continue
|
||||
|
||||
current.append(line)
|
||||
|
||||
remaining = "\n".join(current).strip()
|
||||
if remaining:
|
||||
statements.append(remaining)
|
||||
|
||||
return statements
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0014_direction.sql")
|
||||
with open(ddl_path) as f:
|
||||
content = f.read()
|
||||
|
||||
statements = _split_statements(content)
|
||||
for stmt in statements:
|
||||
stripped = stmt.strip().rstrip(";").strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
||||
continue
|
||||
op.execute(stripped)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
1281
api/alembic/versions/sql/0014_direction.sql
Normal file
1281
api/alembic/versions/sql/0014_direction.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,7 @@ def timezone_from_offset(offset: str) -> datetime.timezone | None:
|
||||
async def get_form_phases(
|
||||
form_id: int,
|
||||
sheet: str | None = None,
|
||||
direction: str | None = None,
|
||||
phase_code: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||
@ -57,6 +58,7 @@ async def get_form_phases(
|
||||
budget_form_id=form_id,
|
||||
user=current_user,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
)
|
||||
if phases:
|
||||
@ -117,6 +119,7 @@ async def update_form_phase(
|
||||
sheet: str,
|
||||
phase_code: str,
|
||||
body: FormPhaseUpdate,
|
||||
direction: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(require_admin),
|
||||
) -> BaseSingleResponse[FormPhaseResponse]:
|
||||
@ -129,6 +132,7 @@ async def update_form_phase(
|
||||
phase = await fp_service.update(
|
||||
budget_form=form,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
body=body,
|
||||
user=current_user,
|
||||
@ -155,6 +159,7 @@ async def delete_form_phase(
|
||||
form_id: int,
|
||||
sheet: str,
|
||||
phase_code: str,
|
||||
direction: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: AppUser = Depends(require_admin),
|
||||
):
|
||||
@ -167,6 +172,7 @@ async def delete_form_phase(
|
||||
deleted = await fp_service.delete(
|
||||
budget_form_id=form_id,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
user=current_user,
|
||||
)
|
||||
|
||||
@ -22,5 +22,6 @@ class FormPhase(Base):
|
||||
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||
direction: Mapped[str] = mapped_column(String)
|
||||
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
@ -22,6 +22,7 @@ class PhaseTemplate(Base):
|
||||
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||
direction: Mapped[str] = mapped_column(String)
|
||||
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
||||
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=False))
|
||||
|
||||
|
||||
@ -416,6 +416,7 @@ class FormPhaseCreate(BaseModel):
|
||||
column_keys: list[str]
|
||||
opens_at: datetime
|
||||
closes_at: datetime
|
||||
direction: str | None = None
|
||||
|
||||
|
||||
class FormPhaseUpdate(BaseModel):
|
||||
|
||||
@ -14,23 +14,33 @@ class FormPhaseRepository:
|
||||
self,
|
||||
budget_form_id: int,
|
||||
sheet: str | None = None,
|
||||
direction: str | None = None,
|
||||
phase_code: str | None = None,
|
||||
) -> list[FormPhase]:
|
||||
query = select(FormPhase).where(FormPhase.budget_form_id == budget_form_id)
|
||||
if sheet is not None:
|
||||
query = query.where(FormPhase.sheet == sheet)
|
||||
query = query.where(
|
||||
FormPhase.sheet == sheet,
|
||||
FormPhase.direction.is_(None) if direction is None else FormPhase.direction == direction
|
||||
)
|
||||
|
||||
if phase_code is not None:
|
||||
query = query.where(FormPhase.phase_code == phase_code)
|
||||
return (await self.db.execute(query)).scalars().all()
|
||||
|
||||
async def get(
|
||||
self, budget_form_id: int, sheet: str, phase_code: str
|
||||
self,
|
||||
budget_form_id: int,
|
||||
sheet: str,
|
||||
direction: str | None,
|
||||
phase_code: str,
|
||||
) -> FormPhase | None:
|
||||
query = (
|
||||
select(FormPhase)
|
||||
.where(FormPhase.budget_form_id == budget_form_id)
|
||||
.where(FormPhase.sheet == sheet)
|
||||
.where(FormPhase.phase_code == phase_code)
|
||||
.where(FormPhase.direction.is_(None) if direction is None else FormPhase.direction == direction)
|
||||
.limit(1)
|
||||
)
|
||||
return (await self.db.execute(query)).scalar_one_or_none()
|
||||
@ -39,6 +49,7 @@ class FormPhaseRepository:
|
||||
self,
|
||||
budget_form_id: int,
|
||||
sheet: str,
|
||||
direction: str | None,
|
||||
phase_code: str,
|
||||
role: str,
|
||||
column_keys: list[str],
|
||||
@ -52,6 +63,7 @@ class FormPhaseRepository:
|
||||
(v3.add_form_phase(
|
||||
:budget_form_id,
|
||||
:sheet,
|
||||
:direction,
|
||||
:phase_code,
|
||||
:role,
|
||||
:column_keys,
|
||||
@ -63,6 +75,7 @@ class FormPhaseRepository:
|
||||
{
|
||||
"budget_form_id": budget_form_id,
|
||||
"sheet": sheet,
|
||||
"direction": direction,
|
||||
"phase_code": phase_code,
|
||||
"role": role,
|
||||
"column_keys": column_keys,
|
||||
@ -72,10 +85,14 @@ class FormPhaseRepository:
|
||||
)
|
||||
if result.scalar_one_or_none() is None:
|
||||
return None
|
||||
return await self.get(budget_form_id, sheet, phase_code)
|
||||
return await self.get(budget_form_id, sheet, direction, phase_code)
|
||||
|
||||
async def update(
|
||||
self, budget_form_id: int, sheet: str, phase_code: str, data: dict
|
||||
self, budget_form_id: int,
|
||||
sheet: str,
|
||||
direction: str | None,
|
||||
phase_code: str,
|
||||
data: dict,
|
||||
) -> FormPhase | None:
|
||||
result = await self.db.execute(
|
||||
text(
|
||||
@ -84,6 +101,7 @@ class FormPhaseRepository:
|
||||
(v3.upd_form_phase(
|
||||
:budget_form_id,
|
||||
:sheet,
|
||||
:direction,
|
||||
:phase_code,
|
||||
:role,
|
||||
:column_keys,
|
||||
@ -95,6 +113,7 @@ class FormPhaseRepository:
|
||||
{
|
||||
"budget_form_id": budget_form_id,
|
||||
"sheet": sheet,
|
||||
"direction": direction,
|
||||
"phase_code": phase_code,
|
||||
"role": data.get("role"),
|
||||
"column_keys": data.get("column_keys"),
|
||||
@ -104,18 +123,19 @@ class FormPhaseRepository:
|
||||
)
|
||||
if result.scalar_one_or_none() is None:
|
||||
return None
|
||||
return await self.get(budget_form_id, sheet, phase_code)
|
||||
return await self.get(budget_form_id, sheet, direction, phase_code)
|
||||
|
||||
async def delete(
|
||||
self, budget_form_id: int, sheet: str, phase_code: str
|
||||
self, budget_form_id: int, sheet: str, direction: str | None, phase_code: str
|
||||
) -> bool:
|
||||
result = await self.db.execute(
|
||||
text(
|
||||
"SELECT v3.del_form_phase(:budget_form_id, :sheet, :phase_code)"
|
||||
"SELECT v3.del_form_phase(:budget_form_id, :sheet, :direction, :phase_code)"
|
||||
),
|
||||
{
|
||||
"budget_form_id": budget_form_id,
|
||||
"sheet": sheet,
|
||||
"direction": direction,
|
||||
"phase_code": phase_code,
|
||||
},
|
||||
)
|
||||
|
||||
@ -50,7 +50,6 @@ class SheetRepository:
|
||||
func_query = "v3.v_form_view(:form_id, :sheet, :sections, :direction)"
|
||||
else:
|
||||
func_query = "v3.v_form_view(:form_id, :sheet, :sections)"
|
||||
|
||||
return (
|
||||
await self.db.execute(
|
||||
text(
|
||||
|
||||
@ -21,20 +21,23 @@ class FormPhaseService:
|
||||
budget_form_id: int,
|
||||
user: AppUser,
|
||||
sheet: str | None = None,
|
||||
direction: str | None = None,
|
||||
phase_code: str | None = None,
|
||||
) -> list[FormPhase]:
|
||||
return await self.fp_repo.get_list(
|
||||
budget_form_id=budget_form_id,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
)
|
||||
|
||||
async def get(
|
||||
self, budget_form_id: int, sheet: str, phase_code: str, user: AppUser
|
||||
self, budget_form_id: int, sheet: str, direction: str | None, phase_code: str, user: AppUser
|
||||
) -> FormPhase | None:
|
||||
return await self.fp_repo.get(
|
||||
budget_form_id=budget_form_id,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
)
|
||||
|
||||
@ -43,12 +46,16 @@ class FormPhaseService:
|
||||
form_phase: FormPhaseCreate | FormPhaseUpdate,
|
||||
budget_form: BudgetForm,
|
||||
sheet: str | None = None,
|
||||
direction: str | None = None,
|
||||
) -> None:
|
||||
errors = {}
|
||||
if budget_form.form_type.sheet_list:
|
||||
sheet = sheet or form_phase.sheet
|
||||
if sheet not in budget_form.form_type.sheet_list:
|
||||
errors["sheet"] = [f"У формы нет листа {sheet}"]
|
||||
if budget_form.form_type.sheet_list_with_directions:
|
||||
if sheet:
|
||||
sheet = {"sheet_name": sheet, "direction": direction}
|
||||
else:
|
||||
sheet = {"sheet_name": form_phase.sheet, "direction": form_phase.direction}
|
||||
if sheet not in budget_form.form_type.sheet_list_with_directions:
|
||||
errors["sheet"] = [f"У формы нет листа {sheet['sheet_name']}/{sheet['direction']}"]
|
||||
if form_phase.column_keys and budget_form.form_type.section_list:
|
||||
for column_key in form_phase.column_keys:
|
||||
section = column_key.split(".")
|
||||
@ -84,6 +91,7 @@ class FormPhaseService:
|
||||
return await self.fp_repo.create(
|
||||
budget_form_id=budget_form_id or budget_form.id,
|
||||
sheet=body.sheet,
|
||||
direction=body.direction,
|
||||
phase_code=body.phase_code,
|
||||
role=body.role,
|
||||
column_keys=body.column_keys,
|
||||
@ -94,6 +102,7 @@ class FormPhaseService:
|
||||
async def update(
|
||||
self,
|
||||
sheet: str,
|
||||
direction: str | None,
|
||||
phase_code: str,
|
||||
body: FormPhaseUpdate,
|
||||
user: AppUser,
|
||||
@ -113,6 +122,7 @@ class FormPhaseService:
|
||||
form_phase=body,
|
||||
budget_form=budget_form,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
)
|
||||
|
||||
data = body.model_dump(exclude_unset=True)
|
||||
@ -120,20 +130,27 @@ class FormPhaseService:
|
||||
return await self.fp_repo.get(
|
||||
budget_form_id=budget_form_id or budget_form.id,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
)
|
||||
return await self.fp_repo.update(
|
||||
budget_form_id=budget_form_id or budget_form.id,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def delete(
|
||||
self, budget_form_id: int, sheet: str, phase_code: str, user: AppUser
|
||||
self, budget_form_id: int,
|
||||
sheet: str,
|
||||
direction: str | None,
|
||||
phase_code: str,
|
||||
user: AppUser,
|
||||
) -> bool:
|
||||
return await self.fp_repo.delete(
|
||||
budget_form_id=budget_form_id,
|
||||
sheet=sheet,
|
||||
direction=direction,
|
||||
phase_code=phase_code,
|
||||
)
|
||||
|
||||
@ -149,7 +149,7 @@ def test_form_phases_update(client, admin_tokens):
|
||||
|
||||
def test_form_phases_delete(client, admin_tokens):
|
||||
response = client.delete(
|
||||
"/api/v1/stages/form/1/AHR/test",
|
||||
"/api/v1/stages/form/1/AHR/test?direction=Support",
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
@ -23,9 +23,10 @@ INSERT INTO v3.budget_line (id,budget_form_id,expense_item_id,"name",internal_or
|
||||
(1,1,88,'1234',NULL,NULL,NULL,NULL,NULL,'2026-05-06 16:58:05.793083',NULL,'2026-06-22 17:41:54.255313','Support');
|
||||
SELECT setval('v3.budget_line_id_seq', 1);
|
||||
|
||||
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at) VALUES
|
||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03'),
|
||||
(4,'AHR','test','DFIP','{{plan.q1}}','2029-05-05 03:00:00+05','2029-06-06 03:00:00+05');
|
||||
INSERT INTO v3.form_phase (budget_form_id,sheet,phase_code,"role",column_keys,opens_at,closes_at,direction) VALUES
|
||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03','Support'),
|
||||
(1,'AHR','test','DFIP','{{plan.q1}}','2026-05-05 03:00:00+03','2026-06-06 03:00:00+03','Development'),
|
||||
(4,'AHR','test','DFIP','{{plan.q1}}','2029-05-05 03:00:00+05','2029-06-06 03:00:00+05',NULL);
|
||||
|
||||
INSERT INTO v3.vsp (id,branch_id,reg_number,address,format,opened_at,placement_type,staff_count,total_area,closed_at,is_active,updated_at,is_deleted,created_at,created_by,system_code,updated_by,vsp_type,notes,rent_contract_num,rent_end_date) VALUES
|
||||
(1,2,'Тестовый всп 1','Тестовая 12','укукк','2026-05-07','ывс',2026,230,'2026-06-16',false,'2026-06-18 15:46:38.611903',false,'2026-06-04 14:36:59.737727',1,'1233',1,'','','',NULL),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user