|
|
@@ -10,14 +10,17 @@ use think\Log;
|
|
|
*
|
|
|
* 表分工:
|
|
|
* - supplier_service_score:订单明细(开标后按 ccydh×供应商写入,含本单总分 score)
|
|
|
- * - supplier_service_final_score:月度评审表(开标后写入本月询价供应商名称;分项与等级人工填写,已存在不覆盖)
|
|
|
+ * - supplier_service_final_score:月度评审表(四项自动统计,满分 105;qp_manual=1 手工锁定不覆盖)
|
|
|
*
|
|
|
- * 总分 = 质量得分×质量% + 价格得分 [+ 交货得分×交货%]
|
|
|
- * 价格得分 = (本单最低单价合计 / 本供应商单价合计) × 价格权重% × 100
|
|
|
- * (计算过程保留四位小数,得分保留两位)
|
|
|
- * 质量得分 = 月度评审表「质量得分」(当月按上月起往前查,都无则默认 50)
|
|
|
- * 权重优先取 purchase_order 下发时快照(score_*_weight);无快照再按 score_rule_id 读规则表;再否则 is_default=1
|
|
|
- * 开标验证通过后写入订单明细评分,并补齐月度评审供应商名称
|
|
|
+ * 开标单笔总分 = 质量得分×质量% + 价格得分 [+ 交货得分×交货%]
|
|
|
+ * 开标价格得分 = (本单最低单价合计 / 本供应商单价合计) × 价格权重% × 100
|
|
|
+ *
|
|
|
+ * 月度评审(与开标单笔分离):
|
|
|
+ * 质量40 = 合格率对照(20) + 客户投诉(10) + 生产中断(10)
|
|
|
+ * 价格15 = 全部严格低于最高限价→15,否则(等于限价/限价空)→10
|
|
|
+ * 交货40 = 准时率对照(30) + 生产中断(10,出现一次得0)
|
|
|
+ * 增值10 = 未报价订单数≤3→10,>3→0(按订单号去重)
|
|
|
+ * 最终得分 = 四项相加;等级 A≥90 / B≥70 / C≥60 / D
|
|
|
*/
|
|
|
class ProcuremenSupplierScore
|
|
|
{
|
|
|
@@ -555,7 +558,7 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 合格率百分比 → 质量得分(图三对照)
|
|
|
+ * 合格率百分比 → 质量「合格率」分项(满分 20)
|
|
|
* 100→20;95-99→18;90-94→16;85-89→14;70-84→12;<70→10
|
|
|
*/
|
|
|
public static function mapPassRatePercentToQualityScore(float $passRatePercent): float
|
|
|
@@ -580,51 +583,81 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 按入库评分统计某供应商某月质量得分
|
|
|
- * 不合格率=(不合格订单数/总订单数)×100%;合格率=100%-不合格率;再对照得分表
|
|
|
- * 同一订单号多工序只计 1 单;任一工序不合格则整单不合格
|
|
|
- * 优先读入库评分表;表空时回退操作日志(入库评分)
|
|
|
+ * 交货准时率百分比 → 交货「准时率」分项(满分 30)
|
|
|
+ * 100→30;95-99→20;90-94→15;85-89→10;70-84→5;<70→0
|
|
|
+ */
|
|
|
+ public static function mapOnTimeRatePercentToDeliveryScore(float $onTimeRatePercent): float
|
|
|
+ {
|
|
|
+ if ($onTimeRatePercent >= 100) {
|
|
|
+ return 30.0;
|
|
|
+ }
|
|
|
+ if ($onTimeRatePercent >= 95) {
|
|
|
+ return 20.0;
|
|
|
+ }
|
|
|
+ if ($onTimeRatePercent >= 90) {
|
|
|
+ return 15.0;
|
|
|
+ }
|
|
|
+ if ($onTimeRatePercent >= 85) {
|
|
|
+ return 10.0;
|
|
|
+ }
|
|
|
+ if ($onTimeRatePercent >= 70) {
|
|
|
+ return 5.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析金额/限价;空或非数字返回 null
|
|
|
*
|
|
|
- * @return float|null 无入库评分订单时返回 null
|
|
|
+ * @param mixed $raw
|
|
|
*/
|
|
|
- public static function calcQualityScoreFromInbound(string $companyName, string $ym): ?float
|
|
|
+ public static function parseMoneyValue($raw): ?float
|
|
|
{
|
|
|
- $companyName = trim($companyName);
|
|
|
- $ym = self::formatScoreYm($ym);
|
|
|
- if ($companyName === '' || $ym === '') {
|
|
|
+ if ($raw === null) {
|
|
|
return null;
|
|
|
}
|
|
|
- $byOrder = self::loadInboundOrderResultsByCompanyYm($companyName, $ym);
|
|
|
- $total = count($byOrder);
|
|
|
- if ($total < 1) {
|
|
|
+ $s = trim((string)$raw);
|
|
|
+ if ($s === '') {
|
|
|
return null;
|
|
|
}
|
|
|
- $fail = 0;
|
|
|
- foreach ($byOrder as $res) {
|
|
|
- if ($res === '不合格') {
|
|
|
- $fail++;
|
|
|
- }
|
|
|
+ $s = str_replace([',', ',', ' '], '', $s);
|
|
|
+ if ($s === '' || !is_numeric($s)) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- // 不合格率 = 不合格次数/总订单×100%;合格率 = 100% - 不合格率
|
|
|
- $passRate = (1 - ($fail / $total)) * 100;
|
|
|
|
|
|
- return self::mapPassRatePercentToQualityScore($passRate);
|
|
|
+ return (float)$s;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 某供应商某月:订单号 => 合格|不合格
|
|
|
+ * 报价是否视为未填(空 / 0 / 0.00)
|
|
|
*
|
|
|
- * @return array<string, string>
|
|
|
+ * @param mixed $raw
|
|
|
*/
|
|
|
- public static function loadInboundOrderResultsByCompanyYm(string $companyName, string $ym): array
|
|
|
+ public static function isBlankQuoteAmount($raw): bool
|
|
|
+ {
|
|
|
+ $v = self::parseMoneyValue($raw);
|
|
|
+ if ($v === null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return abs($v) < 0.0000001;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 某供应商某月入库评分订单聚合(按订单号去重)
|
|
|
+ *
|
|
|
+ * @return array<string, array{result:string,delivery_status:string,customer_complaint:int,order_interrupt:int}>
|
|
|
+ */
|
|
|
+ public static function loadInboundOrderAggByCompanyYm(string $companyName, string $ym): array
|
|
|
{
|
|
|
$companyName = trim($companyName);
|
|
|
$ym = self::formatScoreYm($ym);
|
|
|
if ($companyName === '' || $ym === '') {
|
|
|
return [];
|
|
|
}
|
|
|
+ /** @var array<string, array{result:string,delivery_status:string,customer_complaint:int,order_interrupt:int}> $byOrder */
|
|
|
$byOrder = [];
|
|
|
- // 1) 入库评分表
|
|
|
try {
|
|
|
$rows = Db::table('purchase_order_inbound_score')
|
|
|
->where('company_name', $companyName)
|
|
|
@@ -632,11 +665,22 @@ class ProcuremenSupplierScore
|
|
|
$q->where('updatetime', 'like', $ym . '%')
|
|
|
->whereOr('createtime', 'like', $ym . '%');
|
|
|
})
|
|
|
- ->field('ccydh,result')
|
|
|
+ ->field('ccydh,result,delivery_status,customer_complaint,order_interrupt')
|
|
|
->select();
|
|
|
} catch (\Throwable $e) {
|
|
|
- $rows = [];
|
|
|
- Log::write('loadInboundOrderResults table: ' . $e->getMessage(), 'error');
|
|
|
+ try {
|
|
|
+ $rows = Db::table('purchase_order_inbound_score')
|
|
|
+ ->where('company_name', $companyName)
|
|
|
+ ->where(function ($q) use ($ym) {
|
|
|
+ $q->where('updatetime', 'like', $ym . '%')
|
|
|
+ ->whereOr('createtime', 'like', $ym . '%');
|
|
|
+ })
|
|
|
+ ->field('ccydh,result')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e2) {
|
|
|
+ $rows = [];
|
|
|
+ Log::write('loadInboundOrderAgg table: ' . $e2->getMessage(), 'error');
|
|
|
+ }
|
|
|
}
|
|
|
if (is_array($rows)) {
|
|
|
foreach ($rows as $r) {
|
|
|
@@ -648,15 +692,75 @@ class ProcuremenSupplierScore
|
|
|
if ($ccydh === '' || ($res !== '合格' && $res !== '不合格')) {
|
|
|
continue;
|
|
|
}
|
|
|
- if (!isset($byOrder[$ccydh]) || $res === '不合格') {
|
|
|
- $byOrder[$ccydh] = $res;
|
|
|
+ $ds = trim((string)($r['delivery_status'] ?? ''));
|
|
|
+ if ($ds !== '准时' && $ds !== '滞后') {
|
|
|
+ $ds = '';
|
|
|
+ }
|
|
|
+ $complaint = (int)($r['customer_complaint'] ?? 0) === 1 ? 1 : 0;
|
|
|
+ $interrupt = (int)($r['order_interrupt'] ?? 0) === 1 ? 1 : 0;
|
|
|
+ if (!isset($byOrder[$ccydh])) {
|
|
|
+ $byOrder[$ccydh] = [
|
|
|
+ 'result' => $res,
|
|
|
+ 'delivery_status' => $ds,
|
|
|
+ 'customer_complaint' => $complaint,
|
|
|
+ 'order_interrupt' => $interrupt,
|
|
|
+ ];
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if ($res === '不合格') {
|
|
|
+ $byOrder[$ccydh]['result'] = '不合格';
|
|
|
+ }
|
|
|
+ if ($byOrder[$ccydh]['delivery_status'] === '' && $ds !== '') {
|
|
|
+ $byOrder[$ccydh]['delivery_status'] = $ds;
|
|
|
+ }
|
|
|
+ if ($complaint === 1) {
|
|
|
+ $byOrder[$ccydh]['customer_complaint'] = 1;
|
|
|
+ }
|
|
|
+ if ($interrupt === 1) {
|
|
|
+ $byOrder[$ccydh]['order_interrupt'] = 1;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if ($byOrder !== []) {
|
|
|
return $byOrder;
|
|
|
}
|
|
|
- // 2) 回退:操作日志「入库评分」+ 采购单供应商
|
|
|
+ // 回退:操作日志仅能还原合格/不合格
|
|
|
+ $legacy = self::loadInboundOrderResultsFromOperLogs($companyName, $ym);
|
|
|
+ foreach ($legacy as $ccydh => $res) {
|
|
|
+ $byOrder[$ccydh] = [
|
|
|
+ 'result' => $res,
|
|
|
+ 'delivery_status' => '',
|
|
|
+ 'customer_complaint' => 0,
|
|
|
+ 'order_interrupt' => 0,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $byOrder;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 某供应商某月:订单号 => 合格|不合格(兼容旧调用)
|
|
|
+ *
|
|
|
+ * @return array<string, string>
|
|
|
+ */
|
|
|
+ public static function loadInboundOrderResultsByCompanyYm(string $companyName, string $ym): array
|
|
|
+ {
|
|
|
+ $out = [];
|
|
|
+ foreach (self::loadInboundOrderAggByCompanyYm($companyName, $ym) as $ccydh => $agg) {
|
|
|
+ $out[$ccydh] = (string)($agg['result'] ?? '');
|
|
|
+ }
|
|
|
+
|
|
|
+ return $out;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 操作日志回退:订单号 => 合格|不合格
|
|
|
+ *
|
|
|
+ * @return array<string, string>
|
|
|
+ */
|
|
|
+ protected static function loadInboundOrderResultsFromOperLogs(string $companyName, string $ym): array
|
|
|
+ {
|
|
|
+ $byOrder = [];
|
|
|
try {
|
|
|
$logs = Db::table('purchase_order_oper_log')
|
|
|
->where(function ($q) {
|
|
|
@@ -723,99 +827,413 @@ class ProcuremenSupplierScore
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 用入库合格/不合格重算并写入月度「质量得分」
|
|
|
- * 已手工保存(qp_manual=1)的供应商跳过,不再覆盖
|
|
|
+ * 质量得分(满分 40)= 合格率分(20) + 客户投诉(10) + 生产中断(10)
|
|
|
*
|
|
|
- * @param string|null $onlyCompany 仅同步该供应商;null=当月有入库评分的全部
|
|
|
+ * @return float|null 无入库评分订单时返回 null
|
|
|
*/
|
|
|
- public static function syncQualityScoreFromInbound(string $ym, ?string $onlyCompany = null): void
|
|
|
+ public static function calcQualityScoreFromInbound(string $companyName, string $ym): ?float
|
|
|
{
|
|
|
+ $companyName = trim($companyName);
|
|
|
$ym = self::formatScoreYm($ym);
|
|
|
- if ($ym === '') {
|
|
|
- return;
|
|
|
+ if ($companyName === '' || $ym === '') {
|
|
|
+ return null;
|
|
|
}
|
|
|
- self::ensureFinalScoreTable();
|
|
|
- // 先尽量从操作日志回填入库评分表(防止表数据丢失导致档案/计算为空)
|
|
|
- self::repairInboundScoreFromOperLogs($ym);
|
|
|
- $companies = [];
|
|
|
- $onlyCompany = $onlyCompany !== null ? trim($onlyCompany) : '';
|
|
|
- if ($onlyCompany !== '') {
|
|
|
- $companies = [$onlyCompany];
|
|
|
+ $byOrder = self::loadInboundOrderAggByCompanyYm($companyName, $ym);
|
|
|
+ $total = count($byOrder);
|
|
|
+ if ($total < 1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $fail = 0;
|
|
|
+ $complaintAny = false;
|
|
|
+ $interruptCnt = 0;
|
|
|
+ foreach ($byOrder as $agg) {
|
|
|
+ if (($agg['result'] ?? '') === '不合格') {
|
|
|
+ $fail++;
|
|
|
+ }
|
|
|
+ if ((int)($agg['customer_complaint'] ?? 0) === 1) {
|
|
|
+ $complaintAny = true;
|
|
|
+ }
|
|
|
+ if ((int)($agg['order_interrupt'] ?? 0) === 1) {
|
|
|
+ $interruptCnt++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $passRate = (1 - ($fail / $total)) * 100;
|
|
|
+ $passScore = self::mapPassRatePercentToQualityScore($passRate);
|
|
|
+ $complaintScore = $complaintAny ? 0.0 : 10.0;
|
|
|
+ if ($interruptCnt >= 3) {
|
|
|
+ $interruptScore = 0.0;
|
|
|
} else {
|
|
|
+ $interruptScore = max(0.0, 10.0 - 2.0 * $interruptCnt);
|
|
|
+ }
|
|
|
+
|
|
|
+ return round($passScore + $complaintScore + $interruptScore, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 交货得分(满分 40)= 准时率分(30) + 生产中断(10,出现一次得 0)
|
|
|
+ *
|
|
|
+ * @return float|null 无入库评分订单时返回 null
|
|
|
+ */
|
|
|
+ public static function calcDeliveryScoreFromInbound(string $companyName, string $ym): ?float
|
|
|
+ {
|
|
|
+ $companyName = trim($companyName);
|
|
|
+ $ym = self::formatScoreYm($ym);
|
|
|
+ if ($companyName === '' || $ym === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $byOrder = self::loadInboundOrderAggByCompanyYm($companyName, $ym);
|
|
|
+ $total = count($byOrder);
|
|
|
+ if ($total < 1) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $late = 0;
|
|
|
+ $interruptAny = false;
|
|
|
+ foreach ($byOrder as $agg) {
|
|
|
+ if (($agg['delivery_status'] ?? '') === '滞后') {
|
|
|
+ $late++;
|
|
|
+ }
|
|
|
+ if ((int)($agg['order_interrupt'] ?? 0) === 1) {
|
|
|
+ $interruptAny = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $onTimeRate = (1 - ($late / $total)) * 100;
|
|
|
+ $onTimeScore = self::mapOnTimeRatePercentToDeliveryScore($onTimeRate);
|
|
|
+ $interruptScore = $interruptAny ? 0.0 : 10.0;
|
|
|
+
|
|
|
+ return round($onTimeScore + $interruptScore, 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 价格得分(满分 15):全部严格低于最高限价→15;任一单等于限价或限价为空→10
|
|
|
+ *
|
|
|
+ * @return float|null 无比价订单时返回 null
|
|
|
+ */
|
|
|
+ public static function calcPriceScoreFromCeiling(string $companyName, string $ym): ?float
|
|
|
+ {
|
|
|
+ $companyName = trim($companyName);
|
|
|
+ $ym = self::formatScoreYm($ym);
|
|
|
+ if ($companyName === '' || $ym === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $byOrder = self::loadInboundOrderAggByCompanyYm($companyName, $ym);
|
|
|
+ if ($byOrder === []) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $ccydhList = array_keys($byOrder);
|
|
|
+ $compared = 0;
|
|
|
+ $allStrictlyBelow = true;
|
|
|
+ try {
|
|
|
+ $poRows = Db::table('purchase_order')
|
|
|
+ ->alias('po')
|
|
|
+ ->join('purchase_order_detail d', 'd.scydgy_id = po.scydgy_id')
|
|
|
+ ->where('po.CCYDH', 'in', $ccydhList)
|
|
|
+ ->where('d.company_name', $companyName)
|
|
|
+ ->whereRaw(
|
|
|
+ '(po.mod_rq IS NULL OR TRIM(CAST(po.mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(po.mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
|
|
|
+ )
|
|
|
+ ->field('po.CCYDH,po.ceilingPrice,d.amount')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
try {
|
|
|
- $rows = Db::table('purchase_order_inbound_score')
|
|
|
- ->where(function ($q) use ($ym) {
|
|
|
- $q->where('updatetime', 'like', $ym . '%')
|
|
|
- ->whereOr('createtime', 'like', $ym . '%');
|
|
|
- })
|
|
|
- ->field('company_name')
|
|
|
- ->group('company_name')
|
|
|
+ $poRows = Db::table('purchase_order')
|
|
|
+ ->alias('po')
|
|
|
+ ->join('purchase_order_detail d', 'd.scydgy_id = po.scydgy_id')
|
|
|
+ ->where('po.CCYDH', 'in', $ccydhList)
|
|
|
+ ->where('d.company_name', $companyName)
|
|
|
+ ->field('po.CCYDH,po.ceilingPrice,d.amount')
|
|
|
->select();
|
|
|
- } catch (\Throwable $e) {
|
|
|
- $rows = [];
|
|
|
- Log::write('syncQualityScoreFromInbound list: ' . $e->getMessage(), 'error');
|
|
|
+ } catch (\Throwable $e2) {
|
|
|
+ Log::write('calcPriceScoreFromCeiling: ' . $e2->getMessage(), 'error');
|
|
|
+
|
|
|
+ return null;
|
|
|
}
|
|
|
- if (is_array($rows)) {
|
|
|
- foreach ($rows as $r) {
|
|
|
- if (!is_array($r)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $cn = trim((string)($r['company_name'] ?? ''));
|
|
|
- if ($cn !== '') {
|
|
|
- $companies[$cn] = true;
|
|
|
- }
|
|
|
+ }
|
|
|
+ if (!is_array($poRows) || $poRows === []) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ /** @var array<string, bool> $orderStrictBelow 订单是否全部严格低于限价 */
|
|
|
+ $orderStrictBelow = [];
|
|
|
+ foreach ($poRows as $r) {
|
|
|
+ if (!is_array($r)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $ccydh = trim((string)($r['CCYDH'] ?? $r['ccydh'] ?? ''));
|
|
|
+ if ($ccydh === '' || !isset($byOrder[$ccydh])) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (self::isBlankQuoteAmount($r['amount'] ?? null)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $amount = self::parseMoneyValue($r['amount'] ?? null);
|
|
|
+ if ($amount === null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $compared++;
|
|
|
+ if (!isset($orderStrictBelow[$ccydh])) {
|
|
|
+ $orderStrictBelow[$ccydh] = true;
|
|
|
+ }
|
|
|
+ $ceiling = self::parseMoneyValue($r['ceilingPrice'] ?? $r['ceiling_price'] ?? null);
|
|
|
+ if ($ceiling === null) {
|
|
|
+ // 限价空 → 该单非严格低于
|
|
|
+ $orderStrictBelow[$ccydh] = false;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 严格低于才算;等于或高于都不算
|
|
|
+ if ($amount >= $ceiling - 0.0000001) {
|
|
|
+ $orderStrictBelow[$ccydh] = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($compared < 1 || $orderStrictBelow === []) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ foreach ($orderStrictBelow as $ok) {
|
|
|
+ if (!$ok) {
|
|
|
+ $allStrictlyBelow = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $allStrictlyBelow ? 15.0 : 10.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 增值服务(满分 10):未报价订单数(按订单号去重)≤3 →10;>3 →0
|
|
|
+ *
|
|
|
+ * @return float|null 当月无询价记录时返回 null
|
|
|
+ */
|
|
|
+ public static function calcValueAddedScoreFromUnquoted(string $companyName, string $ym): ?float
|
|
|
+ {
|
|
|
+ $companyName = trim($companyName);
|
|
|
+ $ym = self::formatScoreYm($ym);
|
|
|
+ if ($companyName === '' || $ym === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $stats = self::loadUnquotedOrderStatsByCompanyYm($companyName, $ym);
|
|
|
+ if ($stats === null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $unquoted = (int)($stats['unquoted'] ?? 0);
|
|
|
+
|
|
|
+ return $unquoted <= 3 ? 10.0 : 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 某供应商某月询价订单:总数 / 未报价数(均按 CCYDH 去重)
|
|
|
+ *
|
|
|
+ * @return array{total:int,unquoted:int}|null 无询价记录时 null
|
|
|
+ */
|
|
|
+ public static function loadUnquotedOrderStatsByCompanyYm(string $companyName, string $ym): ?array
|
|
|
+ {
|
|
|
+ $companyName = trim($companyName);
|
|
|
+ $ym = self::formatScoreYm($ym);
|
|
|
+ if ($companyName === '' || $ym === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $ymEsc = str_replace(["\\", "'", '%', '_'], ["\\\\", "''", '\\%', '\\_'], $ym);
|
|
|
+ try {
|
|
|
+ $rows = Db::table('purchase_order_detail')
|
|
|
+ ->alias('d')
|
|
|
+ ->join('purchase_order po', 'po.scydgy_id = d.scydgy_id')
|
|
|
+ ->where('d.company_name', $companyName)
|
|
|
+ ->whereRaw(
|
|
|
+ "("
|
|
|
+ . "CASE "
|
|
|
+ . "WHEN po.createtime IS NOT NULL AND CAST(po.createtime AS CHAR(32)) REGEXP '^[0-9]{10,}$' "
|
|
|
+ . "THEN FROM_UNIXTIME(CAST(po.createtime AS UNSIGNED)) "
|
|
|
+ . "WHEN po.createtime IS NOT NULL AND TRIM(CAST(po.createtime AS CHAR(32))) <> '' "
|
|
|
+ . "AND TRIM(CAST(po.createtime AS CHAR(32))) NOT LIKE '0000-00-00%' "
|
|
|
+ . "THEN TRIM(CAST(po.createtime AS CHAR(32))) "
|
|
|
+ . "ELSE TRIM(CAST(IFNULL(po.dStamp, '') AS CHAR(32))) "
|
|
|
+ . "END"
|
|
|
+ . ") LIKE '{$ymEsc}-%'"
|
|
|
+ )
|
|
|
+ ->whereRaw(
|
|
|
+ '(po.mod_rq IS NULL OR TRIM(CAST(po.mod_rq AS CHAR(32))) = \'\' OR TRIM(CAST(po.mod_rq AS CHAR(32))) LIKE \'0000-00-00%\')'
|
|
|
+ )
|
|
|
+ ->field('po.CCYDH,d.amount')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::write('loadUnquotedOrderStats: ' . $e->getMessage(), 'error');
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!is_array($rows) || $rows === []) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ /** @var array<string, bool> $quotedByOrder 订单是否至少有一条有效报价 */
|
|
|
+ $quotedByOrder = [];
|
|
|
+ foreach ($rows as $r) {
|
|
|
+ if (!is_array($r)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $ccydh = trim((string)($r['CCYDH'] ?? $r['ccydh'] ?? ''));
|
|
|
+ if ($ccydh === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (!isset($quotedByOrder[$ccydh])) {
|
|
|
+ $quotedByOrder[$ccydh] = false;
|
|
|
+ }
|
|
|
+ if (!self::isBlankQuoteAmount($r['amount'] ?? null)) {
|
|
|
+ $quotedByOrder[$ccydh] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ($quotedByOrder === []) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ $total = count($quotedByOrder);
|
|
|
+ $unquoted = 0;
|
|
|
+ foreach ($quotedByOrder as $quoted) {
|
|
|
+ if (!$quoted) {
|
|
|
+ $unquoted++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ['total' => $total, 'unquoted' => $unquoted];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 收集某月需重算的供应商名称(入库评分 + 当月询价明细)
|
|
|
+ *
|
|
|
+ * @return string[]
|
|
|
+ */
|
|
|
+ public static function collectMonthlyReviewCompanyNames(string $ym, ?string $onlyCompany = null): array
|
|
|
+ {
|
|
|
+ $ym = self::formatScoreYm($ym);
|
|
|
+ if ($ym === '') {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $onlyCompany = $onlyCompany !== null ? trim($onlyCompany) : '';
|
|
|
+ if ($onlyCompany !== '') {
|
|
|
+ return [$onlyCompany];
|
|
|
+ }
|
|
|
+ $companies = [];
|
|
|
+ try {
|
|
|
+ $rows = Db::table('purchase_order_inbound_score')
|
|
|
+ ->where(function ($q) use ($ym) {
|
|
|
+ $q->where('updatetime', 'like', $ym . '%')
|
|
|
+ ->whereOr('createtime', 'like', $ym . '%');
|
|
|
+ })
|
|
|
+ ->field('company_name')
|
|
|
+ ->group('company_name')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $rows = [];
|
|
|
+ Log::write('collectMonthlyReviewCompanyNames inbound: ' . $e->getMessage(), 'error');
|
|
|
+ }
|
|
|
+ if (is_array($rows)) {
|
|
|
+ foreach ($rows as $r) {
|
|
|
+ if (!is_array($r)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $cn = trim((string)($r['company_name'] ?? ''));
|
|
|
+ if ($cn !== '') {
|
|
|
+ $companies[$cn] = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ $logs = Db::table('purchase_order_oper_log')
|
|
|
+ ->where(function ($q) {
|
|
|
+ $q->where('action', '入库评分')->whereOr('action', 'inbound_score');
|
|
|
+ })
|
|
|
+ ->where('createtime', 'like', $ym . '%')
|
|
|
+ ->field('scydgy_id')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $logs = [];
|
|
|
+ }
|
|
|
+ $sids = [];
|
|
|
+ if (is_array($logs)) {
|
|
|
+ foreach ($logs as $lg) {
|
|
|
+ if (!is_array($lg)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $sid = (int)($lg['scydgy_id'] ?? 0);
|
|
|
+ if ($sid > 0) {
|
|
|
+ $sids[$sid] = true;
|
|
|
}
|
|
|
}
|
|
|
- // 再并入操作日志涉及的供应商
|
|
|
+ }
|
|
|
+ if ($sids !== []) {
|
|
|
try {
|
|
|
- $logs = Db::table('purchase_order_oper_log')
|
|
|
- ->where(function ($q) {
|
|
|
- $q->where('action', '入库评分')->whereOr('action', 'inbound_score');
|
|
|
- })
|
|
|
- ->where('createtime', 'like', $ym . '%')
|
|
|
- ->field('scydgy_id')
|
|
|
+ $poRows = Db::table('purchase_order')
|
|
|
+ ->where('scydgy_id', 'in', array_keys($sids))
|
|
|
+ ->field('pick_company_name')
|
|
|
+ ->group('pick_company_name')
|
|
|
->select();
|
|
|
} catch (\Throwable $e) {
|
|
|
- $logs = [];
|
|
|
+ $poRows = [];
|
|
|
}
|
|
|
- $sids = [];
|
|
|
- if (is_array($logs)) {
|
|
|
- foreach ($logs as $lg) {
|
|
|
- if (!is_array($lg)) {
|
|
|
+ if (is_array($poRows)) {
|
|
|
+ foreach ($poRows as $po) {
|
|
|
+ if (!is_array($po)) {
|
|
|
continue;
|
|
|
}
|
|
|
- $sid = (int)($lg['scydgy_id'] ?? 0);
|
|
|
- if ($sid > 0) {
|
|
|
- $sids[$sid] = true;
|
|
|
+ $cn = trim((string)($po['pick_company_name'] ?? ''));
|
|
|
+ if ($cn !== '') {
|
|
|
+ $companies[$cn] = true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- if ($sids !== []) {
|
|
|
- try {
|
|
|
- $poRows = Db::table('purchase_order')
|
|
|
- ->where('scydgy_id', 'in', array_keys($sids))
|
|
|
- ->field('pick_company_name')
|
|
|
- ->group('pick_company_name')
|
|
|
- ->select();
|
|
|
- } catch (\Throwable $e) {
|
|
|
- $poRows = [];
|
|
|
+ }
|
|
|
+ // 并入当月有询价明细的供应商(增值服务未报价统计)
|
|
|
+ $ymEsc = str_replace(["\\", "'", '%', '_'], ["\\\\", "''", '\\%', '\\_'], $ym);
|
|
|
+ try {
|
|
|
+ $detailRows = Db::table('purchase_order_detail')
|
|
|
+ ->alias('d')
|
|
|
+ ->join('purchase_order po', 'po.scydgy_id = d.scydgy_id')
|
|
|
+ ->whereRaw(
|
|
|
+ "("
|
|
|
+ . "CASE "
|
|
|
+ . "WHEN po.createtime IS NOT NULL AND CAST(po.createtime AS CHAR(32)) REGEXP '^[0-9]{10,}$' "
|
|
|
+ . "THEN FROM_UNIXTIME(CAST(po.createtime AS UNSIGNED)) "
|
|
|
+ . "WHEN po.createtime IS NOT NULL AND TRIM(CAST(po.createtime AS CHAR(32))) <> '' "
|
|
|
+ . "AND TRIM(CAST(po.createtime AS CHAR(32))) NOT LIKE '0000-00-00%' "
|
|
|
+ . "THEN TRIM(CAST(po.createtime AS CHAR(32))) "
|
|
|
+ . "ELSE TRIM(CAST(IFNULL(po.dStamp, '') AS CHAR(32))) "
|
|
|
+ . "END"
|
|
|
+ . ") LIKE '{$ymEsc}-%'"
|
|
|
+ )
|
|
|
+ ->field('d.company_name')
|
|
|
+ ->group('d.company_name')
|
|
|
+ ->select();
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ $detailRows = [];
|
|
|
+ }
|
|
|
+ if (is_array($detailRows)) {
|
|
|
+ foreach ($detailRows as $dr) {
|
|
|
+ if (!is_array($dr)) {
|
|
|
+ continue;
|
|
|
}
|
|
|
- if (is_array($poRows)) {
|
|
|
- foreach ($poRows as $po) {
|
|
|
- if (!is_array($po)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $cn = trim((string)($po['pick_company_name'] ?? ''));
|
|
|
- if ($cn !== '') {
|
|
|
- $companies[$cn] = true;
|
|
|
- }
|
|
|
- }
|
|
|
+ $cn = trim((string)($dr['company_name'] ?? ''));
|
|
|
+ if ($cn !== '') {
|
|
|
+ $companies[$cn] = true;
|
|
|
}
|
|
|
}
|
|
|
- $companies = array_keys($companies);
|
|
|
}
|
|
|
+
|
|
|
+ return array_keys($companies);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按新月度规则重算四项得分 + 最终分/等级(满分 105)
|
|
|
+ * 已手工保存(qp_manual=1)的供应商跳过
|
|
|
+ *
|
|
|
+ * @param string|null $onlyCompany 仅同步该供应商;null=当月相关全部
|
|
|
+ * @return array{updated:int,skipped:int}
|
|
|
+ */
|
|
|
+ public static function syncMonthlyReviewScoresFromRules(string $ym, ?string $onlyCompany = null): array
|
|
|
+ {
|
|
|
+ $ym = self::formatScoreYm($ym);
|
|
|
+ $result = ['updated' => 0, 'skipped' => 0];
|
|
|
+ if ($ym === '') {
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+ self::ensureFinalScoreTable();
|
|
|
+ self::repairInboundScoreFromOperLogs($ym);
|
|
|
+ $companies = self::collectMonthlyReviewCompanyNames($ym, $onlyCompany);
|
|
|
if ($companies === []) {
|
|
|
- return;
|
|
|
+ return $result;
|
|
|
}
|
|
|
self::ensureMonthlySupplierNames($ym, $companies);
|
|
|
$now = date('Y-m-d H:i:s');
|
|
|
@@ -824,41 +1242,63 @@ class ProcuremenSupplierScore
|
|
|
if ($cn === '') {
|
|
|
continue;
|
|
|
}
|
|
|
- $score = self::calcQualityScoreFromInbound($cn, $ym);
|
|
|
- if ($score === null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
try {
|
|
|
$exists = Db::table(self::TABLE_FINAL)->where('ym', $ym)->where('company_name', $cn)->find();
|
|
|
+ if (is_array($exists) && $exists !== [] && (int)($exists['qp_manual'] ?? 0) === 1) {
|
|
|
+ $result['skipped']++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $quality = self::calcQualityScoreFromInbound($cn, $ym);
|
|
|
+ $price = self::calcPriceScoreFromCeiling($cn, $ym);
|
|
|
+ $delivery = self::calcDeliveryScoreFromInbound($cn, $ym);
|
|
|
+ $valueAdded = self::calcValueAddedScoreFromUnquoted($cn, $ym);
|
|
|
+ $final = self::sumFinalScore($quality, $price, $delivery, $valueAdded);
|
|
|
+ $grade = $final !== null ? self::gradeFromFinalScore($final) : '';
|
|
|
+ $scoreCompat = $final !== null ? $final : 0.0;
|
|
|
if (!is_array($exists) || $exists === []) {
|
|
|
Db::table(self::TABLE_FINAL)->insert([
|
|
|
'ym' => $ym,
|
|
|
'company_name' => $cn,
|
|
|
- 'score' => 0,
|
|
|
- 'quality_score' => $score,
|
|
|
- 'price_score' => null,
|
|
|
- 'delivery_score' => null,
|
|
|
- 'value_added_score' => null,
|
|
|
- 'final_score' => null,
|
|
|
- 'score_grade' => '',
|
|
|
+ 'score' => $scoreCompat,
|
|
|
+ 'quality_score' => $quality,
|
|
|
+ 'price_score' => $price,
|
|
|
+ 'delivery_score' => $delivery,
|
|
|
+ 'value_added_score' => $valueAdded,
|
|
|
+ 'final_score' => $final,
|
|
|
+ 'score_grade' => $grade,
|
|
|
'qp_manual' => 0,
|
|
|
'createtime' => $now,
|
|
|
'updatetime' => $now,
|
|
|
]);
|
|
|
- continue;
|
|
|
- }
|
|
|
- // 已手工保存:不再按入库规则覆盖质量分
|
|
|
- if ((int)($exists['qp_manual'] ?? 0) === 1) {
|
|
|
- continue;
|
|
|
+ } else {
|
|
|
+ Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update([
|
|
|
+ 'score' => $scoreCompat,
|
|
|
+ 'quality_score' => $quality,
|
|
|
+ 'price_score' => $price,
|
|
|
+ 'delivery_score' => $delivery,
|
|
|
+ 'value_added_score' => $valueAdded,
|
|
|
+ 'final_score' => $final,
|
|
|
+ 'score_grade' => $grade,
|
|
|
+ 'updatetime' => $now,
|
|
|
+ ]);
|
|
|
}
|
|
|
- Db::table(self::TABLE_FINAL)->where('id', (int)($exists['id'] ?? 0))->update([
|
|
|
- 'quality_score' => $score,
|
|
|
- 'updatetime' => $now,
|
|
|
- ]);
|
|
|
+ $result['updated']++;
|
|
|
} catch (\Throwable $e) {
|
|
|
- Log::write('syncQualityScoreFromInbound write: ' . $e->getMessage(), 'error');
|
|
|
+ Log::write('syncMonthlyReviewScoresFromRules write: ' . $e->getMessage(), 'error');
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 兼容旧入口:改为按月度四项规则同步
|
|
|
+ *
|
|
|
+ * @param string|null $onlyCompany 仅同步该供应商;null=当月相关全部
|
|
|
+ */
|
|
|
+ public static function syncQualityScoreFromInbound(string $ym, ?string $onlyCompany = null): void
|
|
|
+ {
|
|
|
+ self::syncMonthlyReviewScoresFromRules($ym, $onlyCompany);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -996,8 +1436,8 @@ class ProcuremenSupplierScore
|
|
|
self::ensureFinalScoreTable();
|
|
|
// 本月询价(开标后已写入订单评分表)的供应商:名称不存在则加空行,已存在不动
|
|
|
self::ensureMonthlySupplierNamesFromOrderYm($ym);
|
|
|
- // 按入库合格/不合格重算质量得分(未人工改过的)
|
|
|
- self::syncQualityScoreFromInbound($ym);
|
|
|
+ // 按月度规则重算质量/价格/交货/增值(满分105;qp_manual=1 跳过)
|
|
|
+ self::syncMonthlyReviewScoresFromRules($ym);
|
|
|
try {
|
|
|
$rows = Db::table(self::TABLE_FINAL)
|
|
|
->where('ym', $ym)
|