|
|
@@ -10,6 +10,7 @@ use app\common\library\ProcuremenStatus;
|
|
|
use app\common\library\ProcuremenTime;
|
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
|
use think\Config;
|
|
|
+use think\Cache;
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
|
|
@@ -239,6 +240,9 @@ class Procuremen extends Backend
|
|
|
'archiveabandon' => $this->hasProcuremenPerm(['archiveabandon', 'archiveAbandon', 'auditabandon', 'auditAbandon']),
|
|
|
'export_month_outward' => $this->hasProcuremenPerm(['export_month_outward', 'exportMonthOutward']),
|
|
|
'review' => $this->hasProcuremenPerm(['review', 'picksubmit', 'pickreview']),
|
|
|
+ 'bidopenusers' => $this->hasProcuremenPerm(['bidopenusers', 'bidopen', 'auditissue', 'auditsubmit']),
|
|
|
+ 'bidopensendcode' => $this->hasProcuremenPerm(['bidopensendcode', 'bidopen', 'auditissue', 'auditsubmit']),
|
|
|
+ 'bidopenverify' => $this->hasProcuremenPerm(['bidopenverify', 'bidopen', 'auditissue', 'auditsubmit']),
|
|
|
'reviewcompanies' => $this->hasProcuremenPerm(['reviewcompanies', 'reviewCompanies', 'pickreview', 'picksubmit', 'review']),
|
|
|
'add' => $this->hasProcuremenPerm(['add']),
|
|
|
'rfqlist' => $this->hasProcuremenPerm(['rfqlist']),
|
|
|
@@ -1911,20 +1915,20 @@ class Procuremen extends Backend
|
|
|
$amountFilled = !empty($line['amount_filled']);
|
|
|
$deliveryFilled = !empty($line['delivery_filled']);
|
|
|
|
|
|
- $line['unit_price_text'] = $amountFilled ? '未到截止时间' : '未填写';
|
|
|
+ $line['unit_price_text'] = $amountFilled ? '开标验证后查看' : '未填写';
|
|
|
$line['amount_show'] = '';
|
|
|
if (array_key_exists('amount_text', $line)) {
|
|
|
$line['amount_text'] = $line['unit_price_text'];
|
|
|
}
|
|
|
|
|
|
- $deliveryText = $deliveryFilled ? '未到截止时间' : '未填写';
|
|
|
+ $deliveryText = $deliveryFilled ? '开标验证后查看' : '未填写';
|
|
|
$line['delivery_show'] = $deliveryText;
|
|
|
if (array_key_exists('delivery_text', $line)) {
|
|
|
$line['delivery_text'] = $deliveryText;
|
|
|
}
|
|
|
$line['delivery_ymd'] = '';
|
|
|
|
|
|
- $line['subtotal_text'] = $amountFilled ? '未到截止时间' : '';
|
|
|
+ $line['subtotal_text'] = $amountFilled ? '' : '';
|
|
|
$line['subtotal_num'] = null;
|
|
|
|
|
|
$line['amount_quote_pending'] = $amountFilled ? 1 : 0;
|
|
|
@@ -1934,7 +1938,380 @@ class Procuremen extends Backend
|
|
|
$line['delivery_filled'] = false;
|
|
|
$line['is_quoted'] = false;
|
|
|
if (array_key_exists('quote_label', $line)) {
|
|
|
- $line['quote_label'] = ($amountFilled || $deliveryFilled) ? '未到截止时间' : '未报价';
|
|
|
+ $line['quote_label'] = ($amountFilled || $deliveryFilled) ? '未开标验证' : '未报价';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function ensurePurchaseOrderBidOpenTable(): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ Db::query('SELECT 1 FROM `purchase_order_bid_open` LIMIT 1');
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Db::execute(
|
|
|
+ "CREATE TABLE IF NOT EXISTS `purchase_order_bid_open` (
|
|
|
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
|
|
+ `ccydh` varchar(64) NOT NULL DEFAULT '' COMMENT '订单号',
|
|
|
+ `verifier1_id` int(10) unsigned NOT NULL DEFAULT 0,
|
|
|
+ `verifier1_name` varchar(64) NOT NULL DEFAULT '',
|
|
|
+ `verifier2_id` int(10) unsigned NOT NULL DEFAULT 0,
|
|
|
+ `verifier2_name` varchar(64) NOT NULL DEFAULT '',
|
|
|
+ `operator_id` int(10) unsigned NOT NULL DEFAULT 0,
|
|
|
+ `operator_name` varchar(64) NOT NULL DEFAULT '',
|
|
|
+ `createtime` datetime DEFAULT NULL,
|
|
|
+ PRIMARY KEY (`id`),
|
|
|
+ UNIQUE KEY `uk_ccydh` (`ccydh`)
|
|
|
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='协助采购开标双重验证'"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function isProcuremenBidOpenVerified(string $ccydh): bool
|
|
|
+ {
|
|
|
+ $ccydh = trim($ccydh);
|
|
|
+ if ($ccydh === '') {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $this->ensurePurchaseOrderBidOpenTable();
|
|
|
+ try {
|
|
|
+ $row = Db::table('purchase_order_bid_open')->where('ccydh', $ccydh)->find();
|
|
|
+
|
|
|
+ return is_array($row);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array{ccydh?:string} $bundle
|
|
|
+ */
|
|
|
+ protected function isProcuremenBidOpenVerifiedForBundle(array $bundle): bool
|
|
|
+ {
|
|
|
+ return $this->isProcuremenBidOpenVerified(trim((string)($bundle['ccydh'] ?? '')));
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function canViewProcuremenSupplierQuotesForBundle(array $bundle): bool
|
|
|
+ {
|
|
|
+ return $this->isProcuremenBidOpenVerifiedForBundle($bundle);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function loadBidOpenAdminUser(int $adminId): ?array
|
|
|
+ {
|
|
|
+ if ($adminId <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $row = Db::name('admin')->where('id', $adminId)->find();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!is_array($row)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $status = strtolower(trim((string)($row['status'] ?? '')));
|
|
|
+ if ($status !== '' && $status !== 'normal') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!$this->isBidOpenVerifierAdmin((int)($row['id'] ?? 0))) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $phone = preg_replace('/\s+/', '', trim((string)($row['mobile'] ?? '')));
|
|
|
+ if (!preg_match('/^1\d{10}$/', $phone)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $name = trim((string)($row['nickname'] ?? ''));
|
|
|
+ if ($name === '') {
|
|
|
+ $name = trim((string)($row['username'] ?? ''));
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'id' => (int)$row['id'],
|
|
|
+ 'username' => trim((string)($row['username'] ?? '')),
|
|
|
+ 'nickname' => $name,
|
|
|
+ 'mobile' => $phone,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开标验证人可选范围:auth_group 根组 id(含其全部子组)
|
|
|
+ */
|
|
|
+ protected function bidOpenVerifierAuthGroupRootId(): int
|
|
|
+ {
|
|
|
+ $id = (int)Config::get('mproc.bid_open_auth_group_root_id');
|
|
|
+
|
|
|
+ return $id > 0 ? $id : 10;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return int[]
|
|
|
+ */
|
|
|
+ protected function loadBidOpenVerifierAuthGroupIds(): array
|
|
|
+ {
|
|
|
+ static $memo = [];
|
|
|
+ $rootId = $this->bidOpenVerifierAuthGroupRootId();
|
|
|
+ if (isset($memo[$rootId])) {
|
|
|
+ return $memo[$rootId];
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $groups = Db::name('auth_group')->where('status', 'normal')->field('id,pid')->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $memo[$rootId] = [$rootId];
|
|
|
+
|
|
|
+ return $memo[$rootId];
|
|
|
+ }
|
|
|
+ if (!is_array($groups) || $groups === []) {
|
|
|
+ $memo[$rootId] = [$rootId];
|
|
|
+
|
|
|
+ return $memo[$rootId];
|
|
|
+ }
|
|
|
+ $groupList = [];
|
|
|
+ foreach ($groups as $g) {
|
|
|
+ if (is_array($g)) {
|
|
|
+ $groupList[] = $g;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($groupList === []) {
|
|
|
+ $memo[$rootId] = [$rootId];
|
|
|
+
|
|
|
+ return $memo[$rootId];
|
|
|
+ }
|
|
|
+ $tree = \fast\Tree::instance();
|
|
|
+ $tree->init($groupList);
|
|
|
+ $ids = $tree->getChildrenIds($rootId, true);
|
|
|
+ if (!is_array($ids) || $ids === []) {
|
|
|
+ $memo[$rootId] = [$rootId];
|
|
|
+
|
|
|
+ return $memo[$rootId];
|
|
|
+ }
|
|
|
+ $memo[$rootId] = array_values(array_unique(array_filter(array_map('intval', $ids))));
|
|
|
+
|
|
|
+ return $memo[$rootId];
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function isBidOpenVerifierAdmin(int $adminId): bool
|
|
|
+ {
|
|
|
+ if ($adminId <= 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $groupIds = $this->loadBidOpenVerifierAuthGroupIds();
|
|
|
+ if ($groupIds === []) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return (bool)Db::name('auth_group_access')
|
|
|
+ ->where('uid', $adminId)
|
|
|
+ ->where('group_id', 'in', $groupIds)
|
|
|
+ ->count();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return array<int, array<string, mixed>>
|
|
|
+ */
|
|
|
+ protected function listBidOpenVerifierCandidates(): array
|
|
|
+ {
|
|
|
+ $groupIds = $this->loadBidOpenVerifierAuthGroupIds();
|
|
|
+ if ($groupIds === []) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $adminIds = Db::name('auth_group_access')
|
|
|
+ ->where('group_id', 'in', $groupIds)
|
|
|
+ ->column('uid');
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $adminIds = [];
|
|
|
+ }
|
|
|
+ $adminIds = is_array($adminIds)
|
|
|
+ ? array_values(array_unique(array_filter(array_map('intval', $adminIds))))
|
|
|
+ : [];
|
|
|
+ if ($adminIds === []) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $rows = Db::name('admin')
|
|
|
+ ->where('status', 'normal')
|
|
|
+ ->where('id', 'in', $adminIds)
|
|
|
+ ->where('mobile', '<>', '')
|
|
|
+ ->order('id', 'asc')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ if (!is_array($rows)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $groupNameMap = $this->loadBidOpenVerifierGroupNameMap($adminIds);
|
|
|
+ $out = [];
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ if (!is_array($row)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $one = $this->loadBidOpenAdminUser((int)($row['id'] ?? 0));
|
|
|
+ if ($one === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $out[] = [
|
|
|
+ 'id' => $one['id'],
|
|
|
+ 'username' => $one['username'],
|
|
|
+ 'nickname' => $one['nickname'],
|
|
|
+ 'group_name' => $groupNameMap[$one['id']] ?? '',
|
|
|
+ 'mobile' => mask_phone($one['mobile']),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param int[] $adminIds
|
|
|
+ * @return array<int, string>
|
|
|
+ */
|
|
|
+ protected function loadBidOpenVerifierGroupNameMap(array $adminIds): array
|
|
|
+ {
|
|
|
+ $adminIds = array_values(array_unique(array_filter(array_map('intval', $adminIds))));
|
|
|
+ if ($adminIds === []) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $groupIds = $this->loadBidOpenVerifierAuthGroupIds();
|
|
|
+ try {
|
|
|
+ $rows = Db::name('auth_group_access')
|
|
|
+ ->alias('aga')
|
|
|
+ ->join('auth_group ag', 'aga.group_id = ag.id')
|
|
|
+ ->where('aga.uid', 'in', $adminIds)
|
|
|
+ ->where('aga.group_id', 'in', $groupIds)
|
|
|
+ ->field('aga.uid,ag.name')
|
|
|
+ ->order('aga.uid', 'asc')
|
|
|
+ ->order('aga.group_id', 'asc')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ if (!is_array($rows)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $out = [];
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ if (!is_array($row)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $uid = (int)($row['uid'] ?? 0);
|
|
|
+ $name = trim((string)($row['name'] ?? ''));
|
|
|
+ if ($uid <= 0 || $name === '' || isset($out[$uid])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $out[$uid] = $name;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function bidOpenSmsCacheKey(int $adminId, string $ccydh): string
|
|
|
+ {
|
|
|
+ return 'procuremen_bidopen_code_' . $adminId . '_' . md5($ccydh);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function bidOpenSmsWaitCacheKey(int $adminId): string
|
|
|
+ {
|
|
|
+ return 'procuremen_bidopen_wait_' . $adminId;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function renderBidOpenVerifySms(string $code): string
|
|
|
+ {
|
|
|
+ $tpl = '';
|
|
|
+ try {
|
|
|
+ $row = $this->loadNotifyTemplateRow('bid_open');
|
|
|
+ if (is_array($row)) {
|
|
|
+ $tpl = trim((string)($row['content'] ?? ''));
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $tpl = '';
|
|
|
+ }
|
|
|
+ if ($tpl === '') {
|
|
|
+ $tpl = '【可集达】您的短信验证码是{code}';
|
|
|
+ }
|
|
|
+
|
|
|
+ return str_replace('{code}', $code, $tpl);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function sendBidOpenVerifySms(string $phone, string $code): void
|
|
|
+ {
|
|
|
+ $this->smsbao($phone, $this->renderBidOpenVerifySms($code));
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function verifyBidOpenSmsCode(int $adminId, string $ccydh, string $code): bool
|
|
|
+ {
|
|
|
+ $code = trim($code);
|
|
|
+ if (!preg_match('/^\d{6}$/', $code)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ $mock = Config::get('mproc.mock_sms_code');
|
|
|
+ if ($mock !== null && $mock !== '' && (string)$mock === $code) {
|
|
|
+ Cache::rm($this->bidOpenSmsCacheKey($adminId, $ccydh));
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ $cached = Cache::get($this->bidOpenSmsCacheKey($adminId, $ccydh));
|
|
|
+ if ($cached === false || $cached === null || (string)$cached !== $code) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ Cache::rm($this->bidOpenSmsCacheKey($adminId, $ccydh));
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array{ccydh?:string,pos?:array,merge_rows?:array} $bundle
|
|
|
+ */
|
|
|
+ protected function recordProcuremenBidOpenVerification(array $bundle, array $verifier1, array $verifier2): void
|
|
|
+ {
|
|
|
+ $ccydh = trim((string)($bundle['ccydh'] ?? ''));
|
|
|
+ if ($ccydh === '') {
|
|
|
+ throw new \InvalidArgumentException('订单号无效');
|
|
|
+ }
|
|
|
+ $this->ensurePurchaseOrderBidOpenTable();
|
|
|
+ list($operatorId, $operatorName) = $this->GetUseName();
|
|
|
+ $now = date('Y-m-d H:i:s');
|
|
|
+ $exists = Db::table('purchase_order_bid_open')->where('ccydh', $ccydh)->find();
|
|
|
+ $payload = [
|
|
|
+ 'ccydh' => $ccydh,
|
|
|
+ 'verifier1_id' => (int)($verifier1['id'] ?? 0),
|
|
|
+ 'verifier1_name' => trim((string)($verifier1['nickname'] ?? '')),
|
|
|
+ 'verifier2_id' => (int)($verifier2['id'] ?? 0),
|
|
|
+ 'verifier2_name' => trim((string)($verifier2['nickname'] ?? '')),
|
|
|
+ 'operator_id' => (int)$operatorId,
|
|
|
+ 'operator_name' => trim((string)$operatorName),
|
|
|
+ 'createtime' => $now,
|
|
|
+ ];
|
|
|
+ if (is_array($exists)) {
|
|
|
+ Db::table('purchase_order_bid_open')->where('ccydh', $ccydh)->update($payload);
|
|
|
+ } else {
|
|
|
+ Db::table('purchase_order_bid_open')->insert($payload);
|
|
|
+ }
|
|
|
+
|
|
|
+ $name1 = $payload['verifier1_name'] !== '' ? $payload['verifier1_name'] : ('ID' . $payload['verifier1_id']);
|
|
|
+ $name2 = $payload['verifier2_name'] !== '' ? $payload['verifier2_name'] : ('ID' . $payload['verifier2_id']);
|
|
|
+ $orderPart = '(订单' . $ccydh . ')';
|
|
|
+ $logMsg = '开标验证' . $orderPart . ':' . $name1 . '、' . $name2 . ' 于 ' . $now . ' 完成双重验证,可查看供应商报价信息';
|
|
|
+
|
|
|
+ $seenSid = [];
|
|
|
+ foreach ($bundle['pos'] ?? [] as $poRow) {
|
|
|
+ if (!is_array($poRow)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $sid = (int)($poRow['scydgy_id'] ?? 0);
|
|
|
+ if (!$this->isValidScydgyRowId($sid) || isset($seenSid[$sid])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $seenSid[$sid] = true;
|
|
|
+ $poIdLog = (int)($poRow['id'] ?? $poRow['ID'] ?? 0);
|
|
|
+ $this->addOrderLog($sid, 'bid_open_verify', $logMsg, $poIdLog > 0 ? $poIdLog : null);
|
|
|
+ }
|
|
|
+ if ($seenSid === []) {
|
|
|
+ $anchor = (int)($this->request->post('scydgy_id', $this->request->param('ids', 0)));
|
|
|
+ if ($this->isValidScydgyRowId($anchor)) {
|
|
|
+ $this->addOrderLog($anchor, 'bid_open_verify', $logMsg, null);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1986,7 +2363,7 @@ class Procuremen extends Backend
|
|
|
if ($sids === []) {
|
|
|
return [];
|
|
|
}
|
|
|
- $deadlineReached = $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle));
|
|
|
+ $quoteVisible = $this->canViewProcuremenSupplierQuotesForBundle($bundle);
|
|
|
$gymcMap = [];
|
|
|
$qtyBySid = [];
|
|
|
foreach ($bundle['merge_rows'] ?? [] as $mr) {
|
|
|
@@ -2094,7 +2471,7 @@ class Procuremen extends Backend
|
|
|
'is_quoted' => $amountFilled,
|
|
|
'quote_label' => $amountFilled ? '已报价' : '未报价',
|
|
|
];
|
|
|
- if (!$deadlineReached) {
|
|
|
+ if (!$quoteVisible) {
|
|
|
$this->maskSupplierQuoteLineBeforeDeadline($lineRow);
|
|
|
}
|
|
|
$byCompany[$cn]['lines'][] = $this->ensureSupplierQuoteLineDisplayKeys($lineRow);
|
|
|
@@ -2104,7 +2481,7 @@ class Procuremen extends Backend
|
|
|
$quoted = 0;
|
|
|
$groupTotal = 0.0;
|
|
|
$groupHasTotal = false;
|
|
|
- if ($deadlineReached) {
|
|
|
+ if ($quoteVisible) {
|
|
|
foreach ($g['lines'] as $ln) {
|
|
|
$am = trim((string)($ln['amount'] ?? ''));
|
|
|
if ($am !== '' && $am !== '0' && $am !== '0.00') {
|
|
|
@@ -2116,10 +2493,10 @@ class Procuremen extends Backend
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- $byCompany[$cn]['has_quote'] = $deadlineReached && $total > 0 && $quoted === $total;
|
|
|
+ $byCompany[$cn]['has_quote'] = $quoteVisible && $total > 0 && $quoted === $total;
|
|
|
$byCompany[$cn]['line_count'] = $total;
|
|
|
$byCompany[$cn]['has_total'] = $total > 0;
|
|
|
- $byCompany[$cn]['total_text'] = ($deadlineReached && $groupHasTotal) ? $this->formatProcuremenMoneyDisplay($groupTotal) : '';
|
|
|
+ $byCompany[$cn]['total_text'] = ($quoteVisible && $groupHasTotal) ? $this->formatProcuremenMoneyDisplay($groupTotal) : '';
|
|
|
$byCompany[$cn]['display_rowspan'] = $total + ($total > 0 ? 1 : 0);
|
|
|
}
|
|
|
|
|
|
@@ -2149,7 +2526,7 @@ class Procuremen extends Backend
|
|
|
if ($sids === []) {
|
|
|
return [];
|
|
|
}
|
|
|
- $deadlineReached = $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle));
|
|
|
+ $quoteVisible = $this->canViewProcuremenSupplierQuotesForBundle($bundle);
|
|
|
$gymcMap = [];
|
|
|
$qtyBySid = [];
|
|
|
$orderedSids = [];
|
|
|
@@ -2260,7 +2637,7 @@ class Procuremen extends Backend
|
|
|
'subtotal_num' => $subtotal,
|
|
|
'is_quoted' => $amountFilled,
|
|
|
];
|
|
|
- if (!$deadlineReached) {
|
|
|
+ if (!$quoteVisible) {
|
|
|
$this->maskSupplierQuoteLineBeforeDeadline($pickLine);
|
|
|
}
|
|
|
$byCompany[$cn]['pick_lines'][$sid] = $this->ensureSupplierQuoteLineDisplayKeys($pickLine);
|
|
|
@@ -2294,7 +2671,7 @@ class Procuremen extends Backend
|
|
|
}
|
|
|
$groupTotal = 0.0;
|
|
|
$groupHasTotal = false;
|
|
|
- if ($deadlineReached) {
|
|
|
+ if ($quoteVisible) {
|
|
|
foreach ($pickLines as $ln) {
|
|
|
if (isset($ln['subtotal_num']) && $ln['subtotal_num'] !== null) {
|
|
|
$groupTotal += (float)$ln['subtotal_num'];
|
|
|
@@ -2306,7 +2683,7 @@ class Procuremen extends Backend
|
|
|
$g['pick_lines'] = $pickLines;
|
|
|
$g['line_count'] = $lineCount;
|
|
|
$g['has_total'] = $lineCount > 0;
|
|
|
- $g['total_text'] = ($deadlineReached && $groupHasTotal) ? $this->formatProcuremenMoneyDisplay($groupTotal) : '';
|
|
|
+ $g['total_text'] = ($quoteVisible && $groupHasTotal) ? $this->formatProcuremenMoneyDisplay($groupTotal) : '';
|
|
|
$g['display_rowspan'] = $lineCount + ($lineCount > 0 ? 1 : 0);
|
|
|
$detailPicks = $g['detail_picks'] ?? [];
|
|
|
$g['detail_picks'] = array_values(is_array($detailPicks) ? $detailPicks : []);
|
|
|
@@ -4921,7 +5298,8 @@ class Procuremen extends Backend
|
|
|
protected function buildProcuremenDetailsQuoteView(array $mergeRows, array $details, array $main): array
|
|
|
{
|
|
|
$processRows = $this->buildAuditProcessDisplayRows($mergeRows);
|
|
|
- $deadlineReached = $this->isProcuremenQuoteDeadlineReached($main['sys_rq'] ?? '');
|
|
|
+ $ccydh = trim((string)($main['CCYDH'] ?? ''));
|
|
|
+ $quoteVisible = $this->isProcuremenBidOpenVerified($ccydh);
|
|
|
$qtyBySid = [];
|
|
|
$gymcBySid = [];
|
|
|
$orderedSids = [];
|
|
|
@@ -4972,7 +5350,7 @@ class Procuremen extends Backend
|
|
|
&$groupVoid,
|
|
|
$qtyBySid,
|
|
|
$gymcBySid,
|
|
|
- $deadlineReached
|
|
|
+ $quoteVisible
|
|
|
) {
|
|
|
$isVoid = $this->isPurchaseOrderDetailVoid($d);
|
|
|
if (!$isVoid) {
|
|
|
@@ -5002,7 +5380,7 @@ class Procuremen extends Backend
|
|
|
'is_void' => $isVoid,
|
|
|
'oper_time_text' => $ct,
|
|
|
];
|
|
|
- if (!$deadlineReached && !$isVoid) {
|
|
|
+ if (!$quoteVisible && !$isVoid) {
|
|
|
$this->maskSupplierQuoteLineBeforeDeadline($line);
|
|
|
} elseif ($sub !== null) {
|
|
|
$groupTotal += $sub;
|
|
|
@@ -5046,7 +5424,7 @@ class Procuremen extends Backend
|
|
|
'lines' => $lines,
|
|
|
'line_count' => $lineCount,
|
|
|
'display_rowspan'=> $lineCount + ($lineCount > 0 ? 1 : 0),
|
|
|
- 'total_text' => ($deadlineReached && $groupHas) ? $this->formatProcuremenMoneyDisplay($groupTotal) : '',
|
|
|
+ 'total_text' => ($quoteVisible && $groupHas) ? $this->formatProcuremenMoneyDisplay($groupTotal) : '',
|
|
|
'has_total' => $lineCount > 0,
|
|
|
];
|
|
|
}
|
|
|
@@ -6510,11 +6888,145 @@ class Procuremen extends Backend
|
|
|
$this->view->assign('quoteGroupsJson', json_encode($this->maskSupplierContactList($quoteGroups), JSON_UNESCAPED_UNICODE));
|
|
|
$this->view->assign('pickedCompanyName', $pickedName);
|
|
|
$this->view->assign('sysRq', $this->formatProcuremenSysRqForInput($po['sys_rq'] ?? null));
|
|
|
- $this->view->assign('quoteDeadlineReached', $this->isProcuremenQuoteDeadlineReached($this->resolveBundleSysRq($bundle)) ? 1 : 0);
|
|
|
+ $this->view->assign('bidOpenVerified', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
|
|
|
+ $this->view->assign('quoteDeadlineReached', $this->isProcuremenBidOpenVerifiedForBundle($bundle) ? 1 : 0);
|
|
|
|
|
|
return $this->view->fetch('procuremen/audit_issue');
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 开标双重验证 — 可选验证人列表
|
|
|
+ */
|
|
|
+ public function bidOpenUsers()
|
|
|
+ {
|
|
|
+ if (!$this->hasProcuremenPerm(['bidopenusers', 'bidopen', 'auditissue', 'auditsubmit'])) {
|
|
|
+ $this->error('无权限');
|
|
|
+ }
|
|
|
+ $scydgyId = trim((string)$this->request->param('scydgy_id', $this->request->param('ids', '')));
|
|
|
+ if ($scydgyId !== '') {
|
|
|
+ $bundle = $this->loadAuditOrderBundleByScydgyId($scydgyId);
|
|
|
+ if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
|
|
|
+ $this->success('已完成开标验证', null, [
|
|
|
+ 'verified' => 1,
|
|
|
+ 'users' => [],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $this->success('', null, [
|
|
|
+ 'verified' => 0,
|
|
|
+ 'users' => $this->listBidOpenVerifierCandidates(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开标双重验证 — 向指定管理员发送短信验证码
|
|
|
+ */
|
|
|
+ public function bidOpenSendCode()
|
|
|
+ {
|
|
|
+ if (!$this->request->isPost()) {
|
|
|
+ $this->error(__('Invalid parameters'));
|
|
|
+ }
|
|
|
+ if (!$this->hasProcuremenPerm(['bidopensendcode', 'bidopen', 'auditissue', 'auditsubmit'])) {
|
|
|
+ $this->error('无权限');
|
|
|
+ }
|
|
|
+ $scydgyId = trim((string)$this->request->post('scydgy_id', ''));
|
|
|
+ $adminId = (int)$this->request->post('admin_id', 0);
|
|
|
+ if ($scydgyId === '' || $adminId <= 0) {
|
|
|
+ $this->error('参数无效');
|
|
|
+ }
|
|
|
+ $bundle = $this->loadAuditOrderBundleByScydgyId($scydgyId);
|
|
|
+ if ($bundle['pos'] === []) {
|
|
|
+ $this->error('该订单不在待确认供应商状态');
|
|
|
+ }
|
|
|
+ if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
|
|
|
+ $this->success('已完成开标验证,无需重复发送');
|
|
|
+ }
|
|
|
+ $ccydh = trim((string)($bundle['ccydh'] ?? ''));
|
|
|
+ if ($ccydh === '') {
|
|
|
+ $this->error('订单号无效');
|
|
|
+ }
|
|
|
+ $admin = $this->loadBidOpenAdminUser($adminId);
|
|
|
+ if ($admin === null) {
|
|
|
+ $this->error('验证人无效或未绑定手机号');
|
|
|
+ }
|
|
|
+ $cd = (int)(Config::get('mproc.sms_resend_cd') ?: 55);
|
|
|
+ $cd = max(30, min(120, $cd));
|
|
|
+ if (Cache::get($this->bidOpenSmsWaitCacheKey($adminId))) {
|
|
|
+ $this->error('发送过于频繁,请稍后再试');
|
|
|
+ }
|
|
|
+ $code = (string)random_int(100000, 999999);
|
|
|
+ $ttl = (int)(Config::get('mproc.sms_code_ttl') ?: 300);
|
|
|
+ $ttl = max(60, min(600, $ttl));
|
|
|
+ Cache::set($this->bidOpenSmsCacheKey($adminId, $ccydh), $code, $ttl);
|
|
|
+ Cache::set($this->bidOpenSmsWaitCacheKey($adminId), 1, $cd);
|
|
|
+ try {
|
|
|
+ $this->sendBidOpenVerifySms($admin['mobile'], $code);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Cache::rm($this->bidOpenSmsCacheKey($adminId, $ccydh));
|
|
|
+ Cache::rm($this->bidOpenSmsWaitCacheKey($adminId));
|
|
|
+ $this->error($e->getMessage());
|
|
|
+ }
|
|
|
+ $this->success('验证码已发送至「' . ($admin['nickname'] !== '' ? $admin['nickname'] : $admin['username']) . '」');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开标双重验证 — 两人验证码校验并记录
|
|
|
+ */
|
|
|
+ public function bidOpenVerify()
|
|
|
+ {
|
|
|
+ if (!$this->request->isPost()) {
|
|
|
+ $this->error(__('Invalid parameters'));
|
|
|
+ }
|
|
|
+ if (!$this->hasProcuremenPerm(['bidopenverify', 'bidopen', 'auditissue', 'auditsubmit'])) {
|
|
|
+ $this->error('无权限');
|
|
|
+ }
|
|
|
+ $scydgyId = trim((string)$this->request->post('scydgy_id', ''));
|
|
|
+ $verifier1Id = (int)$this->request->post('verifier1_id', 0);
|
|
|
+ $verifier2Id = (int)$this->request->post('verifier2_id', 0);
|
|
|
+ $code1 = trim((string)$this->request->post('code1', ''));
|
|
|
+ $code2 = trim((string)$this->request->post('code2', ''));
|
|
|
+ if ($scydgyId === '') {
|
|
|
+ $this->error('参数无效');
|
|
|
+ }
|
|
|
+ if ($verifier1Id <= 0 || $verifier2Id <= 0) {
|
|
|
+ $this->error('请选择两位验证人');
|
|
|
+ }
|
|
|
+ if ($verifier1Id === $verifier2Id) {
|
|
|
+ $this->error('两位验证人不能相同');
|
|
|
+ }
|
|
|
+ if (!preg_match('/^\d{6}$/', $code1) || !preg_match('/^\d{6}$/', $code2)) {
|
|
|
+ $this->error('请输入6位短信验证码');
|
|
|
+ }
|
|
|
+ $bundle = $this->loadAuditOrderBundleByScydgyId($scydgyId);
|
|
|
+ if ($bundle['pos'] === []) {
|
|
|
+ $this->error('该订单不在待确认供应商状态');
|
|
|
+ }
|
|
|
+ $ccydh = trim((string)($bundle['ccydh'] ?? ''));
|
|
|
+ if ($ccydh === '') {
|
|
|
+ $this->error('订单号无效');
|
|
|
+ }
|
|
|
+ if ($this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
|
|
|
+ $this->success('已完成开标验证');
|
|
|
+ }
|
|
|
+ $verifier1 = $this->loadBidOpenAdminUser($verifier1Id);
|
|
|
+ $verifier2 = $this->loadBidOpenAdminUser($verifier2Id);
|
|
|
+ if ($verifier1 === null || $verifier2 === null) {
|
|
|
+ $this->error('验证人无效或未绑定手机号');
|
|
|
+ }
|
|
|
+ if (!$this->verifyBidOpenSmsCode($verifier1Id, $ccydh, $code1)) {
|
|
|
+ $this->error('验证人「' . ($verifier1['nickname'] !== '' ? $verifier1['nickname'] : $verifier1['username']) . '」验证码不正确或已过期');
|
|
|
+ }
|
|
|
+ if (!$this->verifyBidOpenSmsCode($verifier2Id, $ccydh, $code2)) {
|
|
|
+ $this->error('验证人「' . ($verifier2['nickname'] !== '' ? $verifier2['nickname'] : $verifier2['username']) . '」验证码不正确或已过期');
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $this->recordProcuremenBidOpenVerification($bundle, $verifier1, $verifier2);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $this->error($e->getMessage());
|
|
|
+ }
|
|
|
+ $this->success('开标验证成功,可查看供应商报价与交货日期');
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 确认供应商提交(选定唯一供应商,进入采购终审;默认不发短信)
|
|
|
*/
|
|
|
@@ -6557,8 +7069,8 @@ class Procuremen extends Backend
|
|
|
}
|
|
|
|
|
|
$sysRqDb = $this->parseProcuremenSysRqInput(trim((string)$this->request->post('sys_rq', '')), true);
|
|
|
- if (!$this->isProcuremenQuoteDeadlineReached($sysRqDb)) {
|
|
|
- $this->error('报价截止时间未到,暂不能确认供应商');
|
|
|
+ if (!$this->isProcuremenBidOpenVerifiedForBundle($bundle)) {
|
|
|
+ $this->error('请先完成开标双重验证后再确认供应商');
|
|
|
}
|
|
|
|
|
|
$ccydhLog = trim((string)($bundle['ccydh'] ?? ''));
|
|
|
@@ -8136,6 +8648,7 @@ class Procuremen extends Backend
|
|
|
'review_sms' => '协助下发-短信',
|
|
|
'confirm_ok' => '采购确认-通过',
|
|
|
'confirm_fail' => '采购确认-未通过',
|
|
|
+ 'bid_open' => '开标双重验证',
|
|
|
];
|
|
|
$label = $map[$scene] ?? $scene;
|
|
|
if ($titleRequired) {
|