Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c40feab76a | ||
|
|
9cf1110495 |
No files matched your search
@@ -287,9 +287,15 @@ def _donation_rows_to_sample_list_items(
|
||||
),
|
||||
"sourceType": "research_donation",
|
||||
"sourceTypeLabel": "科研捐赠",
|
||||
"consentAttachmentFiles": urls_to_attachment_list(
|
||||
donation.consent_form_urls if donation else None, db, tenant_no
|
||||
),
|
||||
"infectiousDiseaseReportAttachmentFiles": urls_to_attachment_list(
|
||||
donation.infectious_disease_report_urls if donation else None, db, tenant_no
|
||||
),
|
||||
"attachmentFiles": urls_to_attachment_list(
|
||||
donation.attachment_urls if donation else None, db, tenant_no
|
||||
),
|
||||
"etaTime": _fmt_dt(transport.eta_time) if transport and transport.eta_time else "",
|
||||
"transportType": (transport.transport_type if transport else "") or "",
|
||||
"waybillNo": (transport.waybill_no if transport else "") or "",
|
||||
@@ -462,6 +468,12 @@ def _rows_to_sample_list_items(
|
||||
**sample_special_to_dict(sample),
|
||||
"sourceType": "cell_storage",
|
||||
"sourceTypeLabel": "细胞存储",
|
||||
"protocolAttachmentFiles": urls_to_attachment_list(
|
||||
protocol.protocol_attachments if protocol else None, db, tenant_no
|
||||
),
|
||||
"consentAttachmentFiles": urls_to_attachment_list(
|
||||
protocol.consent_form_urls if protocol else None, db, tenant_no
|
||||
),
|
||||
"healthSurveyAttachmentFiles": urls_to_attachment_list(
|
||||
protocol.health_survey_form_urls if protocol else None, db, tenant_no
|
||||
),
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""样本接收列表项应携带协议侧附件字段,供质检查看。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class _FakeQuery:
|
||||
def filter(self, *args, **kwargs):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return []
|
||||
|
||||
def first(self):
|
||||
return None
|
||||
|
||||
|
||||
class _FakeDb:
|
||||
def query(self, *args, **kwargs):
|
||||
return _FakeQuery()
|
||||
|
||||
|
||||
class TestSampleReceptionProtocolAttachments(unittest.TestCase):
|
||||
def test_cell_storage_item_includes_protocol_consent_health_files(self):
|
||||
from app.crud import sample_reception as svc
|
||||
|
||||
protocol = SimpleNamespace(
|
||||
id="proto-1",
|
||||
protocol_no="P1",
|
||||
protocol_status="effective",
|
||||
protocol_attachments="http://a/p.pdf",
|
||||
consent_form_urls="http://a/c.pdf",
|
||||
health_survey_form_urls="http://a/h.pdf",
|
||||
)
|
||||
sample = SimpleNamespace(
|
||||
id="sample-1",
|
||||
sample_code="S001",
|
||||
sample_type_id="t1",
|
||||
collect_amount=None,
|
||||
sample_status="",
|
||||
)
|
||||
party = SimpleNamespace(mother_name="妈妈", newborn_name="宝宝")
|
||||
transport = SimpleNamespace(
|
||||
eta_time=None,
|
||||
transport_type="",
|
||||
waybill_no="",
|
||||
express_company_id="",
|
||||
carrier_name="",
|
||||
carrier_phone="",
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
svc,
|
||||
"urls_to_attachment_list",
|
||||
side_effect=lambda urls, *_a, **_k: [{"fileUrl": urls}] if urls else [],
|
||||
), patch.object(svc, "sample_special_to_dict", return_value={}), patch.object(
|
||||
svc.prod_reception_svc, "get_map_by_reception_ids", return_value={}
|
||||
), patch(
|
||||
"app.services.qc_work_order_fee_summary.build_protocol_payment_summaries",
|
||||
return_value={},
|
||||
):
|
||||
items = svc._rows_to_sample_list_items(
|
||||
_FakeDb(),
|
||||
[(sample, protocol, party, transport)],
|
||||
"t1",
|
||||
reception_map={},
|
||||
)
|
||||
|
||||
self.assertEqual(len(items), 1)
|
||||
item = items[0]
|
||||
self.assertEqual(item["protocolAttachmentFiles"][0]["fileUrl"], "http://a/p.pdf")
|
||||
self.assertEqual(item["consentAttachmentFiles"][0]["fileUrl"], "http://a/c.pdf")
|
||||
self.assertEqual(item["healthSurveyAttachmentFiles"][0]["fileUrl"], "http://a/h.pdf")
|
||||
|
||||
def test_donation_item_includes_consent_infectious_other_files(self):
|
||||
from app.crud import sample_reception as svc
|
||||
|
||||
donation = SimpleNamespace(
|
||||
id="don-1",
|
||||
donation_no="D1",
|
||||
donation_status="effective",
|
||||
consent_form_urls="http://a/c.pdf",
|
||||
infectious_disease_report_urls="http://a/i.pdf",
|
||||
attachment_urls="http://a/o.pdf",
|
||||
)
|
||||
sample = SimpleNamespace(
|
||||
id="ds-1",
|
||||
sample_code="D001",
|
||||
sample_type_id="t1",
|
||||
collect_amount=None,
|
||||
)
|
||||
party = SimpleNamespace(mother_name="妈妈", newborn_name="宝宝")
|
||||
transport = SimpleNamespace(
|
||||
eta_time=None,
|
||||
transport_type="",
|
||||
waybill_no="",
|
||||
express_company_id="",
|
||||
carrier_name="",
|
||||
carrier_phone="",
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
svc,
|
||||
"urls_to_attachment_list",
|
||||
side_effect=lambda urls, *_a, **_k: [{"fileUrl": urls}] if urls else [],
|
||||
), patch.object(svc, "sample_special_to_dict", return_value={}), patch.object(
|
||||
svc.prod_reception_svc, "get_map_by_reception_ids", return_value={}
|
||||
):
|
||||
items = svc._donation_rows_to_sample_list_items(
|
||||
_FakeDb(),
|
||||
[(sample, donation, party, transport)],
|
||||
"t1",
|
||||
reception_map={},
|
||||
)
|
||||
|
||||
self.assertEqual(len(items), 1)
|
||||
item = items[0]
|
||||
self.assertEqual(item["consentAttachmentFiles"][0]["fileUrl"], "http://a/c.pdf")
|
||||
self.assertEqual(
|
||||
item["infectiousDiseaseReportAttachmentFiles"][0]["fileUrl"], "http://a/i.pdf"
|
||||
)
|
||||
self.assertEqual(item["attachmentFiles"][0]["fileUrl"], "http://a/o.pdf")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
getConsentProtocolAttachment,
|
||||
getSourceProtocolAttachment,
|
||||
getSourceProtocolAttachmentGroups
|
||||
} from './protocol-source-attachment'
|
||||
|
||||
describe('protocol-source-attachment', () => {
|
||||
it('returns health survey as primary for cell storage', () => {
|
||||
const meta = getSourceProtocolAttachment({
|
||||
sourceType: 'cell_storage',
|
||||
healthSurveyAttachmentFiles: [{ id: '1', fileUrl: '/a.png' }]
|
||||
})
|
||||
assert.equal(meta.label, '孕妇健康调查表')
|
||||
assert.equal(meta.files.length, 1)
|
||||
})
|
||||
|
||||
it('returns infectious disease report as primary for research donation', () => {
|
||||
const meta = getSourceProtocolAttachment({
|
||||
sourceType: 'research_donation',
|
||||
infectiousDiseaseReportAttachmentFiles: [{ id: '2', fileUrl: '/b.png' }]
|
||||
})
|
||||
assert.equal(meta.label, '传染病检测报告')
|
||||
assert.equal(meta.files.length, 1)
|
||||
})
|
||||
|
||||
it('returns consent attachment from shared field', () => {
|
||||
const meta = getConsentProtocolAttachment({
|
||||
consentAttachmentFiles: [{ id: '3', fileUrl: '/c.png' }]
|
||||
})
|
||||
assert.equal(meta.label, '知情同意书')
|
||||
assert.equal(meta.files.length, 1)
|
||||
})
|
||||
|
||||
it('groups cell storage attachments for reception dialog', () => {
|
||||
const groups = getSourceProtocolAttachmentGroups({
|
||||
sourceType: 'cell_storage',
|
||||
protocolAttachmentFiles: [{ id: 'p' }],
|
||||
consentAttachmentFiles: [{ id: 'c' }],
|
||||
healthSurveyAttachmentFiles: [{ id: 'h' }]
|
||||
})
|
||||
assert.deepEqual(
|
||||
groups.map((g) => g.label),
|
||||
['协议附件', '知情同意书', '孕妇健康调查表']
|
||||
)
|
||||
assert.ok(groups.every((g) => g.files.length === 1))
|
||||
})
|
||||
|
||||
it('groups research donation attachments for reception dialog', () => {
|
||||
const groups = getSourceProtocolAttachmentGroups({
|
||||
sourceType: 'research_donation',
|
||||
consentAttachmentFiles: [{ id: 'c' }],
|
||||
infectiousDiseaseReportAttachmentFiles: [{ id: 'i' }],
|
||||
attachmentFiles: [{ id: 'o' }]
|
||||
})
|
||||
assert.deepEqual(
|
||||
groups.map((g) => g.label),
|
||||
['知情同意书', '传染病检测报告', '其他附件']
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ function normalizeAttachmentFiles(raw: unknown): Array<Record<string, unknown>>
|
||||
return raw.filter((item) => item && typeof item === 'object') as Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
/** 按来源返回协议级附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */
|
||||
/** 按来源返回协议级主附件标签与文件列表(细胞存储=孕妇健康调查表,科研捐赠=传染病检测报告) */
|
||||
export function getSourceProtocolAttachment(
|
||||
row: Record<string, unknown>
|
||||
): ProtocolSourceAttachmentMeta {
|
||||
@@ -24,3 +24,54 @@ export function getSourceProtocolAttachment(
|
||||
files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles)
|
||||
}
|
||||
}
|
||||
|
||||
/** 知情同意书(细胞存储与科研捐赠共用字段) */
|
||||
export function getConsentProtocolAttachment(
|
||||
row: Record<string, unknown>
|
||||
): ProtocolSourceAttachmentMeta {
|
||||
return {
|
||||
label: '知情同意书',
|
||||
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 样本接收弹窗:按来源返回质检需查看的协议附件分组。
|
||||
* 细胞存储:协议附件 / 知情同意书 / 孕妇健康调查表
|
||||
* 科研捐赠:知情同意书 / 传染病检测报告 / 其他附件
|
||||
*/
|
||||
export function getSourceProtocolAttachmentGroups(
|
||||
row: Record<string, unknown>
|
||||
): ProtocolSourceAttachmentMeta[] {
|
||||
const source = String(row.sourceType || 'cell_storage').trim()
|
||||
if (source === 'research_donation') {
|
||||
return [
|
||||
{
|
||||
label: '知情同意书',
|
||||
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
|
||||
},
|
||||
{
|
||||
label: '传染病检测报告',
|
||||
files: normalizeAttachmentFiles(row.infectiousDiseaseReportAttachmentFiles)
|
||||
},
|
||||
{
|
||||
label: '其他附件',
|
||||
files: normalizeAttachmentFiles(row.attachmentFiles)
|
||||
}
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: '协议附件',
|
||||
files: normalizeAttachmentFiles(row.protocolAttachmentFiles)
|
||||
},
|
||||
{
|
||||
label: '知情同意书',
|
||||
files: normalizeAttachmentFiles(row.consentAttachmentFiles)
|
||||
},
|
||||
{
|
||||
label: '孕妇健康调查表',
|
||||
files: normalizeAttachmentFiles(row.healthSurveyAttachmentFiles)
|
||||
}
|
||||
]
|
||||
}
|
||||
+18
-1
@@ -80,6 +80,16 @@
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="sample-reception-card__meta-item sample-reception-card__meta-item--attachment">
|
||||
<dt>{{ consentAttachmentMeta(row).label }}</dt>
|
||||
<dd>
|
||||
<ProtocolAttachmentMiniReadonly
|
||||
:files="consentAttachmentMeta(row).files"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="activeTab === 'received'" class="sample-reception-card__meta-item">
|
||||
<dt>实验室入库编码</dt>
|
||||
<dd>
|
||||
@@ -242,7 +252,10 @@
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import QcWorkOrderProtocolPaymentPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue'
|
||||
import ProtocolAttachmentMiniReadonly from '@/components/business/protocol-attachment-mini-readonly/index.vue'
|
||||
import { getSourceProtocolAttachment } from '@/utils/protocol-source-attachment'
|
||||
import {
|
||||
getConsentProtocolAttachment,
|
||||
getSourceProtocolAttachment
|
||||
} from '@/utils/protocol-source-attachment'
|
||||
import { useAttachmentActions } from '@/composables/useAttachmentActions'
|
||||
|
||||
defineOptions({ name: 'SampleReceptionCardList' })
|
||||
@@ -284,6 +297,10 @@
|
||||
return getSourceProtocolAttachment(row)
|
||||
}
|
||||
|
||||
function consentAttachmentMeta(row: Record<string, unknown>) {
|
||||
return getConsentProtocolAttachment(row)
|
||||
}
|
||||
|
||||
function protocolStatusLabelBySource(status: string, source: string, voidType?: string): string {
|
||||
return source === 'research_donation'
|
||||
? donationStatusDisplayLabel(status, voidType)
|
||||
|
||||
@@ -204,41 +204,62 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="form-section-transport" class="sample-transport-col">
|
||||
<div class="section-title section-title--compact">
|
||||
<div class="section-title-left"><h3>运输信息</h3></div>
|
||||
<div id="form-section-transport" class="sample-transport-col sample-transport-col--stack">
|
||||
<div class="sample-transport-stack-block">
|
||||
<div class="section-title section-title--compact">
|
||||
<div class="section-title-left"><h3>运输信息</h3></div>
|
||||
</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输方式</div>
|
||||
<div class="data-value">
|
||||
{{ transportTypeLabel(String(currentRow.transportType || '')) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ formatDisplayOrDash(currentRow.etaTime) }}</div>
|
||||
</div>
|
||||
<template v-if="currentRow.transportType === 'kdys'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递公司</div>
|
||||
<div class="data-value">{{ currentRow.expressCompanyName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递单号</div>
|
||||
<div class="data-value">{{ currentRow.waybillNo || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="currentRow.transportType === 'ryxd'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人</div>
|
||||
<div class="data-value">{{ currentRow.carrierName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人电话</div>
|
||||
<div class="data-value">{{ currentRow.carrierPhone || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输方式</div>
|
||||
<div class="data-value">
|
||||
{{ transportTypeLabel(String(currentRow.transportType || '')) }}
|
||||
<div class="sample-transport-stack-block sample-protocol-attachments">
|
||||
<div class="section-title section-title--compact">
|
||||
<div class="section-title-left"><h3>协议附件</h3></div>
|
||||
</div>
|
||||
<div class="sample-protocol-attachments__list">
|
||||
<div
|
||||
v-for="group in receiveProtocolAttachmentGroups"
|
||||
:key="group.label"
|
||||
class="sample-protocol-attachments__item"
|
||||
>
|
||||
<div class="data-label">{{ group.label }}</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="group.files"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ formatDisplayOrDash(currentRow.etaTime) }}</div>
|
||||
</div>
|
||||
<template v-if="currentRow.transportType === 'kdys'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递公司</div>
|
||||
<div class="data-value">{{ currentRow.expressCompanyName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">快递单号</div>
|
||||
<div class="data-value">{{ currentRow.waybillNo || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="currentRow.transportType === 'ryxd'">
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人</div>
|
||||
<div class="data-value">{{ currentRow.carrierName || '-' }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">携带人电话</div>
|
||||
<div class="data-value">{{ currentRow.carrierPhone || '-' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -745,6 +766,7 @@
|
||||
buildPrefilledLabStorageCode,
|
||||
parseLabStorageReferenceDate
|
||||
} from '@/utils/sample-reception-lab-storage-code'
|
||||
import { getSourceProtocolAttachmentGroups } from '@/utils/protocol-source-attachment'
|
||||
import type { ProductionReceptionRecord } from '@/utils/sample-production-reception-fields'
|
||||
import {
|
||||
formatProductionFieldValue,
|
||||
@@ -955,6 +977,12 @@
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
const receiveProtocolAttachmentGroups = computed(() => {
|
||||
const row = currentRow.value
|
||||
if (!row) return []
|
||||
return getSourceProtocolAttachmentGroups(row)
|
||||
})
|
||||
|
||||
function formatMetric(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
const n = Number(v)
|
||||
@@ -1653,6 +1681,35 @@
|
||||
padding-left: 20px;
|
||||
border-left: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
&--stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.sample-transport-stack-block {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sample-protocol-attachments {
|
||||
background: #ecfdf5;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
.sample-protocol-attachments__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sample-protocol-attachments__item {
|
||||
.data-label {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
||||
Reference in new issue
Block a user