feat: 顺丰运费保存单号并清空金额,已结算批次禁止改关联
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d67965aae2
commit
11a46bf1eb
7 files changed
+155
-25
No files matched your search
@@ -900,6 +900,7 @@ def _register_freight_routes(scope_kind: ScopeKind) -> None:
|
||||
user_info=user,
|
||||
transport_mode=body.transport_mode,
|
||||
freight_amount=body.freight_amount,
|
||||
waybill_no=body.waybill_no,
|
||||
items=[
|
||||
item.model_dump(by_alias=True) for item in body.items
|
||||
],
|
||||
@@ -925,6 +926,9 @@ def _register_freight_routes(scope_kind: ScopeKind) -> None:
|
||||
user_info=user,
|
||||
transport_mode=body.transport_mode,
|
||||
freight_amount=body.freight_amount,
|
||||
waybill_no=body.waybill_no,
|
||||
waybill_supplied='waybill_no' in body.model_fields_set,
|
||||
amount_supplied='freight_amount' in body.model_fields_set,
|
||||
),
|
||||
message='保存成功',
|
||||
)
|
||||
|
||||
@@ -47,7 +47,8 @@ class SampleFreightCreateBody(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
transport_mode: str = Field(..., alias="transportMode")
|
||||
freight_amount: float = Field(..., alias="freightAmount")
|
||||
freight_amount: Optional[float] = Field(None, alias="freightAmount")
|
||||
waybill_no: Optional[str] = Field(None, alias="waybillNo")
|
||||
items: Optional[List[SampleFreightItemBody]] = None
|
||||
|
||||
|
||||
@@ -56,6 +57,7 @@ class SampleFreightUpdateBody(BaseModel):
|
||||
|
||||
transport_mode: Optional[str] = Field(None, alias="transportMode")
|
||||
freight_amount: Optional[float] = Field(None, alias="freightAmount")
|
||||
waybill_no: Optional[str] = Field(None, alias="waybillNo")
|
||||
|
||||
|
||||
class SampleFreightItemsBody(BaseModel):
|
||||
@@ -163,6 +165,9 @@ async def create_sample_freight(
|
||||
user_id=user_id,
|
||||
transport_mode_raw=body.transport_mode,
|
||||
freight_amount=body.freight_amount,
|
||||
waybill_no=body.waybill_no,
|
||||
waybill_supplied=True,
|
||||
amount_supplied=True,
|
||||
items=items,
|
||||
)
|
||||
return success_response(data, message="创建成功")
|
||||
@@ -190,6 +195,9 @@ async def update_sample_freight(
|
||||
user_id=user_id,
|
||||
transport_mode_raw=body.transport_mode,
|
||||
freight_amount=body.freight_amount,
|
||||
waybill_no=body.waybill_no,
|
||||
waybill_supplied="waybill_no" in body.model_fields_set,
|
||||
amount_supplied="freight_amount" in body.model_fields_set,
|
||||
)
|
||||
return success_response(data, message="更新成功")
|
||||
except PermissionError as e:
|
||||
|
||||
@@ -27,7 +27,11 @@ from app.models.research_donation import (
|
||||
BizResearchDonationSample,
|
||||
)
|
||||
from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value
|
||||
from app.services.sf_express_settlement import freight_amount_label
|
||||
from app.services.sf_express_settlement import (
|
||||
assert_freight_mutable,
|
||||
freight_amount_label,
|
||||
resolve_header_for_save,
|
||||
)
|
||||
|
||||
SAMPLE_SOURCE_CELL = "cell_storage"
|
||||
SAMPLE_SOURCE_DONATION = "research_donation"
|
||||
@@ -839,19 +843,34 @@ def create_freight(
|
||||
items: Optional[List[Dict[str, Any]]] = None,
|
||||
commit: bool = True,
|
||||
user_info: Optional[dict] = None,
|
||||
waybill_no: Any = None,
|
||||
waybill_supplied: bool = True,
|
||||
amount_supplied: bool = True,
|
||||
) -> Dict[str, Any]:
|
||||
if user_info is not None:
|
||||
items = _validated_items(db, items or [], tenant_no, user_info)
|
||||
mode = resolve_transport_mode_value(
|
||||
db, tenant_no=tenant_no, raw=transport_mode_raw, user_id=user_id
|
||||
)
|
||||
amount = _parse_amount(freight_amount)
|
||||
plan = resolve_header_for_save(
|
||||
transport_mode=mode,
|
||||
existing_waybill=None,
|
||||
existing_amount=None,
|
||||
existing_settle_status=None,
|
||||
waybill_supplied=waybill_supplied,
|
||||
waybill_no=waybill_no,
|
||||
amount_supplied=amount_supplied,
|
||||
freight_amount=freight_amount,
|
||||
)
|
||||
now = _now()
|
||||
obj = BizSampleFreight(
|
||||
id=generate_uuid(),
|
||||
freight_no=_generate_freight_no(db, tenant_no, when=now),
|
||||
transport_mode=mode,
|
||||
freight_amount=amount,
|
||||
transport_mode=plan.transport_mode,
|
||||
freight_amount=plan.freight_amount,
|
||||
waybill_no=plan.waybill_no,
|
||||
settle_status=plan.settle_status,
|
||||
settle_time=None,
|
||||
tenant_no=tenant_no,
|
||||
order_num=0,
|
||||
remark=None,
|
||||
@@ -883,16 +902,34 @@ def update_freight(
|
||||
freight_amount: Any = None,
|
||||
commit: bool = True,
|
||||
user_info: Optional[dict] = None,
|
||||
waybill_no: Any = None,
|
||||
waybill_supplied: bool = False,
|
||||
amount_supplied: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
obj = get_freight(db, freight_id, tenant_no, user_info)
|
||||
if not obj:
|
||||
raise ValueError("运费批次不存在")
|
||||
mode = obj.transport_mode or ""
|
||||
if transport_mode_raw is not None and str(transport_mode_raw).strip() != "":
|
||||
obj.transport_mode = resolve_transport_mode_value(
|
||||
mode = resolve_transport_mode_value(
|
||||
db, tenant_no=tenant_no, raw=str(transport_mode_raw), user_id=user_id
|
||||
)
|
||||
if freight_amount is not None and freight_amount != "":
|
||||
obj.freight_amount = _parse_amount(freight_amount)
|
||||
plan = resolve_header_for_save(
|
||||
transport_mode=mode,
|
||||
existing_waybill=obj.waybill_no,
|
||||
existing_amount=obj.freight_amount,
|
||||
existing_settle_status=obj.settle_status,
|
||||
waybill_supplied=waybill_supplied,
|
||||
waybill_no=waybill_no,
|
||||
amount_supplied=amount_supplied,
|
||||
freight_amount=freight_amount,
|
||||
)
|
||||
obj.transport_mode = plan.transport_mode
|
||||
obj.waybill_no = plan.waybill_no
|
||||
obj.freight_amount = plan.freight_amount
|
||||
obj.settle_status = plan.settle_status
|
||||
if plan.clear_settle_time:
|
||||
obj.settle_time = None
|
||||
obj.update_by = user_id
|
||||
obj.update_time = _now()
|
||||
db.add(obj)
|
||||
@@ -913,9 +950,10 @@ def link_samples(
|
||||
commit: bool = True,
|
||||
user_info: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
obj = get_freight(db, freight_id, tenant_no, user_info)
|
||||
if not obj:
|
||||
header = get_freight(db, freight_id, tenant_no, user_info)
|
||||
if not header:
|
||||
raise ValueError("运费批次不存在")
|
||||
assert_freight_mutable(header.settle_status, "items")
|
||||
if not items:
|
||||
raise ValueError("请选择要关联的样本")
|
||||
if user_info is not None:
|
||||
@@ -939,9 +977,11 @@ def unlink_samples(
|
||||
commit: bool = True,
|
||||
user_info: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
obj = get_freight(db, freight_id, tenant_no, user_info)
|
||||
if not obj:
|
||||
header = get_freight(db, freight_id, tenant_no, user_info)
|
||||
if not header:
|
||||
raise ValueError("运费批次不存在")
|
||||
assert_freight_mutable(header.settle_status, "items")
|
||||
obj = header
|
||||
now = _now()
|
||||
for it in items or []:
|
||||
norm = _normalize_item(it)
|
||||
@@ -992,8 +1032,10 @@ def delete_freight(
|
||||
user_info: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""级联软删运费批次及其全部有效关联样本。"""
|
||||
if user_info is not None and not get_freight(db, freight_id, tenant_no, user_info):
|
||||
header = get_freight(db, freight_id, tenant_no, user_info)
|
||||
if not header:
|
||||
raise ValueError("运费批次不存在")
|
||||
assert_freight_mutable(header.settle_status, "delete")
|
||||
obj = (
|
||||
db.query(BizSampleFreight)
|
||||
.filter(
|
||||
@@ -1204,6 +1246,9 @@ def apply_freight_on_protocol_save(
|
||||
"freightAmount": draft.get("freightAmount")
|
||||
if draft.get("freightAmount") is not None
|
||||
else draft.get("freight_amount"),
|
||||
"waybillNo": draft.get("waybillNo")
|
||||
if draft.get("waybillNo") is not None
|
||||
else draft.get("waybill_no"),
|
||||
"items": [item],
|
||||
}
|
||||
else:
|
||||
@@ -1216,6 +1261,9 @@ def apply_freight_on_protocol_save(
|
||||
user_id=user_id,
|
||||
transport_mode_raw=str(group.get("transportMode") or ""),
|
||||
freight_amount=group.get("freightAmount"),
|
||||
waybill_no=group.get("waybillNo"),
|
||||
waybill_supplied=True,
|
||||
amount_supplied=True,
|
||||
items=group["items"],
|
||||
commit=False,
|
||||
)
|
||||
|
||||
@@ -217,6 +217,7 @@ class MarketProtocolSampleFreightDraftBody(BaseModel):
|
||||
client_key: Optional[str] = Field(None, alias='clientKey')
|
||||
transport_mode: Optional[str] = Field(None, alias='transportMode')
|
||||
freight_amount: Optional[MarketContractAmount] = Field(None, alias='freightAmount')
|
||||
waybill_no: Optional[str] = Field(None, alias='waybillNo')
|
||||
|
||||
|
||||
class MarketProtocolSampleBody(BaseModel):
|
||||
@@ -486,10 +487,11 @@ class MarketFreightCreateBody(MarketFreightItemsBody):
|
||||
model_config = ConfigDict(extra='forbid', populate_by_name=True)
|
||||
|
||||
transport_mode: str = Field(..., min_length=1, max_length=100, alias='transportMode')
|
||||
freight_amount: Decimal = Field(
|
||||
..., ge=Decimal('0'), max_digits=18, decimal_places=2,
|
||||
freight_amount: Optional[Decimal] = Field(
|
||||
None, ge=Decimal('0'), max_digits=18, decimal_places=2,
|
||||
allow_inf_nan=False, alias='freightAmount'
|
||||
)
|
||||
waybill_no: Optional[str] = Field(None, alias='waybillNo')
|
||||
|
||||
|
||||
class MarketFreightUpdateBody(BaseModel):
|
||||
@@ -502,3 +504,4 @@ class MarketFreightUpdateBody(BaseModel):
|
||||
None, ge=Decimal('0'), max_digits=18, decimal_places=2,
|
||||
allow_inf_nan=False, alias='freightAmount'
|
||||
)
|
||||
waybill_no: Optional[str] = Field(None, alias='waybillNo')
|
||||
@@ -43,7 +43,11 @@ from app.services.market_field_scope import (
|
||||
apply_donation_scope,
|
||||
)
|
||||
from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value
|
||||
from app.services.sf_express_settlement import freight_amount_label
|
||||
from app.services.sf_express_settlement import (
|
||||
assert_freight_mutable,
|
||||
freight_amount_label,
|
||||
resolve_header_for_save,
|
||||
)
|
||||
from app.services.web_auth_roles import (
|
||||
_map_role_name_to_web,
|
||||
resolve_web_role_for_user,
|
||||
@@ -1056,7 +1060,7 @@ def _next_freight_no(db: Session, tenant_no: str, now: datetime, offset: int = 0
|
||||
return f'{prefix}{seq:03d}'
|
||||
|
||||
|
||||
def create_freight(db: Session, *, ctx: MarketFieldContext, user_info: dict, transport_mode: str, freight_amount: Any, items: List[Dict[str, Any]]):
|
||||
def create_freight(db: Session, *, ctx: MarketFieldContext, user_info: dict, transport_mode: str, freight_amount: Any, items: List[Dict[str, Any]], waybill_no: Any = None):
|
||||
keys = _normalize_items(items)
|
||||
try:
|
||||
samples = _lock_sample_graph(db, keys, ctx, user_info)
|
||||
@@ -1066,11 +1070,20 @@ def create_freight(db: Session, *, ctx: MarketFieldContext, user_info: dict, tra
|
||||
if conflicts:
|
||||
raise ValueError('样本已关联其他运费批次')
|
||||
mode = resolve_transport_mode_value(db, tenant_no=ctx.tenant_no, raw=transport_mode, user_id=ctx.user_id)
|
||||
amount = _amount(freight_amount)
|
||||
plan = resolve_header_for_save(
|
||||
transport_mode=mode,
|
||||
existing_waybill=None,
|
||||
existing_amount=None,
|
||||
existing_settle_status=None,
|
||||
waybill_supplied=True,
|
||||
waybill_no=waybill_no,
|
||||
amount_supplied=True,
|
||||
freight_amount=freight_amount,
|
||||
)
|
||||
now = datetime.now()
|
||||
obj = None
|
||||
for attempt in range(3):
|
||||
candidate = BizSampleFreight(id=generate_uuid(), freight_no=_next_freight_no(db, ctx.tenant_no, now, attempt), transport_mode=mode, freight_amount=amount, tenant_no=ctx.tenant_no, order_num=0, remark=None, create_by=ctx.user_id, create_dept_id=str(user_info.get('dept_id') or user_info.get('region_id') or ''), create_time=now, update_by=ctx.user_id, update_time=now, del_flag=0)
|
||||
candidate = BizSampleFreight(id=generate_uuid(), freight_no=_next_freight_no(db, ctx.tenant_no, now, attempt), transport_mode=plan.transport_mode, freight_amount=plan.freight_amount, waybill_no=plan.waybill_no, settle_status=plan.settle_status, settle_time=None, tenant_no=ctx.tenant_no, order_num=0, remark=None, create_by=ctx.user_id, create_dept_id=str(user_info.get('dept_id') or user_info.get('region_id') or ''), create_time=now, update_by=ctx.user_id, update_time=now, del_flag=0)
|
||||
try:
|
||||
with db.begin_nested():
|
||||
db.add(candidate)
|
||||
@@ -1095,7 +1108,7 @@ def create_freight(db: Session, *, ctx: MarketFieldContext, user_info: dict, tra
|
||||
raise
|
||||
|
||||
|
||||
def update_freight(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict, transport_mode: Optional[str] = None, freight_amount: Any = None):
|
||||
def update_freight(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict, transport_mode: Optional[str] = None, freight_amount: Any = None, waybill_no: Any = None, waybill_supplied: bool = False, amount_supplied: bool = False):
|
||||
try:
|
||||
obj = _lock_batch_header(db, freight_id, ctx)
|
||||
expected_keys = _discover_batch_keys(db, freight_id, ctx)
|
||||
@@ -1112,10 +1125,25 @@ def update_freight(db: Session, freight_id: str, *, ctx: MarketFieldContext, use
|
||||
(row.sample_source, row.sample_id) not in visible for row in active
|
||||
):
|
||||
raise PermissionError('仅当批次全部有效样本均在当前范围内时可修改批次信息')
|
||||
if transport_mode not in (None, ''):
|
||||
obj.transport_mode = resolve_transport_mode_value(db, tenant_no=ctx.tenant_no, raw=str(transport_mode), user_id=ctx.user_id)
|
||||
if freight_amount not in (None, ''):
|
||||
obj.freight_amount = _amount(freight_amount)
|
||||
mode = obj.transport_mode or ''
|
||||
if transport_mode is not None and str(transport_mode).strip() != '':
|
||||
mode = resolve_transport_mode_value(db, tenant_no=ctx.tenant_no, raw=str(transport_mode), user_id=ctx.user_id)
|
||||
plan = resolve_header_for_save(
|
||||
transport_mode=mode,
|
||||
existing_waybill=obj.waybill_no,
|
||||
existing_amount=obj.freight_amount,
|
||||
existing_settle_status=obj.settle_status,
|
||||
waybill_supplied=waybill_supplied,
|
||||
waybill_no=waybill_no,
|
||||
amount_supplied=amount_supplied,
|
||||
freight_amount=freight_amount,
|
||||
)
|
||||
obj.transport_mode = plan.transport_mode
|
||||
obj.waybill_no = plan.waybill_no
|
||||
obj.freight_amount = plan.freight_amount
|
||||
obj.settle_status = plan.settle_status
|
||||
if plan.clear_settle_time:
|
||||
obj.settle_time = None
|
||||
obj.update_by, obj.update_time = ctx.user_id, datetime.now()
|
||||
db.flush()
|
||||
db.commit()
|
||||
@@ -1145,6 +1173,7 @@ def link_samples(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_
|
||||
if not _freight_accessible(db, freight_id, ctx=ctx, user_info=user_info):
|
||||
raise HTTPException(status_code=404, detail='数据不存在或无权访问')
|
||||
obj = _lock_batch_header(db, freight_id, ctx)
|
||||
assert_freight_mutable(obj.settle_status, 'items')
|
||||
expected_keys = _discover_batch_keys(db, freight_id, ctx)
|
||||
samples = _lock_sample_graph(
|
||||
db, sorted(set(expected_keys + keys)), ctx, user_info
|
||||
@@ -1185,6 +1214,7 @@ def unlink_samples(db: Session, freight_id: str, *, ctx: MarketFieldContext, use
|
||||
keys = _normalize_items(items)
|
||||
try:
|
||||
obj = _lock_batch_header(db, freight_id, ctx)
|
||||
assert_freight_mutable(obj.settle_status, 'items')
|
||||
expected_keys = _discover_batch_keys(db, freight_id, ctx)
|
||||
visible = _lock_sample_graph(db, expected_keys, ctx, user_info)
|
||||
active = _lock_active_items(
|
||||
@@ -1233,6 +1263,7 @@ def delete_freight(db: Session, freight_id: str, *, ctx: MarketFieldContext, use
|
||||
"""级联软删运费单;仅当全部有效关联样本均在当前范围内时允许。"""
|
||||
try:
|
||||
obj = _lock_batch_header(db, freight_id, ctx)
|
||||
assert_freight_mutable(obj.settle_status, 'delete')
|
||||
expected_keys = _discover_batch_keys(db, freight_id, ctx)
|
||||
visible = _lock_sample_graph(db, expected_keys, ctx, user_info)
|
||||
active = _lock_active_items(
|
||||
|
||||
@@ -364,7 +364,10 @@ def test_header_update_allowed_when_all_active_items_are_visible():
|
||||
db = _session()
|
||||
own, op = _sample(db, 'own')
|
||||
freight = _freight(db, 'own', [('cell_storage', own, op)])
|
||||
result = update_freight(db, freight.id, ctx=_ctx(), user_info=_user(), freight_amount='99.10')
|
||||
result = update_freight(
|
||||
db, freight.id, ctx=_ctx(), user_info=_user(),
|
||||
freight_amount='99.10', amount_supplied=True,
|
||||
)
|
||||
assert result['freightAmount'] == '99.10'
|
||||
|
||||
|
||||
|
||||
@@ -226,3 +226,36 @@ def test_sample_card_payload_keeps_null_amount():
|
||||
assert numeric["freightAmount"] == "88.00"
|
||||
assert numeric["freightAmountLabel"] == "88.00"
|
||||
assert freight_amount_payload(Decimal("0"), "zxc", False)["freightAmount"] == 0.0
|
||||
|
||||
|
||||
def test_admin_and_market_bodies_accept_sf_without_amount():
|
||||
from app.api.sample_freight import SampleFreightCreateBody, SampleFreightUpdateBody
|
||||
from app.schemas.market_mobile import (
|
||||
MarketFreightCreateBody,
|
||||
MarketFreightUpdateBody,
|
||||
MarketProtocolSampleFreightDraftBody,
|
||||
)
|
||||
|
||||
admin = SampleFreightCreateBody.model_validate({
|
||||
"transportMode": "sfkd",
|
||||
"waybillNo": "SF100",
|
||||
"items": [{"sampleSource": "cell_storage", "sampleId": "s1"}],
|
||||
})
|
||||
assert admin.freight_amount is None
|
||||
assert admin.waybill_no == "SF100"
|
||||
updated = SampleFreightUpdateBody.model_validate({"waybillNo": "SF101"})
|
||||
assert "waybill_no" in updated.model_fields_set
|
||||
market = MarketFreightCreateBody.model_validate({
|
||||
"transportMode": "sfkd",
|
||||
"waybillNo": "SF100",
|
||||
"items": [{"sampleSource": "cell_storage", "sampleId": "s1"}],
|
||||
})
|
||||
assert market.freight_amount is None
|
||||
assert market.waybill_no == "SF100"
|
||||
draft = MarketProtocolSampleFreightDraftBody.model_validate({
|
||||
"clientKey": "fd_1",
|
||||
"transportMode": "sfkd",
|
||||
"waybillNo": "SF100",
|
||||
})
|
||||
assert draft.waybill_no == "SF100"
|
||||
MarketFreightUpdateBody.model_validate({"waybillNo": "SF100"})
|
||||
Reference in new issue
Block a user