diff --git a/Antis.Erp.Plat/antis-ncc-admin/src/utils/wtRejectApproval.js b/Antis.Erp.Plat/antis-ncc-admin/src/utils/wtRejectApproval.js index 3de0e46..fb201e0 100644 --- a/Antis.Erp.Plat/antis-ncc-admin/src/utils/wtRejectApproval.js +++ b/Antis.Erp.Plat/antis-ncc-admin/src/utils/wtRejectApproval.js @@ -67,3 +67,39 @@ export function postApprovePurchaseInbound(id, remark) { data: { remark: remark || '' } }) } + +/** 应收款增加减少(wt_yskzjjs):提交审核 */ +export function postYskzjjsSubmitForAudit(id) { + return request({ + url: `/api/Extend/WtYskzjjs/Actions/SubmitForAudit/${id}`, + method: 'POST', + data: {} + }) +} + +/** 应收款增加减少:撤回审核(回草稿) */ +export function postYskzjjsWithdrawAudit(id) { + return request({ + url: `/api/Extend/WtYskzjjs/Actions/WithdrawAudit/${id}`, + method: 'POST', + data: {} + }) +} + +/** 应收款增加减少:审核通过 */ +export function postYskzjjsApprove(id, remark) { + return request({ + url: `/api/Extend/WtYskzjjs/Actions/Approve/${id}`, + method: 'POST', + data: { remark: remark || '' } + }) +} + +/** 应收款增加减少:审核不通过 */ +export function postYskzjjsReject(id, remark) { + return request({ + url: `/api/Extend/WtYskzjjs/Actions/Reject/${id}`, + method: 'POST', + data: { remark: remark || '' } + }) +} diff --git a/Antis.Erp.Plat/antis-ncc-admin/src/views/basic/todoCenter/index.vue b/Antis.Erp.Plat/antis-ncc-admin/src/views/basic/todoCenter/index.vue index 6336616..f71405f 100644 --- a/Antis.Erp.Plat/antis-ncc-admin/src/views/basic/todoCenter/index.vue +++ b/Antis.Erp.Plat/antis-ncc-admin/src/views/basic/todoCenter/index.vue @@ -4,34 +4,72 @@
- + 待办中心 - 同价调拨单、销售/预售出库、销售/预售退货、采购入库单、委托代销发/退/结算、商品调价单等待办(与首页待办数据源一致) + 同价调拨单、销售/预售出库、销售/预售退货、采购入库单、委托代销发/退/结算、商品调价单等待办(与首页待办数据源一致)
- +
- - - + + + - - + + @@ -53,9 +91,25 @@ > @@ -67,9 +121,25 @@ > @@ -81,9 +151,25 @@ > @@ -95,9 +181,25 @@ > @@ -109,9 +211,25 @@ > @@ -123,9 +241,25 @@ > @@ -137,9 +271,25 @@ > @@ -151,9 +301,25 @@ > @@ -165,9 +331,25 @@ > @@ -179,44 +361,98 @@ > + + + +
diff --git a/Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_qt/index.vue b/Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_qt/index.vue index 52e9b04..07c29df 100644 --- a/Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_qt/index.vue +++ b/Antis.Erp.Plat/antis-ncc-admin/src/views/wtYskzjjs_qt/index.vue @@ -1,294 +1,605 @@ \ No newline at end of file + } +}; + diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdCrInput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdCrInput.cs index 2becf43..dfb58fe 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdCrInput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdCrInput.cs @@ -77,6 +77,26 @@ namespace NCC.Extend.Entitys.Dto.WtFysrd /// 付款单位 /// public string fkdw { get; set; } + + /// + /// 往来单位(wt_wldw.F_Id);与 fkdw 同步保存 + /// + public string wldw { get; set; } + + /// + /// 兼容请求:客户主键,落库时与 wldw/fkdw 对齐 + /// + public string kh { get; set; } + + /// + /// 兼容请求:供应商主键,落库时与 wldw/fkdw 对齐 + /// + public string gys { get; set; } + + /// + /// 单据状态(一般仅查询回显;写入以服务端为准) + /// + public string djzt { get; set; } /// /// 费用收入单明细 diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdInfoOutput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdInfoOutput.cs index 4316cba..30fe63e 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdInfoOutput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdInfoOutput.cs @@ -77,6 +77,36 @@ namespace NCC.Extend.Entitys.Dto.WtFysrd /// 付款单位 /// public string fkdw { get; set; } + + /// + /// 往来单位主键(与 fkdw 一致) + /// + public string wldw { get; set; } + + /// + /// 往来单位名称 + /// + public string wldwmc { get; set; } + + /// + /// 单据状态 + /// + public string djzt { get; set; } + + /// + /// 审批备注 + /// + public string spbz { get; set; } + + /// + /// 一级审核人 + /// + public string shr1 { get; set; } + + /// + /// 二级审核人 + /// + public string shr2 { get; set; } /// /// 费用收入单明细 diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListOutput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListOutput.cs index 18faae0..1c67d8a 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListOutput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListOutput.cs @@ -71,6 +71,26 @@ namespace NCC.Extend.Entitys.Dto.WtFysrd /// 单据类型 /// public string djlx { get; set; } + + /// + /// 付款单位 + /// + public string fkdw { get; set; } + + /// + /// 往来单位主键 + /// + public string wldw { get; set; } + + /// + /// 往来单位名称 + /// + public string wldwmc { get; set; } + + /// + /// 单据状态 + /// + public string djzt { get; set; } } } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListQueryInput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListQueryInput.cs index 20a8400..586df88 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListQueryInput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtFysrdListQueryInput.cs @@ -83,6 +83,16 @@ namespace NCC.Extend.Entitys.Dto.WtFysrd /// 单据类型 /// public string djlx { get; set; } + + /// + /// 付款单位 + /// + public string fkdw { get; set; } + + /// + /// 往来单位(与 fkdw 任一匹配) + /// + public string wldw { get; set; } } } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPurchaseSummaryQueryInput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPurchaseSummaryQueryInput.cs index 04bcae5..6c63263 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPurchaseSummaryQueryInput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtPurchaseSummaryQueryInput.cs @@ -36,11 +36,21 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd public string Warehouse { get; set; } /// - /// 单据类型(支持逗号分隔多值) + /// 单据类型(逗号分隔,仅允许采购/销售/预售/委托代销共 9 类;空或 all 表示默认包含全部 9 类) /// public string BillType { get; set; } /// + /// 排序列键(白名单):分类/品牌/商品聚合支持「分类名称」「采购金额」等;明细/线性列表支持「单据日期」「数量」等;非法则走各接口默认排序。 + /// + public string SortField { get; set; } + + /// + /// 排序方向:asc / desc(默认 desc) + /// + public string SortOrder { get; set; } + + /// /// 下钻:商品分类主键(wt_pl.F_Id),与明细接口联用 /// public string CategoryId { get; set; } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtStockSummaryQueryInput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtStockSummaryQueryInput.cs index f534977..2916245 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtStockSummaryQueryInput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtStockSummaryQueryInput.cs @@ -24,5 +24,10 @@ namespace NCC.Extend.Entitys.Dto.WtXsckd /// 商品关键字(匹配 wt_sp 名称或编码) /// public string Product { get; set; } + + /// + /// 商品主键(wt_sp.F_Id);非空时按精确商品筛选(优先级高于 ) + /// + public string ProductSpId { get; set; } } } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsCrInput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsCrInput.cs index ec28e7f..6c78ca5 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsCrInput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsCrInput.cs @@ -29,6 +29,11 @@ namespace NCC.Extend.Entitys.Dto.WtYskzjjs public string jsr { get; set; } /// + /// 往来单位(主表 wldw,与明细 kh 对应) + /// + public string wldw { get; set; } + + /// /// 审核状态 /// public string djzt { get; set; } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsInfoOutput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsInfoOutput.cs index eae5032..e64f672 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsInfoOutput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsInfoOutput.cs @@ -27,6 +27,11 @@ namespace NCC.Extend.Entitys.Dto.WtYskzjjs /// 往来单位 /// public string wldw { get; set; } + + /// + /// 往来单位名称(详情展示,服务端解析) + /// + public string wldwmc { get; set; } /// /// 经手人 @@ -34,6 +39,26 @@ namespace NCC.Extend.Entitys.Dto.WtYskzjjs public string jsr { get; set; } /// + /// 审批备注 + /// + public string spbz { get; set; } + + /// + /// 审核人(终审) + /// + public string shr { get; set; } + + /// + /// 一级审核人 + /// + public string shr1 { get; set; } + + /// + /// 二级审核人 + /// + public string shr2 { get; set; } + + /// /// 审核状态 /// public string djzt { get; set; } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsListOutput.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsListOutput.cs index d1a6a52..43d259c 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsListOutput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Dto/WtYskzjjsListOutput.cs @@ -86,6 +86,26 @@ namespace NCC.Extend.Entitys.Dto.WtYskzjjs /// 往来单位名称(按往来单位/会员/供应商自动解析) /// public string wldwmc { get; set; } + + /// + /// 审批备注 + /// + public string spbz { get; set; } + + /// + /// 审核人(终审) + /// + public string shr { get; set; } + + /// + /// 一级审核人 + /// + public string shr1 { get; set; } + + /// + /// 二级审核人 + /// + public string shr2 { get; set; } } } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtFysrdEntity.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtFysrdEntity.cs index bcd1a05..88defcc 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtFysrdEntity.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtFysrdEntity.cs @@ -94,6 +94,36 @@ namespace NCC.Extend.Entitys /// [SugarColumn(ColumnName = "fkdw")] public string Fkdw { get; set; } + + /// + /// 往来单位(与 fkdw 存同一 wt_wldw.F_Id,便于与 kh/gys/wldw 统一口径) + /// + [SugarColumn(ColumnName = "wldw", IsNullable = true)] + public string Wldw { get; set; } + + /// + /// 单据状态(草稿/待审核/一级已审/已审核/审核不通过) + /// + [SugarColumn(ColumnName = "djzt", IsNullable = true)] + public string Djzt { get; set; } + + /// + /// 审批备注(与业务备注 bz 分离) + /// + [SugarColumn(ColumnName = "spbz", IsNullable = true)] + public string Spbz { get; set; } + + /// + /// 一级审核人 + /// + [SugarColumn(ColumnName = "shr1", IsNullable = true)] + public string Shr1 { get; set; } + + /// + /// 二级审核人 + /// + [SugarColumn(ColumnName = "shr2", IsNullable = true)] + public string Shr2 { get; set; } } } \ No newline at end of file diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtYskzjjsEntity.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtYskzjjsEntity.cs index 107e8de..c928fd5 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtYskzjjsEntity.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend.Entitys/Entity/WtYskzjjsEntity.cs @@ -42,6 +42,30 @@ namespace NCC.Extend.Entitys public string Jsr { get; set; } /// + /// 审批备注(与业务备注分离;库表须含列 spbz) + /// + [SugarColumn(ColumnName = "spbz", IsNullable = true, ColumnDataType = "text")] + public string Spbz { get; set; } + + /// + /// 审核人(终审) + /// + [SugarColumn(ColumnName = "shr", IsNullable = true)] + public string Shr { get; set; } + + /// + /// 一级审核人 + /// + [SugarColumn(ColumnName = "shr1", IsNullable = true)] + public string Shr1 { get; set; } + + /// + /// 二级审核人 + /// + [SugarColumn(ColumnName = "shr2", IsNullable = true)] + public string Shr2 { get; set; } + + /// /// 审核状态 /// [SugarColumn(ColumnName = "djzt")] diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdService.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdService.cs index eb72ab8..09bf578 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdService.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdService.cs @@ -36,6 +36,7 @@ namespace NCC.Extend.WtFysrd private readonly ISqlSugarRepository _wtFysrdMxRepository; private readonly SqlSugarScope _db; private readonly IUserManager _userManager; + private readonly WtFysrdWorkflowHelper _fysrdWorkflow; /// /// 初始化一个类型的新实例 @@ -43,12 +44,71 @@ namespace NCC.Extend.WtFysrd public WtFysrdService( ISqlSugarRepository wtFysrdRepository, ISqlSugarRepository wtFysrdMxRepository, - IUserManager userManager) + IUserManager userManager, + WtFysrdWorkflowHelper fysrdWorkflow) { _wtFysrdRepository = wtFysrdRepository; _db = _wtFysrdRepository.Context; _wtFysrdMxRepository = wtFysrdMxRepository; _userManager = userManager; + _fysrdWorkflow = fysrdWorkflow; + } + + private static bool IsLockedForEdit(string djzt) + { + var s = djzt?.Trim() ?? ""; + return s == "待审核" || s == "已审核" || s == "一级已审" || s == "待二级" || s == "一级已审/待二级"; + } + + private static bool IsDeletableState(string djzt) + { + var s = djzt?.Trim() ?? ""; + return string.IsNullOrEmpty(s) || s == "草稿" || s == "审核不通过"; + } + + private void NormalizeHeaderFromInput(WtFysrdCrInput input, WtFysrdEntity entity) + { + if (input == null || entity == null) return; + if (!string.IsNullOrWhiteSpace(input.djlx)) + entity.Djlx = WtFysrdWorkflowHelper.NormalizeOtherIncomeDjlx(input.djlx) ?? input.djlx; + if (!string.IsNullOrWhiteSpace(WtFysrdWorkflowHelper.ResolveCounterpartyId(input.wldw, input.fkdw, input.kh, input.gys))) + WtFysrdWorkflowHelper.ApplyCounterpartyToEntity(entity, input.wldw, input.fkdw, input.kh, input.gys); + } + + private async Task EnrichWldwForListAsync(IEnumerable rows) + { + if (rows == null) return; + var rowList = rows as IList ?? rows.ToList(); + if (rowList.Count == 0) return; + foreach (var r in rowList) + { + if (string.IsNullOrWhiteSpace(r.wldw)) + r.wldw = r.fkdw; + if (string.IsNullOrWhiteSpace(r.fkdw)) + r.fkdw = r.wldw; + } + var ids = rowList.Select(x => x.wldw).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(); + if (ids.Count == 0) return; + var wldwRows = await _db.Queryable().In(w => w.Id, ids).ToListAsync(); + var map = wldwRows.ToDictionary(x => x.Id, x => x.Dwmc, StringComparer.OrdinalIgnoreCase); + foreach (var r in rowList) + { + if (!string.IsNullOrWhiteSpace(r.wldw) && map.TryGetValue(r.wldw, out var mc)) + r.wldwmc = mc; + } + } + + /// + /// 提交审核:草稿/审核不通过 → 待审核(其他收入单;除备注外必填校验) + /// + /// 单据编号 + [HttpPost("Actions/SubmitForAudit/{id}")] + public async Task SubmitForAudit(string id) + { + dynamic r = await _fysrdWorkflow.SubmitForAuditAsync(id); + if (r.success == true) + return new { msg = (string)(r.message ?? "已提交") }; + throw NCCException.Bah((string)(r.message ?? "提交失败")); } /// @@ -59,9 +119,22 @@ namespace NCC.Extend.WtFysrd [HttpGet("{id}")] public async Task GetInfo(string id) { + _fysrdWorkflow.EnsureFysrdAuditColumns(); var entity = await _db.Queryable().FirstAsync(p => p.Id == id); var output = entity.Adapt(); - + output.djlx = WtFysrdWorkflowHelper.NormalizeOtherIncomeDjlx(entity.Djlx) ?? entity.Djlx; + output.wldw = string.IsNullOrWhiteSpace(entity.Wldw) ? entity.Fkdw : entity.Wldw; + output.fkdw = string.IsNullOrWhiteSpace(entity.Fkdw) ? entity.Wldw : entity.Fkdw; + output.djzt = entity.Djzt; + output.spbz = entity.Spbz; + output.shr1 = entity.Shr1; + output.shr2 = entity.Shr2; + var wldwId = output.wldw ?? output.fkdw; + if (!string.IsNullOrWhiteSpace(wldwId)) + { + var w = await _db.Queryable().FirstAsync(x => x.Id == wldwId); + output.wldwmc = w?.Dwmc; + } var wtFysrdMxList = await _db.Queryable().Where(w => w.Djbh == entity.Id).ToListAsync(); output.wtFysrdMxList = wtFysrdMxList.Adapt>(); return output; @@ -75,6 +148,7 @@ namespace NCC.Extend.WtFysrd [HttpGet("")] public async Task GetList([FromQuery] WtFysrdListQueryInput input) { + _fysrdWorkflow.EnsureFysrdAuditColumns(); var sidx = input.sidx == null ? "id" : input.sidx; List queryDjrq = input.djrq != null ? input.djrq.Split(',').ToObeject>() : null; DateTime? startDjrq = queryDjrq != null ? Ext.GetDateTime(queryDjrq.First()) : null; @@ -94,6 +168,8 @@ namespace NCC.Extend.WtFysrd .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) .WhereIF(!string.IsNullOrEmpty(input.zy), p => p.Zy.Contains(input.zy)) .WhereIF(!string.IsNullOrEmpty(input.djlx), p => p.Djlx.Contains(input.djlx)) + .WhereIF(!string.IsNullOrEmpty(input.fkdw), p => p.Fkdw == input.fkdw || p.Wldw == input.fkdw) + .WhereIF(!string.IsNullOrEmpty(input.wldw), p => p.Wldw == input.wldw || p.Fkdw == input.wldw) .Select(it=> new WtFysrdListOutput { id = it.Id, @@ -109,7 +185,11 @@ namespace NCC.Extend.WtFysrd bz=it.Bz, zy=it.Zy, djlx=it.Djlx, + fkdw = it.Fkdw, + wldw = it.Wldw, + djzt = it.Djzt, }).MergeTable().OrderBy(sidx+" "+input.sort).ToPagedListAsync(input.currentPage, input.pageSize); + await EnrichWldwForListAsync(data.list); return PageResult.SqlSugarPageResult(data); } @@ -121,15 +201,18 @@ namespace NCC.Extend.WtFysrd [HttpPost("")] public async Task Create([FromBody] WtFysrdCrInput input) { + _fysrdWorkflow.EnsureFysrdAuditColumns(); var userInfo = await _userManager.GetUserInfo(); var entity = input.Adapt(); + NormalizeHeaderFromInput(input, entity); + entity.Djzt = "草稿"; // 生成每日递增单号 var today = DateTime.Now.ToString("yyyyMMdd"); var prefix = "FY"; // 根据单据类型设置前缀 - if (!string.IsNullOrEmpty(input.djlx)) { - if (input.djlx.Contains("现金费用单")) prefix = "XF"; - else if (input.djlx.Contains("其他收入单")) prefix = "QT"; + if (!string.IsNullOrEmpty(entity.Djlx)) { + if (entity.Djlx.Contains("现金费用单")) prefix = "XF"; + else if (WtFysrdWorkflowHelper.IsOtherIncomeDjlx(entity.Djlx)) prefix = "QT"; else prefix = "FY"; // 默认费用单前缀 } var maxId = await _db.Queryable() @@ -199,6 +282,7 @@ namespace NCC.Extend.WtFysrd [NonAction] public async Task GetNoPagingList([FromQuery] WtFysrdListQueryInput input) { + _fysrdWorkflow.EnsureFysrdAuditColumns(); var sidx = input.sidx == null ? "id" : input.sidx; List queryDjrq = input.djrq != null ? input.djrq.Split(',').ToObeject>() : null; DateTime? startDjrq = queryDjrq != null ? Ext.GetDateTime(queryDjrq.First()) : null; @@ -218,6 +302,8 @@ namespace NCC.Extend.WtFysrd .WhereIF(!string.IsNullOrEmpty(input.bz), p => p.Bz.Contains(input.bz)) .WhereIF(!string.IsNullOrEmpty(input.zy), p => p.Zy.Contains(input.zy)) .WhereIF(!string.IsNullOrEmpty(input.djlx), p => p.Djlx.Contains(input.djlx)) + .WhereIF(!string.IsNullOrEmpty(input.fkdw), p => p.Fkdw == input.fkdw || p.Wldw == input.fkdw) + .WhereIF(!string.IsNullOrEmpty(input.wldw), p => p.Wldw == input.wldw || p.Fkdw == input.wldw) .Select(it=> new WtFysrdListOutput { id = it.Id, @@ -233,7 +319,11 @@ namespace NCC.Extend.WtFysrd bz=it.Bz, zy=it.Zy, djlx=it.Djlx, + fkdw = it.Fkdw, + wldw = it.Wldw, + djzt = it.Djzt, }).MergeTable().OrderBy(sidx+" "+input.sort).ToListAsync(); + await EnrichWldwForListAsync(data); return data; } @@ -256,7 +346,7 @@ namespace NCC.Extend.WtFysrd { exportData = await this.GetNoPagingList(input); } - List paramList = "[{\"value\":\"单据编号\",\"field\":\"id\"},{\"value\":\"单据日期\",\"field\":\"djrq\"},{\"value\":\"分支机构\",\"field\":\"fzjg\"},{\"value\":\"部门\",\"field\":\"bm\"},{\"value\":\"经手人\",\"field\":\"jsr\"},{\"value\":\"结算账户\",\"field\":\"jszh\"},{\"value\":\"金额\",\"field\":\"je\"},{\"value\":\"制单人\",\"field\":\"zdr\"},{\"value\":\"审核人\",\"field\":\"shr\"},{\"value\":\"过账人\",\"field\":\"gzr\"},{\"value\":\"备注\",\"field\":\"bz\"},{\"value\":\"摘要\",\"field\":\"zy\"},{\"value\":\"单据类型\",\"field\":\"djlx\"},]".ToList(); + List paramList = "[{\"value\":\"单据编号\",\"field\":\"id\"},{\"value\":\"单据日期\",\"field\":\"djrq\"},{\"value\":\"分支机构\",\"field\":\"fzjg\"},{\"value\":\"部门\",\"field\":\"bm\"},{\"value\":\"经手人\",\"field\":\"jsr\"},{\"value\":\"结算账户\",\"field\":\"jszh\"},{\"value\":\"金额\",\"field\":\"je\"},{\"value\":\"制单人\",\"field\":\"zdr\"},{\"value\":\"审核人\",\"field\":\"shr\"},{\"value\":\"过账人\",\"field\":\"gzr\"},{\"value\":\"备注\",\"field\":\"bz\"},{\"value\":\"摘要\",\"field\":\"zy\"},{\"value\":\"单据类型\",\"field\":\"djlx\"},{\"value\":\"往来单位\",\"field\":\"wldw\"},{\"value\":\"往来单位名称\",\"field\":\"wldwmc\"},{\"value\":\"付款单位\",\"field\":\"fkdw\"},{\"value\":\"单据状态\",\"field\":\"djzt\"},]".ToList(); ExcelConfig excelconfig = new ExcelConfig(); excelconfig.FileName = "费用单.xls"; excelconfig.HeadFont = "微软雅黑"; @@ -292,6 +382,11 @@ namespace NCC.Extend.WtFysrd public async Task BatchRemove([FromBody] List ids) { var entitys = await _db.Queryable().In(it => it.Id, ids).ToListAsync(); + foreach (var e in entitys) + { + if (!IsDeletableState(e.Djzt)) + throw NCCException.Bah($"单据 {e.Id} 当前状态不允许删除"); + } if (entitys.Count > 0) { try @@ -324,7 +419,37 @@ namespace NCC.Extend.WtFysrd [HttpPut("{id}")] public async Task Update(string id, [FromBody] WtFysrdUpInput input) { + _fysrdWorkflow.EnsureFysrdAuditColumns(); + var existing = await _db.Queryable().FirstAsync(p => p.Id == id); + if (IsLockedForEdit(existing.Djzt)) + throw NCCException.Bah("当前单据状态不允许修改"); + var entity = input.Adapt(); + entity.Id = id; + entity.Djzt = existing.Djzt; + if (string.IsNullOrWhiteSpace(entity.Djlx)) + entity.Djlx = existing.Djlx; + NormalizeHeaderFromInput(input, entity); + if (string.IsNullOrWhiteSpace(WtFysrdWorkflowHelper.ResolveCounterpartyId(entity.Wldw, entity.Fkdw, null, null))) + { + entity.Wldw = existing.Wldw; + entity.Fkdw = existing.Fkdw; + } + + if (WtFysrdWorkflowHelper.IsOtherIncomeDjlx(entity.Djlx) + && !string.Equals(existing.Djzt?.Trim(), "草稿", StringComparison.Ordinal)) + { + var mxForVal = input.wtFysrdMxList.Adapt>(); + try + { + WtFysrdWorkflowHelper.ValidateOtherIncomeForSubmit(entity, mxForVal); + } + catch (InvalidOperationException ex) + { + throw NCCException.Bah(ex.Message); + } + } + try { //开启事务 @@ -367,6 +492,8 @@ namespace NCC.Extend.WtFysrd { var entity = await _db.Queryable().FirstAsync(p => p.Id == id); _ = entity ?? throw NCCException.Oh(ErrorCode.COM1005); + if (!IsDeletableState(entity.Djzt)) + throw NCCException.Bah("当前单据状态不允许删除"); try { //开启事务 diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdWorkflowHelper.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdWorkflowHelper.cs new file mode 100644 index 0000000..3844ef8 --- /dev/null +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtFysrdWorkflowHelper.cs @@ -0,0 +1,446 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NCC.Common.Core.Manager; +using NCC.Dependency; +using NCC.Extend.Entitys; +using SqlSugar; + +namespace NCC.Extend.WtFysrd +{ + /// + /// 其他收入单(wt_fysrd)审批流:与待办中心、ApproveGeneric/RejectGeneric/ReverseApproval 对接。 + /// + public class WtFysrdWorkflowHelper : ITransient + { + /// wt_shrysz.djmc、待办 billType 一致 + public const string BillName = "其他收入单"; + + private readonly SqlSugarScope _db; + private readonly IUserManager _userManager; + private static bool _auditColumnsChecked; + + public WtFysrdWorkflowHelper(ISqlSugarRepository fysrdRepository, IUserManager userManager) + { + _db = fysrdRepository.Context; + _userManager = userManager; + } + + /// + /// 确保 wt_fysrd 具备审批与往来单位统一字段(djzt、spbz、shr、shr1、shr2、wldw) + /// + public void EnsureFysrdAuditColumns() + { + if (_auditColumnsChecked) return; + lock (typeof(WtFysrdWorkflowHelper)) + { + if (_auditColumnsChecked) return; + try + { + if (!_db.DbMaintenance.IsAnyTable("wt_fysrd")) { _auditColumnsChecked = true; return; } + var cols = _db.DbMaintenance.GetColumnInfosByTableName("wt_fysrd"); + var names = cols.Select(c => c.DbColumnName.ToLowerInvariant()).ToHashSet(); + if (!names.Contains("djzt")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `djzt` varchar(32) NULL COMMENT '单据状态'"); + if (!names.Contains("spbz")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `spbz` text NULL COMMENT '审批备注'"); + if (!names.Contains("shr")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `shr` varchar(64) NULL COMMENT '审核人'"); + if (!names.Contains("shr1")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `shr1` varchar(500) NULL COMMENT '一级审核人'"); + if (!names.Contains("shr2")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `shr2` varchar(500) NULL COMMENT '二级审核人'"); + if (!names.Contains("wldw")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `wldw` varchar(64) NULL COMMENT '往来单位(wt_wldw.F_Id),与 fkdw 同步'"); + + _db.Ado.ExecuteCommand( + "UPDATE `wt_fysrd` SET `djzt`='草稿' WHERE `djzt` IS NULL OR TRIM(IFNULL(`djzt`,''))=''"); + _db.Ado.ExecuteCommand( + "UPDATE `wt_fysrd` SET `wldw`=`fkdw` WHERE (`wldw` IS NULL OR TRIM(IFNULL(`wldw`,''))='') AND `fkdw` IS NOT NULL AND TRIM(`fkdw`)<>''"); + } + catch (Exception ex) { Console.WriteLine($"EnsureFysrdAuditColumns: {ex.Message}"); } + _auditColumnsChecked = true; + } + } + + /// 是否按「其他收入单」口径处理的单据类型(含历史前端传值) + public static bool IsOtherIncomeDjlx(string djlx) + { + var n = NormalizeOtherIncomeDjlx(djlx); + return string.Equals(n, BillName, StringComparison.Ordinal); + } + + /// 统一单据类型展示与审核配置匹配用字符串 + public static string NormalizeOtherIncomeDjlx(string djlx) + { + if (string.IsNullOrWhiteSpace(djlx)) return djlx; + var t = djlx.Trim().Replace("其它", "其他"); + if (t == "其他收入" || t == "其他收入单" || t.Contains("其他收入单")) + return BillName; + if (t.Contains("其他收入")) + return BillName; + return djlx.Trim(); + } + + /// 往来单位主键:优先 wldw,其次 fkdw、kh、gys(请求兼容) + public static string ResolveCounterpartyId(string wldw, string fkdw, string kh, string gys) + { + var v = !string.IsNullOrWhiteSpace(wldw) ? wldw + : !string.IsNullOrWhiteSpace(fkdw) ? fkdw + : !string.IsNullOrWhiteSpace(kh) ? kh + : gys; + return string.IsNullOrWhiteSpace(v) ? null : v.Trim(); + } + + public static void ApplyCounterpartyToEntity(WtFysrdEntity entity, string wldw, string fkdw, string kh, string gys) + { + var id = ResolveCounterpartyId(wldw, fkdw, kh, gys); + if (string.IsNullOrEmpty(id)) return; + entity.Wldw = id; + entity.Fkdw = id; + } + + private static void AppendSpbz(WtFysrdEntity entity, string actionTag, string remark) + { + var r = (remark ?? "").Trim(); + if (string.IsNullOrEmpty(r)) return; + var line = string.IsNullOrEmpty(actionTag) ? r : $"[{actionTag}] {r}"; + var existing = entity.Spbz?.TrimEnd() ?? ""; + entity.Spbz = string.IsNullOrWhiteSpace(existing) ? line : existing + "\n" + line; + } + + private static bool IsUserInAuditConfig(string configValue, string userId, string userAccount) + { + if (string.IsNullOrWhiteSpace(configValue)) return false; + if (string.IsNullOrWhiteSpace(userId) && string.IsNullOrWhiteSpace(userAccount)) return false; + var users = configValue + .Split(new[] { ',', ',', ';', ';', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + return users.Any(x => + (!string.IsNullOrWhiteSpace(userId) && string.Equals(x, userId.Trim(), StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrWhiteSpace(userAccount) && string.Equals(x, userAccount.Trim(), StringComparison.OrdinalIgnoreCase))); + } + + /// + /// 提交审核前校验:除备注外主表及明细必填(仅其他收入单) + /// + public static void ValidateOtherIncomeForSubmit(WtFysrdEntity h, List mxList) + { + if (h == null) throw new ArgumentNullException(nameof(h)); + if (!IsOtherIncomeDjlx(h.Djlx)) + return; + + if (h.Djrq == null) + throw new InvalidOperationException("单据日期不能为空"); + if (string.IsNullOrWhiteSpace(h.Fzjg)) + throw new InvalidOperationException("分支机构不能为空"); + if (string.IsNullOrWhiteSpace(h.Bm)) + throw new InvalidOperationException("部门不能为空"); + if (string.IsNullOrWhiteSpace(h.Jsr)) + throw new InvalidOperationException("经手人不能为空"); + if (string.IsNullOrWhiteSpace(h.Jszh)) + throw new InvalidOperationException("结算账户不能为空"); + if (h.Je <= 0) + throw new InvalidOperationException("金额必须大于 0"); + if (string.IsNullOrWhiteSpace(h.Zdr)) + throw new InvalidOperationException("制单人不能为空"); + if (string.IsNullOrWhiteSpace(h.Zy)) + throw new InvalidOperationException("摘要不能为空"); + var cp = ResolveCounterpartyId(h.Wldw, h.Fkdw, null, null); + if (string.IsNullOrWhiteSpace(cp)) + throw new InvalidOperationException("往来单位不能为空"); + + if (mxList == null || mxList.Count == 0) + throw new InvalidOperationException("请先维护收入明细"); + + foreach (var mx in mxList) + { + if (string.IsNullOrWhiteSpace(mx.Km)) + throw new InvalidOperationException("明细科目不能为空"); + if (mx.Je <= 0) + throw new InvalidOperationException("明细金额必须大于 0"); + if (string.IsNullOrWhiteSpace(mx.Mxlx)) + throw new InvalidOperationException("明细类型不能为空"); + } + } + + /// + /// 草稿 / 审核不通过 → 待审核 + /// + public async Task SubmitForAuditAsync(string id) + { + EnsureFysrdAuditColumns(); + var entity = await _db.Queryable().FirstAsync(p => p.Id == id); + if (entity == null) + return new { success = false, message = "单据不存在" }; + if (!IsOtherIncomeDjlx(entity.Djlx)) + return new { success = false, message = "该接口仅支持其他收入单" }; + + var st = entity.Djzt?.Trim() ?? ""; + if (st != "草稿" && st != "审核不通过") + return new { success = false, message = "仅草稿或审核不通过状态可提交审核" }; + + var mxList = await _db.Queryable().Where(w => w.Djbh == id).ToListAsync(); + try + { + ValidateOtherIncomeForSubmit(entity, mxList); + } + catch (InvalidOperationException ex) + { + return new { success = false, message = ex.Message }; + } + + entity.Djzt = "待审核"; + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt }).ExecuteCommandAsync(); + return new { success = true, message = "已提交审核,请在待办中心处理" }; + } + + /// + /// 与 对接 + /// + public async Task ApproveAsync(string id, string remark) + { + EnsureFysrdAuditColumns(); + try + { + _db.BeginTran(); + var entity = await _db.Queryable().FirstAsync(p => p.Id == id); + if (entity == null) + { + _db.RollbackTran(); + return new { success = false, message = "单据不存在" }; + } + if (!IsOtherIncomeDjlx(entity.Djlx)) + { + _db.RollbackTran(); + return new { success = false, message = "该单据不是其他收入单" }; + } + + if (entity.Djzt == "已审核") + { + _db.RollbackTran(); + return new { success = false, message = "该单据已经审核,无需重复审核" }; + } + + var userInfo = await _userManager.GetUserInfo(); + var userId = userInfo?.userId ?? ""; + var userAccount = userInfo?.userAccount?.Trim(); + if (string.IsNullOrEmpty(userAccount)) + userAccount = _userManager.Account?.Trim(); + + var approvalConfig = await _db.Queryable() + .Where(c => c.Djmc == BillName) + .FirstAsync(); + + var configShr1 = approvalConfig?.Shr1; + var configShr2 = approvalConfig?.Shr2; + var hasTwoLevel = !string.IsNullOrWhiteSpace(configShr1) && !string.IsNullOrWhiteSpace(configShr2); + + if (string.IsNullOrWhiteSpace(configShr1)) + { + _db.RollbackTran(); + return new { success = false, message = "未配置其他收入单一级审核人员,请先在审核人员设置中维护" }; + } + + var djztTrim = entity.Djzt?.Trim() ?? ""; + if (entity.Djzt == "待审核" || string.IsNullOrEmpty(djztTrim)) + { + if (!IsUserInAuditConfig(configShr1, userId, userAccount)) + { + _db.RollbackTran(); + return new { success = false, message = "当前用户不在其他收入单一级审核人员范围内" }; + } + + if (hasTwoLevel) + { + entity.Djzt = "一级已审"; + entity.Shr1 = userId; + AppendSpbz(entity, "一级通过", remark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr1, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "一级审核通过,等待二级审核" }; + } + + entity.Djzt = "已审核"; + entity.Shr = userId; + entity.Shr1 = userId; + AppendSpbz(entity, "审核通过", remark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr, x.Shr1, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "审核通过" }; + } + + if (entity.Djzt == "一级已审" || entity.Djzt == "待二级" || entity.Djzt == "一级已审/待二级") + { + if (!hasTwoLevel) + { + _db.RollbackTran(); + return new { success = false, message = "当前为单级审核配置,单据状态异常" }; + } + if (string.IsNullOrWhiteSpace(configShr2)) + { + _db.RollbackTran(); + return new { success = false, message = "未配置其他收入单二级审核人员,请先在审核人员设置中维护" }; + } + if (!IsUserInAuditConfig(configShr2, userId, userAccount)) + { + _db.RollbackTran(); + return new { success = false, message = "当前用户不在其他收入单二级审核人员范围内" }; + } + + entity.Djzt = "已审核"; + entity.Shr2 = userId; + entity.Shr = userId; + AppendSpbz(entity, "二级通过", remark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr, x.Shr2, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "二级审核通过,审核完成" }; + } + + _db.RollbackTran(); + return new { success = false, message = $"当前单据状态「{entity.Djzt}」不允许审核" }; + } + catch (Exception ex) + { + _db.RollbackTran(); + return new { success = false, message = $"审核失败: {ex.Message}" }; + } + } + + /// + /// 与 RejectGeneric 对接 + /// + public async Task RejectAsync(string id, string remark) + { + EnsureFysrdAuditColumns(); + var entity = await _db.Queryable().FirstAsync(p => p.Id == id); + if (entity == null) + return new { success = false, message = "单据不存在" }; + if (!IsOtherIncomeDjlx(entity.Djlx)) + return new { success = false, message = "该单据不是其他收入单" }; + if (entity.Djzt == "已审核") + return new { success = false, message = "该单据已审核通过" }; + if (entity.Djzt == "审核不通过") + return new { success = false, message = "该单据已是审核不通过状态" }; + if (entity.Djzt == "草稿") + return new { success = false, message = "草稿状态请直接修改或删除单据" }; + + var userInfo = await _userManager.GetUserInfo(); + var userId = userInfo?.userId ?? ""; + var userAccount = userInfo?.userAccount?.Trim(); + if (string.IsNullOrEmpty(userAccount)) + userAccount = _userManager.Account?.Trim(); + + var approvalConfig = await _db.Queryable() + .Where(c => c.Djmc == BillName) + .FirstAsync(); + var configShr1 = approvalConfig?.Shr1; + var configShr2 = approvalConfig?.Shr2; + + var djztTrim = entity.Djzt?.Trim() ?? ""; + var level1 = djztTrim == "待审核" || string.IsNullOrEmpty(djztTrim); + var level2 = entity.Djzt == "一级已审" || entity.Djzt == "待二级" || entity.Djzt == "一级已审/待二级"; + + if (!level1 && !level2) + return new { success = false, message = $"当前单据状态「{entity.Djzt}」不允许审核不通过" }; + + if (level1) + { + if (string.IsNullOrWhiteSpace(configShr1)) + return new { success = false, message = "未配置其他收入单一级审核人员,请先在审核人员设置中维护" }; + if (!IsUserInAuditConfig(configShr1, userId, userAccount)) + return new { success = false, message = "当前用户不在其他收入单一级审核人员范围内" }; + } + else + { + if (string.IsNullOrWhiteSpace(configShr2)) + return new { success = false, message = "未配置其他收入单二级审核人员,请先在审核人员设置中维护" }; + if (!IsUserInAuditConfig(configShr2, userId, userAccount)) + return new { success = false, message = "当前用户不在其他收入单二级审核人员范围内" }; + } + + _db.BeginTran(); + try + { + var row = await _db.Queryable().FirstAsync(p => p.Id == id); + if (row == null) + { + _db.RollbackTran(); + return new { success = false, message = "单据不存在" }; + } + AppendSpbz(row, "审核不通过", remark); + row.Djzt = "审核不通过"; + row.Shr = userId; + row.Shr1 = null; + row.Shr2 = null; + await _db.Updateable(row).UpdateColumns(x => new { x.Djzt, x.Spbz, x.Shr, x.Shr1, x.Shr2 }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "已标记为审核不通过" }; + } + catch (Exception ex) + { + _db.RollbackTran(); + return new { success = false, message = $"操作失败: {ex.Message}" }; + } + } + + /// + /// 与 ReverseApproval 对接:已审核或一级已审 → 待审核 + /// + public async Task ReverseAsync(string id) + { + EnsureFysrdAuditColumns(); + try + { + var entity = await _db.Queryable().FirstAsync(p => p.Id == id); + if (entity == null) + return new { success = false, message = "单据不存在" }; + if (!IsOtherIncomeDjlx(entity.Djlx)) + return new { success = false, message = "该单据不是其他收入单" }; + + if (entity.Djzt != "已审核" && entity.Djzt != "一级已审" && entity.Djzt != "一级已审/待二级" && entity.Djzt != "待二级") + return new { success = false, message = "当前单据状态不允许反审" }; + + var userInfo = await _userManager.GetUserInfo(); + var userId = userInfo?.userId ?? ""; + var userAccount = userInfo?.userAccount?.Trim(); + if (string.IsNullOrEmpty(userAccount)) + userAccount = _userManager.Account?.Trim(); + + var approvalConfig = await _db.Queryable() + .Where(c => c.Djmc == BillName) + .FirstAsync(); + + var configShr1 = approvalConfig?.Shr1; + var configShr2 = approvalConfig?.Shr2; + var bothUnset = string.IsNullOrWhiteSpace(configShr1) && string.IsNullOrWhiteSpace(configShr2); + var isAuthorized = bothUnset + || IsUserInAuditConfig(configShr1 ?? "", userId, userAccount) + || IsUserInAuditConfig(configShr2 ?? "", userId, userAccount); + + if (!isAuthorized) + return new { success = false, message = "您没有反审权限,只有一级或二级审核人可以反审" }; + + entity.Djzt = "待审核"; + entity.Shr = null; + entity.Shr1 = null; + entity.Shr2 = null; + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr, x.Shr1, x.Shr2 }).ExecuteCommandAsync(); + return new { success = true, message = "反审成功,单据已恢复为待审核状态" }; + } + catch (Exception ex) + { + return new { success = false, message = $"反审失败: {ex.Message}" }; + } + } + } +} diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtXsckdService.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtXsckdService.cs index 101fe05..dc56895 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtXsckdService.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtXsckdService.cs @@ -50,6 +50,7 @@ namespace NCC.Extend.WtXsckd private readonly WtBillSummaryService _billSummaryService; private readonly WtDjzyService _wtDjzyService; private readonly WtTjd.WtTjdWorkflowHelper _tjdWorkflowHelper; + private readonly WtFysrd.WtFysrdWorkflowHelper _fysrdWorkflowHelper; // 序列号生成信号量,确保线程安全 private static readonly SemaphoreSlim _serialNumberSemaphore = new SemaphoreSlim(1, 1); @@ -1691,7 +1692,8 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') IUserManager userManager, WtBillSummaryService billSummaryService, WtDjzyService wtDjzyService, - WtTjd.WtTjdWorkflowHelper tjdWorkflowHelper) + WtTjd.WtTjdWorkflowHelper tjdWorkflowHelper, + WtFysrd.WtFysrdWorkflowHelper fysrdWorkflowHelper) { _wtXsckdRepository = wtXsckdRepository; _db = _wtXsckdRepository.Context; @@ -1700,6 +1702,7 @@ WHERE d.djlx IN ('销售退货单','预售退货单','委托代销退货单') _billSummaryService = billSummaryService; _wtDjzyService = wtDjzyService; _tjdWorkflowHelper = tjdWorkflowHelper; + _fysrdWorkflowHelper = fysrdWorkflowHelper; } /// @@ -4453,7 +4456,12 @@ ORDER BY IFNULL(MAX(pp.F_Ppmc), ''), IFNULL(MAX(s.F_Spbm), '')"; AddInCondition("c.ck", input.Warehouse, "stockWh"); AddInCondition("s.F_Pp", input.Brand, "stockPp"); - if (!string.IsNullOrWhiteSpace(input.Product)) + if (!string.IsNullOrWhiteSpace(input.ProductSpId)) + { + whereList.Add("s.F_Id = @stockProdId"); + paramList.Add(new SugarParameter("@stockProdId", input.ProductSpId.Trim())); + } + else if (!string.IsNullOrWhiteSpace(input.Product)) { whereList.Add("(s.F_Spmc LIKE @stockProdKw OR s.F_Spbm LIKE @stockProdKw)"); paramList.Add(new SugarParameter("@stockProdKw", $"%{input.Product.Trim()}%")); @@ -4464,6 +4472,51 @@ ORDER BY IFNULL(MAX(pp.F_Ppmc), ''), IFNULL(MAX(s.F_Spbm), '')"; } /// + /// 商品库存汇总(按仓库聚合):用于“选择某个商品后,展示该商品在各仓数量/成本”。 + /// + /// 必须提供 ProductSpId;可选 Warehouse(限定仓库范围) + [HttpGet("Actions/GetStockSummaryByWarehouse")] + public async Task GetStockSummaryByWarehouse([FromQuery] WtStockSummaryQueryInput input) + { + input ??= new WtStockSummaryQueryInput(); + if (string.IsNullOrWhiteSpace(input.ProductSpId)) + { + throw NCCException.Oh("商品 productSpId 不能为空"); + } + + // 仓库维度不需要分类/品牌筛选,但允许传 warehouse 限定仓库范围 + var (whereSql, paramList) = BuildWtStockCostSummaryWhere( + new WtStockSummaryQueryInput + { + Warehouse = input.Warehouse, + ProductSpId = input.ProductSpId + }, + null); + + var sql = $@" +SELECT + c.ck AS `仓库Id`, + IFNULL(NULLIF(TRIM(ck.F_mdmc), ''), CONCAT('未知仓库(', c.ck, ')')) AS `仓库名称`, + IFNULL(NULLIF(TRIM(MAX(s.F_Spbm)), ''), '无') AS `商品编码`, + IFNULL(NULLIF(TRIM(MAX(s.F_Spmc)), ''), '无') AS `商品名称`, + SUM(CAST(c.sl AS DECIMAL(18,4))) AS `数量`, + CASE WHEN SUM(CAST(c.sl AS DECIMAL(18,4))) = 0 THEN 0 + ELSE SUM(CAST(c.sl AS DECIMAL(18,4)) * c.cbj) / SUM(CAST(c.sl AS DECIMAL(18,4))) END AS `成本均价`, + SUM(CAST(c.sl AS DECIMAL(18,4)) * c.cbj) AS `总成本` +FROM wt_sp_cost c +INNER JOIN wt_sp s ON s.F_Id = c.spbh +LEFT JOIN wt_ck ck ON ck.F_Id = c.ck +WHERE c.sl <> 0 + AND c.ck IS NOT NULL AND TRIM(c.ck) <> '' + {whereSql} +GROUP BY c.ck, ck.F_mdmc +ORDER BY SUM(CAST(c.sl AS DECIMAL(18,4)) * c.cbj) DESC"; + + var list = await _db.Ado.GetDataTableAsync(sql, paramList); + return list; + } + + /// /// 获取指定商品在指定门店/仓库范围内的库存数量(与商品库存汇总口径一致:仅汇总 wt_sp_cost.sl,不按序列号条数统计;大量无序列号商品亦适用)。 /// /// 商品ID @@ -4580,11 +4633,135 @@ INNER JOIN wt_xsckd d ON d.F_Id = mx.djbh LEFT JOIN wt_sp sp ON sp.F_Id = mx.spbh LEFT JOIN wt_pl pl ON pl.F_Id = sp.F_Pl LEFT JOIN wt_pp pp ON pp.F_Id = sp.F_Pp -LEFT JOIN wt_wldw w ON w.F_Id = d.gys -LEFT JOIN BASE_USER u ON u.F_Id = d.jsr +LEFT JOIN wt_wldw w ON w.F_Id = IFNULL(NULLIF(TRIM(d.gys), ''), d.kh) +LEFT JOIN BASE_USER u ON u.F_Id = IFNULL(NULLIF(TRIM(d.jsr), ''), d.zdr) LEFT JOIN wt_ck ck ON ck.F_Id = IFNULL(NULLIF(TRIM(mx.rkck), ''), d.rkck) WHERE 1 = 1"; + /// 采购汇总限定单据类型(默认全集)。 + private static readonly string[] PurSumAllowedDjlx = + { + "采购入库单", "采购退货单", "销售出库单", "销售退货单", "预售出库单", "预售退货单", + "委托代销发货单", "委托代销退货单", "委托代销结算单" + }; + + /// 主表 d.djlx 加减符号(与明细 mx 解耦)。 + private const string PurSumBizSignCase = + "CASE WHEN d.djlx IN ('采购入库单','销售退货单','预售退货单','委托代销退货单') THEN 1 " + + "WHEN d.djlx IN ('采购退货单','销售出库单','预售出库单','委托代销发货单','委托代销结算单') THEN -1 ELSE 0 END"; + + // 注意:部分退货/冲销类单据明细 mx.sl/mx.je 可能已为负数;若再按单据类型乘 -1 会出现“双重取反”导致统计方向错误。 + // 统一口径:先取 ABS,再按单据类型决定正负。 + private static string PurSumSignedSlExpr => $"(ABS(CAST(mx.sl AS DECIMAL(18,4))) * ({PurSumBizSignCase}))"; + + private static string PurSumSignedJeExpr => $"(ABS(CAST(mx.je AS DECIMAL(18,4))) * ({PurSumBizSignCase}))"; + + // 单价保持正数展示(方向由数量/金额体现),避免出现负单价 + private static string PurSumSignedDjExpr => "(ABS(CAST(mx.dj AS DECIMAL(18,6))))"; + + private static string NormalizePurSumDjlx(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return raw; + } + + var t = raw.Trim(); + return t == "委托代销结算" ? "委托代销结算单" : t; + } + + /// + /// 采购汇总聚合 ORDER BY(白名单)。 + /// + private static string BuildPurchaseSummaryAggOrderBy(string kind, string sortField, string sortOrder) + { + var dir = string.Equals(sortOrder?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) + ? "ASC" + : "DESC"; + + var sumJe = $"SUM({PurSumSignedJeExpr})"; + var sumSl = $"SUM({PurSumSignedSlExpr})"; + var avgDj = $"CASE WHEN ABS({sumSl}) < 0.00000001 THEN 0 ELSE {sumJe} / {sumSl} END"; + + if (string.Equals(kind, "category", StringComparison.OrdinalIgnoreCase)) + { + var col = (sortField?.Trim()) switch + { + "分类Id" or "categoryId" => "IFNULL(NULLIF(TRIM(sp.F_Pl), ''), '__NONE__')", + "分类名称" or "categoryName" => "IFNULL(NULLIF(TRIM(MAX(pl.F_Plmc)), ''), '无')", + "数量" or "qty" => sumSl, + "入库单价" or "inboundPrice" or "unitPrice" => avgDj, + "采购金额" or "purchaseAmount" => sumJe, + _ => sumJe + }; + return $"{col} {dir}"; + } + + if (string.Equals(kind, "brand", StringComparison.OrdinalIgnoreCase)) + { + var col = (sortField?.Trim()) switch + { + "品牌Id" or "brandId" => "IFNULL(NULLIF(TRIM(sp.F_Pp), ''), '')", + "品牌名称" or "brandName" => "IFNULL(NULLIF(TRIM(MAX(pp.F_Ppmc)), ''), '无')", + "数量" or "qty" => sumSl, + "入库单价" or "inboundPrice" or "unitPrice" => avgDj, + "采购金额" or "purchaseAmount" => sumJe, + _ => sumJe + }; + return $"{col} {dir}"; + } + + if (string.Equals(kind, "productAgg", StringComparison.OrdinalIgnoreCase)) + { + var col = (sortField?.Trim()) switch + { + "商品Id" or "productId" => "sp.F_Id", + "商品编号" or "productCode" => "IFNULL(NULLIF(TRIM(MAX(sp.F_Spbm)), ''), '无')", + "商品名称" or "productName" => "IFNULL(NULLIF(TRIM(MAX(sp.F_Spmc)), ''), '无')", + "明细分类" or "detailCategory" => + "IFNULL(NULLIF(TRIM(CONCAT_WS(' / ', NULLIF(TRIM(sp.F_Splx1), ''), NULLIF(TRIM(sp.F_Splx2), ''))), ''), '无')", + "数量" or "qty" => sumSl, + "入库单价" or "inboundPrice" or "unitPrice" => avgDj, + "采购金额" or "purchaseAmount" => sumJe, + _ => sumJe + }; + return $"{col} {dir}"; + } + + return $"{sumJe} DESC"; + } + + /// + /// 采购明细 / 线性列表 ORDER BY(仅允许白名单列,防注入);数量金额单价按采购汇总符号口径排序。 + /// + private static string BuildPurchaseSummaryDetailOrderBy(string sortField, string sortOrder) + { + var dir = string.Equals(sortOrder?.Trim(), "asc", StringComparison.OrdinalIgnoreCase) + ? "ASC" + : "DESC"; + + var agentExpr = "IFNULL(NULLIF(TRIM(u.F_RealName), ''), '无')"; + + var col = sortField?.Trim() switch + { + "单据日期" or "billDate" => "d.djrq", + "单据编号" or "billId" => "d.F_Id", + "单据类型" or "billType" => "d.djlx", + "往来单位" or "contactName" => "IFNULL(NULLIF(TRIM(w.dwmc), ''), '无')", + "经手人" or "agentName" => agentExpr, + "仓库名称" or "warehouseName" => "IFNULL(NULLIF(TRIM(ck.F_mdmc), ''), '无')", + "商品名称" or "productName" => "IFNULL(NULLIF(TRIM(mx.spmc), ''), '无')", + "数量" or "qty" => PurSumSignedSlExpr, + "入库单价" or "inboundPrice" or "unitPrice" => PurSumSignedDjExpr, + "采购金额" or "purchaseAmount" => PurSumSignedJeExpr, + _ => null + }; + + return string.IsNullOrEmpty(col) + ? "d.djrq DESC, d.F_Id DESC, mx.F_Id" + : $"{col} {dir}, d.F_Id DESC, mx.F_Id"; + } + /// /// 采购汇总公共 WHERE(需配合含 sp/pl/pp 的 PurchaseSummaryJoinFromSql 使用)。 /// @@ -4644,22 +4821,56 @@ WHERE 1 = 1"; paramList.Add(new SugarParameter("@purSumEndDate", endDate.Date.AddDays(1))); } - AddInCondition("d.gys", input.ContactUnit, "purSumContact"); - - // 经手人:主表 wt_xsckd.jsr 存 BASE_USER.F_Id,按用户主键 IN 筛选(与前端下拉 value 一致) - AddInCondition("d.jsr", input.Agent, "purSumJsr"); + whereList.Add("d.djzt = @purSumDjzt"); + paramList.Add(new SugarParameter("@purSumDjzt", "已审核")); - AddInCondition("IFNULL(NULLIF(TRIM(mx.rkck), ''), d.rkck)", input.Warehouse, "purSumWh"); - - var billTypeValues = SplitCsv(input.BillType); + var allowedSet = new HashSet(PurSumAllowedDjlx, StringComparer.Ordinal); + var billRaw = input.BillType?.Trim(); + var billAll = string.IsNullOrEmpty(billRaw) || billRaw.Equals("all", StringComparison.OrdinalIgnoreCase); + var billRequested = SplitCsv(input.BillType).Select(NormalizePurSumDjlx).Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); + var billTypeValues = billAll || billRequested.Count == 0 + ? PurSumAllowedDjlx.ToList() + : billRequested.Where(allowedSet.Contains).Distinct().ToList(); if (billTypeValues.Count == 0) { - billTypeValues.Add("采购入库单"); - billTypeValues.Add("采购退货单"); + billTypeValues = PurSumAllowedDjlx.ToList(); } AddInCondition("d.djlx", string.Join(",", billTypeValues), "purSumDjlx"); + var contactVals = SplitCsv(input.ContactUnit); + if (contactVals.Count > 0) + { + var gysParts = new List(); + var khParts = new List(); + for (var i = 0; i < contactVals.Count; i++) + { + var pn = $"@purSumContact{i}"; + gysParts.Add($"d.gys = {pn}"); + khParts.Add($"d.kh = {pn}"); + paramList.Add(new SugarParameter(pn, contactVals[i])); + } + + whereList.Add($"(({string.Join(" OR ", gysParts)}) OR ({string.Join(" OR ", khParts)}))"); + } + + var agentVals = SplitCsv(input.Agent); + if (agentVals.Count > 0) + { + var agentParts = new List(); + for (var i = 0; i < agentVals.Count; i++) + { + var pn = $"@purSumJsr{i}"; + agentParts.Add( + $"IFNULL(NULLIF(TRIM(d.jsr), ''), IFNULL(NULLIF(TRIM(d.zdr), ''), '')) = {pn}"); + paramList.Add(new SugarParameter(pn, agentVals[i])); + } + + whereList.Add($"({string.Join(" OR ", agentParts)})"); + } + + AddInCondition("IFNULL(NULLIF(TRIM(mx.rkck), ''), d.rkck)", input.Warehouse, "purSumWh"); + var productValues = SplitCsv(input.Product); if (productValues.Count > 0) { @@ -4668,7 +4879,8 @@ WHERE 1 = 1"; { var pEq = $"@purProdEq{i}"; var pLike = $"@purProdLike{i}"; - productConditions.Add($"(mx.spbh = {pEq} OR sp.F_Spbm = {pEq} OR mx.spmc LIKE {pLike} OR IFNULL(sp.F_Spmc, '') LIKE {pLike})"); + productConditions.Add( + $"(mx.spbh = {pEq} OR sp.F_Id = {pEq} OR sp.F_Spbm = {pEq} OR mx.spmc LIKE {pLike} OR IFNULL(sp.F_Spmc, '') LIKE {pLike})"); paramList.Add(new SugarParameter(pEq, productValues[i])); paramList.Add(new SugarParameter(pLike, $"%{productValues[i]}%")); } @@ -4697,7 +4909,7 @@ WHERE 1 = 1"; var spId = !string.IsNullOrWhiteSpace(scopeProductSpId) ? scopeProductSpId.Trim() : (input.ProductSpId ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(spId)) { - whereList.Add("mx.spbh = @purSumScopeSp"); + whereList.Add("(mx.spbh = @purSumScopeSp OR sp.F_Id = @purSumScopeSp)"); paramList.Add(new SugarParameter("@purSumScopeSp", spId)); } @@ -4713,20 +4925,21 @@ WHERE 1 = 1"; { input ??= new WtPurchaseSummaryQueryInput(); var (whereSql, paramList) = BuildPurchaseSummaryWhere(input, null, null, null); + var orderBy = BuildPurchaseSummaryAggOrderBy("category", input.SortField, input.SortOrder); var sql = $@" SELECT IFNULL(NULLIF(TRIM(sp.F_Pl), ''), '__NONE__') AS `分类Id`, IFNULL(NULLIF(TRIM(MAX(pl.F_Plmc)), ''), '无') AS `分类名称`, '无' AS `商品编号`, IFNULL(NULLIF(TRIM(MAX(pl.F_Plmc)), ''), '无') AS `商品名称`, - SUM(CAST(mx.sl AS DECIMAL(18,4))) AS `数量`, - CASE WHEN ABS(SUM(CAST(mx.sl AS DECIMAL(18,4)))) < 0.00000001 THEN 0 - ELSE SUM(CAST(mx.je AS DECIMAL(18,4))) / SUM(CAST(mx.sl AS DECIMAL(18,4))) END AS `入库单价`, - SUM(CAST(mx.je AS DECIMAL(18,4))) AS `采购金额` + SUM({PurSumSignedSlExpr}) AS `数量`, + CASE WHEN ABS(SUM({PurSumSignedSlExpr})) < 0.00000001 THEN 0 + ELSE SUM({PurSumSignedJeExpr}) / SUM({PurSumSignedSlExpr}) END AS `入库单价`, + SUM({PurSumSignedJeExpr}) AS `采购金额` {PurchaseSummaryJoinFromSql} {whereSql} GROUP BY sp.F_Pl, pl.F_Plmc -ORDER BY SUM(CAST(mx.je AS DECIMAL(18,4))) DESC"; +ORDER BY {orderBy}"; var list = await _db.Ado.GetDataTableAsync(sql, paramList); return list; } @@ -4744,20 +4957,21 @@ ORDER BY SUM(CAST(mx.je AS DECIMAL(18,4))) DESC"; input ??= new WtPurchaseSummaryQueryInput(); var (whereSql, paramList) = BuildPurchaseSummaryWhere(input, categoryId, null, null); + var orderBy = BuildPurchaseSummaryAggOrderBy("brand", input.SortField, input.SortOrder); var sql = $@" SELECT IFNULL(NULLIF(TRIM(sp.F_Pp), ''), '') AS `品牌Id`, IFNULL(NULLIF(TRIM(MAX(pp.F_Ppmc)), ''), '无') AS `品牌名称`, '无' AS `商品编号`, IFNULL(NULLIF(TRIM(MAX(pp.F_Ppmc)), ''), '无') AS `商品名称`, - SUM(CAST(mx.sl AS DECIMAL(18,4))) AS `数量`, - CASE WHEN ABS(SUM(CAST(mx.sl AS DECIMAL(18,4)))) < 0.00000001 THEN 0 - ELSE SUM(CAST(mx.je AS DECIMAL(18,4))) / SUM(CAST(mx.sl AS DECIMAL(18,4))) END AS `入库单价`, - SUM(CAST(mx.je AS DECIMAL(18,4))) AS `采购金额` + SUM({PurSumSignedSlExpr}) AS `数量`, + CASE WHEN ABS(SUM({PurSumSignedSlExpr})) < 0.00000001 THEN 0 + ELSE SUM({PurSumSignedJeExpr}) / SUM({PurSumSignedSlExpr}) END AS `入库单价`, + SUM({PurSumSignedJeExpr}) AS `采购金额` {PurchaseSummaryJoinFromSql} {whereSql} GROUP BY sp.F_Pp, pp.F_Ppmc -ORDER BY SUM(CAST(mx.je AS DECIMAL(18,4))) DESC"; +ORDER BY {orderBy}"; var list = await _db.Ado.GetDataTableAsync(sql, paramList); return list; } @@ -4780,26 +4994,27 @@ ORDER BY SUM(CAST(mx.je AS DECIMAL(18,4))) DESC"; input ??= new WtPurchaseSummaryQueryInput(); var (whereSql, paramList) = BuildPurchaseSummaryWhere(input, categoryId, brandId, null); + var orderBy = BuildPurchaseSummaryAggOrderBy("productAgg", input.SortField, input.SortOrder); var sql = $@" SELECT sp.F_Id AS `商品Id`, IFNULL(NULLIF(TRIM(MAX(sp.F_Spbm)), ''), '无') AS `商品编号`, IFNULL(NULLIF(TRIM(MAX(sp.F_Spmc)), ''), '无') AS `商品名称`, IFNULL(NULLIF(TRIM(CONCAT_WS(' / ', NULLIF(TRIM(sp.F_Splx1), ''), NULLIF(TRIM(sp.F_Splx2), ''))), ''), '无') AS `明细分类`, - SUM(CAST(mx.sl AS DECIMAL(18,4))) AS `数量`, - CASE WHEN ABS(SUM(CAST(mx.sl AS DECIMAL(18,4)))) < 0.00000001 THEN 0 - ELSE SUM(CAST(mx.je AS DECIMAL(18,4))) / SUM(CAST(mx.sl AS DECIMAL(18,4))) END AS `入库单价`, - SUM(CAST(mx.je AS DECIMAL(18,4))) AS `采购金额` + SUM({PurSumSignedSlExpr}) AS `数量`, + CASE WHEN ABS(SUM({PurSumSignedSlExpr})) < 0.00000001 THEN 0 + ELSE SUM({PurSumSignedJeExpr}) / SUM({PurSumSignedSlExpr}) END AS `入库单价`, + SUM({PurSumSignedJeExpr}) AS `采购金额` {PurchaseSummaryJoinFromSql} {whereSql} GROUP BY sp.F_Id, sp.F_Splx1, sp.F_Splx2 -ORDER BY IFNULL(MAX(sp.F_Spbm), ''), IFNULL(MAX(sp.F_Spmc), '')"; +ORDER BY {orderBy}"; var list = await _db.Ado.GetDataTableAsync(sql, paramList); return list; } /// - /// 商品采购「线性列表」:指定分类下全部采购明细行(最多 2000 条),与明细列表列一致。 + /// 商品采购「线性列表」:指定分类下全部相关单据明细(分页,与 GetPurchaseSummary 筛选及加减口径一致)。 /// [HttpGet("Actions/GetPurchaseSummaryLinear")] public async Task GetPurchaseSummaryLinear([FromQuery] string categoryId, [FromQuery] WtPurchaseSummaryQueryInput input = null) @@ -4811,25 +5026,50 @@ ORDER BY IFNULL(MAX(sp.F_Spbm), ''), IFNULL(MAX(sp.F_Spmc), '')"; input ??= new WtPurchaseSummaryQueryInput(); var (whereSql, paramList) = BuildPurchaseSummaryWhere(input, categoryId, null, null); - const int maxRows = 2000; + + var page = input.CurrentPage.GetValueOrDefault(1); + if (page < 1) + { + page = 1; + } + + // 线性列表单次可拉较大窗口(默认 2000),与历史「最多 2000 条」一致;仍支持分页防一次过大。 + var pageSize = input.PageSize.GetValueOrDefault(2000); + if (pageSize < 1) + { + pageSize = 2000; + } + + if (pageSize > 2000) + { + pageSize = 2000; + } + + var offset = (page - 1) * pageSize; + + var countSql = "SELECT COUNT(*) " + PurchaseSummaryJoinFromSql + whereSql; + var totalObj = await _db.Ado.GetScalarAsync(countSql, paramList); + var total = Convert.ToInt32(totalObj ?? 0); + + var orderByLin = BuildPurchaseSummaryDetailOrderBy(input.SortField, input.SortOrder); var sql = $@" SELECT DATE_FORMAT(d.djrq, '%Y-%m-%d') AS `单据日期`, d.F_Id AS `单据编号`, d.djlx AS `单据类型`, IFNULL(NULLIF(TRIM(w.dwmc), ''), '无') AS `往来单位`, - IFNULL(NULLIF(TRIM(u.F_RealName), ''), IFNULL(NULLIF(TRIM(d.jsr), ''), '无')) AS `经手人`, + IFNULL(NULLIF(TRIM(u.F_RealName), ''), '无') AS `经手人`, IFNULL(NULLIF(TRIM(ck.F_mdmc), ''), '无') AS `仓库名称`, IFNULL(NULLIF(TRIM(mx.spmc), ''), '无') AS `商品名称`, - CAST(mx.sl AS DECIMAL(18,4)) AS `数量`, - mx.dj AS `入库单价`, - mx.je AS `采购金额` + {PurSumSignedSlExpr} AS `数量`, + {PurSumSignedDjExpr} AS `入库单价`, + {PurSumSignedJeExpr} AS `采购金额` {PurchaseSummaryJoinFromSql} {whereSql} -ORDER BY d.djrq DESC, d.F_Id, mx.F_Id -LIMIT {maxRows}"; +ORDER BY {orderByLin} +LIMIT {offset}, {pageSize}"; var list = await _db.Ado.GetDataTableAsync(sql, paramList); - return list; + return new { list, total }; } /// @@ -4866,21 +5106,22 @@ LIMIT {maxRows}"; var totalObj = await _db.Ado.GetScalarAsync(countSql, paramList); var total = Convert.ToInt32(totalObj ?? 0); + var orderByDetail = BuildPurchaseSummaryDetailOrderBy(input.SortField, input.SortOrder); var listSql = $@" SELECT DATE_FORMAT(d.djrq, '%Y-%m-%d') AS `单据日期`, d.F_Id AS `单据编号`, d.djlx AS `单据类型`, IFNULL(NULLIF(TRIM(w.dwmc), ''), '无') AS `往来单位`, - IFNULL(NULLIF(TRIM(u.F_RealName), ''), IFNULL(NULLIF(TRIM(d.jsr), ''), '无')) AS `经手人`, + IFNULL(NULLIF(TRIM(u.F_RealName), ''), '无') AS `经手人`, IFNULL(NULLIF(TRIM(ck.F_mdmc), ''), '无') AS `仓库名称`, IFNULL(NULLIF(TRIM(mx.spmc), ''), '无') AS `商品名称`, - CAST(mx.sl AS DECIMAL(18,4)) AS `数量`, - mx.dj AS `入库单价`, - mx.je AS `采购金额` + {PurSumSignedSlExpr} AS `数量`, + {PurSumSignedDjExpr} AS `入库单价`, + {PurSumSignedJeExpr} AS `采购金额` {PurchaseSummaryJoinFromSql} {whereSql} -ORDER BY d.djrq DESC, d.F_Id, mx.F_Id +ORDER BY {orderByDetail} LIMIT {offset}, {pageSize}"; var list = await _db.Ado.GetDataTableAsync(listSql, paramList); return new { list, total }; @@ -5744,11 +5985,13 @@ LIMIT {offset}, {pageSize}"; } /// - /// 通用审核接口:自动根据单据的 djlx 匹配审核配置;编号以 TJD 开头时优先判断是否为 wt_xsckd 同价调拨单,否则走商品调价单审批流 + /// 通用审核接口:自动根据单据的 djlx 匹配审核配置;编号以 TJD 开头时优先判断是否为 wt_xsckd 同价调拨单,否则走商品调价单审批流;wt_fysrd 其他收入单走费用收入单审批流 /// [HttpPost("ApproveGeneric/{id}")] public async Task ApproveGeneric(string id, [FromBody] WtApprovalRemarkInput input) { + if (!string.IsNullOrEmpty(id) && await _db.Queryable().AnyAsync(x => x.Id == id)) + return await _fysrdWorkflowHelper.ApproveAsync(id, input?.remark); if (!string.IsNullOrEmpty(id) && id.StartsWith("TJD", StringComparison.Ordinal)) { var isSamePriceTransfer = await _db.Queryable() @@ -5768,6 +6011,8 @@ LIMIT {offset}, {pageSize}"; [HttpPost("RejectGeneric/{id}")] public async Task RejectGeneric(string id, [FromBody] WtApprovalRemarkInput input) { + if (!string.IsNullOrEmpty(id) && await _db.Queryable().AnyAsync(x => x.Id == id)) + return await _fysrdWorkflowHelper.RejectAsync(id, input?.remark); if (!string.IsNullOrEmpty(id) && id.StartsWith("TJD", StringComparison.Ordinal)) { var isSamePriceTransfer = await _db.Queryable() @@ -5942,6 +6187,9 @@ LIMIT {offset}, {pageSize}"; { try { + if (!string.IsNullOrEmpty(id) && await _db.Queryable().AnyAsync(x => x.Id == id)) + return await _fysrdWorkflowHelper.ReverseAsync(id); + var entity = await _db.Queryable().Where(x => x.Id == id).FirstAsync(); if (entity == null) return new { success = false, message = "单据不存在" }; diff --git a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtYskzjjsService.cs b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtYskzjjsService.cs index ff67f4a..d0cc931 100644 --- a/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtYskzjjsService.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/Extend/NCC.Extend/WtYskzjjsService.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using NCC.Extend.Entitys; +using NCC.Extend.Entitys.Dto; using NCC.Extend.Entitys.Dto.WtYskzjjs; using Yitter.IdGenerator; using NCC.Common.Helper; @@ -40,6 +41,7 @@ namespace NCC.Extend.WtYskzjjs private readonly IUserManager _userManager; private readonly WtDjzyService _wtDjzyService; private readonly WtBillSummaryService _billSummaryService; + private static bool _yskAuditColsEnsured; /// /// 初始化一个类型的新实例 @@ -60,6 +62,39 @@ namespace NCC.Extend.WtYskzjjs } /// + /// 确保 wt_yskzjjs 具备审批相关列(避免线上旧库缺列导致 Unknown column)。 + /// + private void EnsureYskzjjsAuditColumns() + { + if (_yskAuditColsEnsured) return; + lock (typeof(WtYskzjjsService)) + { + if (_yskAuditColsEnsured) return; + try + { + if (!_db.DbMaintenance.IsAnyTable("wt_yskzjjs")) { _yskAuditColsEnsured = true; return; } + var cols = _db.DbMaintenance.GetColumnInfosByTableName("wt_yskzjjs"); + var names = cols.Select(c => c.DbColumnName.ToLowerInvariant()).ToHashSet(); + if (!names.Contains("djzt")) + _db.Ado.ExecuteCommand("ALTER TABLE `wt_yskzjjs` ADD COLUMN `djzt` varchar(32) NULL COMMENT '单据状态'"); + if (!names.Contains("spbz")) + _db.Ado.ExecuteCommand("ALTER TABLE `wt_yskzjjs` ADD COLUMN `spbz` text NULL COMMENT '审批备注'"); + if (!names.Contains("shr")) + _db.Ado.ExecuteCommand("ALTER TABLE `wt_yskzjjs` ADD COLUMN `shr` varchar(64) NULL COMMENT '审核人'"); + if (!names.Contains("shr1")) + _db.Ado.ExecuteCommand("ALTER TABLE `wt_yskzjjs` ADD COLUMN `shr1` varchar(500) NULL COMMENT '一级审核人'"); + if (!names.Contains("shr2")) + _db.Ado.ExecuteCommand("ALTER TABLE `wt_yskzjjs` ADD COLUMN `shr2` varchar(500) NULL COMMENT '二级审核人'"); + } + catch (Exception ex) + { + Console.WriteLine($"EnsureYskzjjsAuditColumns: {ex.Message}"); + } + _yskAuditColsEnsured = true; + } + } + + /// /// 获取应收款增加减少 /// /// 参数 @@ -67,12 +102,14 @@ namespace NCC.Extend.WtYskzjjs [HttpGet("{id}")] public async Task GetInfo(string id) { + EnsureYskzjjsAuditColumns(); var entity = await _db.Queryable().FirstAsync(p => p.Id == id); var output = entity.Adapt(); var wtYskzjjsMxList = await _db.Queryable().Where(w => w.Djbh == entity.Id).ToListAsync(); output.wtYskzjjsMxList = wtYskzjjsMxList.Adapt>(); output.billZy = await _billSummaryService.ComputeWtYskzjjsZyByBillIdAsync(entity.Id); + output.wldwmc = await ResolveWldwMcAsync(output.wldw); return output; } @@ -84,6 +121,7 @@ namespace NCC.Extend.WtYskzjjs [HttpGet("")] public async Task GetList([FromQuery] WtYskzjjsListQueryInput input) { + EnsureYskzjjsAuditColumns(); var sidx = input.sidx == null ? "id" : input.sidx; List queryDjrq = input.djrq != null ? input.djrq.Split(',').ToObeject>() : null; DateTime? startDjrq = queryDjrq != null ? Ext.GetDateTime(queryDjrq.First()) : null; @@ -113,6 +151,10 @@ namespace NCC.Extend.WtYskzjjs djrq = d.Djrq, jsr = d.Jsr, djzt = d.Djzt, + spbz = d.Spbz, + shr = d.Shr, + shr1 = d.Shr1, + shr2 = d.Shr2, dyddh = d.Dyddh, hysjh = d.Hysjh, fkzh = d.Fkzh, @@ -132,6 +174,10 @@ namespace NCC.Extend.WtYskzjjs djrq = it.djrq, jsr = it.jsr, djzt = it.djzt, + spbz = it.spbz, + shr = it.shr, + shr1 = it.shr1, + shr2 = it.shr2, dyddh = it.dyddh, hysjh = it.hysjh, fkzh = it.fkzh, @@ -158,9 +204,12 @@ namespace NCC.Extend.WtYskzjjs [HttpPost("")] public async Task Create([FromBody] WtYskzjjsCrInput input) { + EnsureYskzjjsAuditColumns(); var userInfo = await _userManager.GetUserInfo(); NormalizeDjlxForQt(input); NormalizeDjlxForTransfer(input); + SyncMxKhFromWldw(input); + ThrowIfInvalidForSubmit(input, input?.djzt); var entity = input.Adapt(); entity.Id = await BuildCreateBillId(input); try @@ -264,6 +313,10 @@ namespace NCC.Extend.WtYskzjjs djrq = d.Djrq, jsr = d.Jsr, djzt = d.Djzt, + spbz = d.Spbz, + shr = d.Shr, + shr1 = d.Shr1, + shr2 = d.Shr2, dyddh = d.Dyddh, hysjh = d.Hysjh, fkzh = d.Fkzh, @@ -283,6 +336,10 @@ namespace NCC.Extend.WtYskzjjs djrq = it.djrq, jsr = it.jsr, djzt = it.djzt, + spbz = it.spbz, + shr = it.shr, + shr1 = it.shr1, + shr2 = it.shr2, dyddh = it.dyddh, hysjh = it.hysjh, fkzh = it.fkzh, @@ -389,8 +446,11 @@ namespace NCC.Extend.WtYskzjjs [HttpPut("{id}")] public async Task Update(string id, [FromBody] WtYskzjjsUpInput input) { + EnsureYskzjjsAuditColumns(); NormalizeDjlxForQt(input); NormalizeDjlxForTransfer(input); + SyncMxKhFromWldw(input); + ThrowIfInvalidForSubmit(input, input?.djzt); var entity = input.Adapt(); try { @@ -461,7 +521,7 @@ namespace NCC.Extend.WtYskzjjs } /// - /// 兼容 qt 页面对 djlx 的历史传值:当明细为“其他收入单”时统一落库为“其他应收款”。 + /// 兼容 qt 页面对 djlx 的历史传值:当明细为“其他收入单”时统一落库为“其他应收单”。 /// 仅修正 qt 场景,不影响其它单据类型。 /// /// 新增/更新入参 @@ -478,9 +538,10 @@ namespace NCC.Extend.WtYskzjjs if (string.IsNullOrWhiteSpace(input.djlx) || input.djlx.Trim().Equals("应收款减少", StringComparison.Ordinal) || - input.djlx.Trim().Equals("其他收入单", StringComparison.Ordinal)) + input.djlx.Trim().Equals("其他收入单", StringComparison.Ordinal) || + input.djlx.Trim().Equals("其他应收款", StringComparison.Ordinal)) { - input.djlx = "其他应收款"; + input.djlx = "其他应收单"; } } @@ -598,5 +659,326 @@ namespace NCC.Extend.WtYskzjjs await _billSummaryService.EnrichWtYskzjjsListBillZyAsync(rowList); } + + /// + /// 主表往来单位写入明细客户字段,便于列表按 kh 筛选与详情列展示。 + /// + private static void SyncMxKhFromWldw(WtYskzjjsCrInput input) + { + if (input == null || string.IsNullOrWhiteSpace(input.wldw) || input.wtYskzjjsMxList == null) + return; + var w = input.wldw.Trim(); + foreach (var mx in input.wtYskzjjsMxList.Where(m => m != null)) + { + if (string.IsNullOrWhiteSpace(mx.kh)) + mx.kh = w; + } + } + + private async Task ResolveWldwMcAsync(string wldwId) + { + if (string.IsNullOrWhiteSpace(wldwId)) + return "无"; + var dwmc = (await _db.Queryable().Where(x => x.Id == wldwId).Select(x => x.Dwmc).ToListAsync()).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(dwmc)) + return dwmc; + var xm = (await _db.Queryable().Where(x => x.Id == wldwId).Select(x => x.Xm).ToListAsync()).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(xm)) + return xm; + var gysmc = (await _db.Queryable().Where(x => x.Id == wldwId).Select(x => x.Gysmc).ToListAsync()).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(gysmc)) + return gysmc; + return "无"; + } + + private static void AppendApprovalSpbzLineYsk(WtYskzjjsEntity entity, string actionTag, string remark) + { + var r = (remark ?? "").Trim(); + if (string.IsNullOrEmpty(r)) return; + var line = string.IsNullOrEmpty(actionTag) ? r : $"[{actionTag}] {r}"; + var existing = entity.Spbz?.TrimEnd() ?? ""; + entity.Spbz = string.IsNullOrWhiteSpace(existing) ? line : existing + "\n" + line; + } + + private static bool IsUserInAuditConfigYsk(string configValue, string userId, string userAccount = null) + { + if (string.IsNullOrWhiteSpace(configValue)) + return false; + if (string.IsNullOrWhiteSpace(userId) && string.IsNullOrWhiteSpace(userAccount)) + return false; + + var users = configValue + .Split(new[] { ',', ',', ';', ';', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList(); + + return users.Any(x => + (!string.IsNullOrWhiteSpace(userId) && string.Equals(x, userId.Trim(), StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrWhiteSpace(userAccount) && string.Equals(x, userAccount.Trim(), StringComparison.OrdinalIgnoreCase))); + } + + /// + /// 提交审核:草稿 / 空 / 审核不通过 → 待审核 + /// + [HttpPost("Actions/SubmitForAudit/{id}")] + public async Task SubmitForAudit(string id) + { + EnsureYskzjjsAuditColumns(); + var entity = await _db.Queryable().FirstAsync(p => p.Id == id); + if (entity == null) + return new { success = false, message = "单据不存在" }; + var z = entity.Djzt?.Trim() ?? ""; + if (z != "草稿" && z != "审核不通过" && !string.IsNullOrEmpty(z)) + return new { success = false, message = $"当前状态「{entity.Djzt}」不可提交审核" }; + + // 提交审核前做必填校验(备注除外) + var mxList = await _db.Queryable().Where(x => x.Djbh == entity.Id).ToListAsync(); + var inputLike = new WtYskzjjsCrInput + { + id = entity.Id, + djlx = entity.Djlx, + djrq = entity.Djrq, + jsr = entity.Jsr, + wldw = entity.Wldw, + djzt = "待审核", + fkzh = entity.Fkzh, + fkje = entity.Fkje, + skzh = entity.Skzh, + skje = entity.Skje, + zy = entity.Zy, + wtYskzjjsMxList = mxList?.Select(m => new WtYskzjjsMxCrInput + { + srxmmc = m.Srxmmc, + je = m.Je, + bz = m.Bz, + mxlx = m.Mxlx, + kh = m.Kh, + zhbh = m.Zhbh + }).ToList() + }; + try + { + ThrowIfInvalidForSubmit(inputLike, "待审核"); + } + catch (Exception ex) + { + return new { success = false, message = ex.Message }; + } + + entity.Djzt = "待审核"; + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt }).ExecuteCommandAsync(); + return new { success = true, message = "已提交审核" }; + } + + /// + /// 除备注外必填:仅在提交审核/非草稿状态强制校验;草稿可放宽。 + /// + private static void ThrowIfInvalidForSubmit(WtYskzjjsCrInput input, string djzt) + { + if (input == null) throw NCCException.Oh("参数不能为空"); + var z = (djzt ?? input.djzt ?? "").Trim(); + if (string.IsNullOrEmpty(z) || z == "草稿") return; + + if (input.djrq == null) throw NCCException.Oh("请选择单据日期"); + if (string.IsNullOrWhiteSpace(input.jsr)) throw NCCException.Oh("请选择经手人"); + if (string.IsNullOrWhiteSpace(input.wldw)) throw NCCException.Oh("请选择往来单位"); + if (string.IsNullOrWhiteSpace(input.skzh)) throw NCCException.Oh("请选择收款账户"); + if (input.skje <= 0) throw NCCException.Oh("请填写明细金额,收款金额须大于 0"); + if (input.wtYskzjjsMxList == null || input.wtYskzjjsMxList.Count == 0) throw NCCException.Oh("请至少添加一条明细"); + + for (var i = 0; i < input.wtYskzjjsMxList.Count; i++) + { + var row = input.wtYskzjjsMxList[i]; + if (row == null) throw NCCException.Oh($"第 {i + 1} 行明细不能为空"); + if (string.IsNullOrWhiteSpace(row.srxmmc)) throw NCCException.Oh($"第 {i + 1} 行收入项目为必填"); + if (row.je <= 0) throw NCCException.Oh($"第 {i + 1} 行原币金额为必填且须大于 0"); + } + } + + /// + /// 撤回:待审核或一级已审 → 草稿 + /// + [HttpPost("Actions/WithdrawAudit/{id}")] + public async Task WithdrawAudit(string id) + { + EnsureYskzjjsAuditColumns(); + var entity = await _db.Queryable().FirstAsync(p => p.Id == id); + if (entity == null) + return new { success = false, message = "单据不存在" }; + var z = entity.Djzt?.Trim() ?? ""; + if (z != "待审核" && z != "一级已审" && z != "待二级" && z != "一级已审/待二级") + return new { success = false, message = $"当前状态「{entity.Djzt}」不可撤回" }; + + entity.Djzt = "草稿"; + entity.Shr1 = null; + entity.Shr2 = null; + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr1, x.Shr2 }).ExecuteCommandAsync(); + return new { success = true, message = "已撤回为草稿" }; + } + + /// + /// 审核通过(按 wt_shrysz.djmc 与主表 djlx 匹配,支持一级/两级) + /// + [HttpPost("Actions/Approve/{id}")] + public async Task Approve(string id, [FromBody] WtApprovalRemarkInput input = null) + { + EnsureYskzjjsAuditColumns(); + return await ApproveYskzjjsDocumentAsync(id, input?.remark); + } + + /// + /// 审核不通过(审批备注写入 spbz) + /// + [HttpPost("Actions/Reject/{id}")] + public async Task Reject(string id, [FromBody] WtApprovalRemarkInput input = null) + { + EnsureYskzjjsAuditColumns(); + return await RejectYskzjjsDocumentAsync(id, input?.remark); + } + + private async Task ApproveYskzjjsDocumentAsync(string id, string approvalRemark) + { + try + { + _db.BeginTran(); + var entity = await _db.Queryable().Where(x => x.Id == id).FirstAsync(); + if (entity == null) + { + _db.RollbackTran(); + return new { success = false, message = "单据不存在" }; + } + + if (entity.Djzt == "已审核") + { + _db.RollbackTran(); + return new { success = false, message = "该单据已经审核,无需重复审核" }; + } + + var userInfo = await _userManager.GetUserInfo(); + var userId = userInfo?.userId ?? ""; + var userAccount = userInfo?.userAccount?.Trim(); + if (string.IsNullOrEmpty(userAccount)) + userAccount = _userManager.Account?.Trim(); + + var djmc = entity.Djlx?.Trim() ?? ""; + var approvalConfig = await _db.Queryable() + .Where(c => c.Djmc == djmc) + .FirstAsync(); + + var configShr1 = approvalConfig?.Shr1; + var configShr2 = approvalConfig?.Shr2; + var hasTwoLevel = !string.IsNullOrEmpty(configShr1) && !string.IsNullOrEmpty(configShr2); + + if (entity.Djzt == "待审核" || string.IsNullOrEmpty(entity.Djzt)) + { + if (hasTwoLevel) + { + entity.Djzt = "一级已审"; + entity.Shr1 = userId; + AppendApprovalSpbzLineYsk(entity, "一级通过", approvalRemark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr1, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "一级审核通过,等待二级审核" }; + } + + entity.Djzt = "已审核"; + entity.Shr = userId; + entity.Shr1 = userId; + AppendApprovalSpbzLineYsk(entity, "审核通过", approvalRemark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr, x.Shr1, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "审核通过" }; + } + + if (entity.Djzt == "一级已审" || entity.Djzt == "一级已审/待二级" || entity.Djzt == "待二级") + { + if (!hasTwoLevel) + { + _db.RollbackTran(); + return new { success = false, message = "未配置两级审核,当前状态不允许二级操作" }; + } + if (string.IsNullOrWhiteSpace(configShr2)) + { + _db.RollbackTran(); + return new { success = false, message = "未配置二级审核人员" }; + } + if (!IsUserInAuditConfigYsk(configShr2, userId, userAccount)) + { + _db.RollbackTran(); + return new { success = false, message = "当前用户不在二级审核人员范围内" }; + } + + entity.Djzt = "已审核"; + entity.Shr2 = userId; + entity.Shr = userId; + AppendApprovalSpbzLineYsk(entity, "二级通过", approvalRemark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Shr, x.Shr2, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "二级审核通过,审核完成" }; + } + + _db.RollbackTran(); + return new { success = false, message = $"当前单据状态「{entity.Djzt}」不允许审核" }; + } + catch (Exception ex) + { + _db.RollbackTran(); + return new { success = false, message = $"审核失败: {ex.Message}" }; + } + } + + private async Task RejectYskzjjsDocumentAsync(string id, string approvalRemark) + { + try + { + _db.BeginTran(); + var entity = await _db.Queryable().Where(x => x.Id == id).FirstAsync(); + if (entity == null) + { + _db.RollbackTran(); + return new { success = false, message = "单据不存在" }; + } + + if (entity.Djzt == "已审核") + { + _db.RollbackTran(); + return new { success = false, message = "该单据已审核通过" }; + } + + if (entity.Djzt == "审核不通过") + { + _db.RollbackTran(); + return new { success = false, message = "该单据已是审核不通过状态" }; + } + + if (entity.Djzt == "草稿") + { + _db.RollbackTran(); + return new { success = false, message = "草稿状态请直接修改或删除单据" }; + } + + var djztTrim = entity.Djzt?.Trim() ?? ""; + var level1 = djztTrim == "待审核" || string.IsNullOrEmpty(djztTrim); + var level2 = entity.Djzt == "一级已审" || entity.Djzt == "待二级" || entity.Djzt == "一级已审/待二级"; + + if (!level1 && !level2) + { + _db.RollbackTran(); + return new { success = false, message = $"当前单据状态「{entity.Djzt}」不允许审核不通过" }; + } + + entity.Djzt = "审核不通过"; + AppendApprovalSpbzLineYsk(entity, "审核不通过", approvalRemark); + await _db.Updateable(entity).UpdateColumns(x => new { x.Djzt, x.Spbz }).ExecuteCommandAsync(); + _db.CommitTran(); + return new { success = true, message = "已标记审核不通过" }; + } + catch (Exception ex) + { + _db.RollbackTran(); + return new { success = false, message = $"操作失败: {ex.Message}" }; + } + } } } diff --git a/Antis.Erp.Plat/netcore/src/Modularity/OAuth/NCC.OAuth/Service/OAuthService.cs b/Antis.Erp.Plat/netcore/src/Modularity/OAuth/NCC.OAuth/Service/OAuthService.cs index 77b6147..0f1b8b4 100755 --- a/Antis.Erp.Plat/netcore/src/Modularity/OAuth/NCC.OAuth/Service/OAuthService.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/OAuth/NCC.OAuth/Service/OAuthService.cs @@ -148,7 +148,11 @@ namespace NCC.OAuth.Service bool isMoble = false; if (input.account == user.MobilePhone) isMoble = true; //获取加密后的密码 - var encryptPasswod = MD5Encryption.Encrypt(input.password + user.Secretkey); + var pwd = (input.password ?? "").Trim(); + // 兼容:前端既可能传 MD5(明文),也可能直接传明文(如收银台)。 + // 系统入库口径为:MD5(MD5(明文) + Secretkey) + var md5Once = NormalizePasswordToMd5Once(pwd); + var encryptPasswod = MD5Encryption.Encrypt(md5Once + user.Secretkey); var userAnyPwd = await _userService.GetInfoByLogin(input.account, encryptPasswod, isMoble); _ = userAnyPwd ?? throw NCCException.Oh(ErrorCode.D1000); @@ -597,7 +601,9 @@ namespace NCC.OAuth.Service var secretkey = (await _userService.GetInfoByAccount(input.account)).Secretkey; //获取加密后的密码 - var encryptPasswod = MD5Encryption.Encrypt(input.password + secretkey); + var pwd = (input.password ?? "").Trim(); + var md5Once = NormalizePasswordToMd5Once(pwd); + var encryptPasswod = MD5Encryption.Encrypt(md5Once + secretkey); bool isMoble = false; if (input.account == users.MobilePhone) isMoble = true; @@ -606,6 +612,25 @@ namespace NCC.OAuth.Service _ = user ?? throw NCCException.Oh(ErrorCode.D1000); } + private static bool LooksLikeMd5(string s) + { + if (string.IsNullOrEmpty(s) || s.Length != 32) return false; + foreach (var ch in s) + { + var isHex = (ch >= '0' && ch <= '9') + || (ch >= 'a' && ch <= 'f') + || (ch >= 'A' && ch <= 'F'); + if (!isHex) return false; + } + return true; + } + + private static string NormalizePasswordToMd5Once(string passwordOrMd5) + { + var p = (passwordOrMd5 ?? "").Trim(); + return LooksLikeMd5(p) ? p : MD5Encryption.Encrypt(p); + } + /// /// 获取当前登录用户信息 /// diff --git a/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System.Entitys/Dto/Permission/User/UserCrInput.cs b/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System.Entitys/Dto/Permission/User/UserCrInput.cs index 2e8ee2c..ebac9b9 100755 --- a/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System.Entitys/Dto/Permission/User/UserCrInput.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System.Entitys/Dto/Permission/User/UserCrInput.cs @@ -133,8 +133,16 @@ namespace NCC.System.Entitys.Dto.Permission.User /// 排序 /// public long? sortCode { get; set; } - - + + /// + /// 初始密码(明文,可选)。为空或未传时仍按系统默认密码规则生成(与历史行为一致)。 + /// + /// + /// 非空时按与登录校验一致的口径入库:MD5(MD5(明文) + 用户 Secretkey)。 + /// (登录请求中的 password 通常为 MD5(明文),服务端再拼接 Secretkey 后做最终 MD5) + /// + public string password { get; set; } + /// /// 门店信息 /// diff --git a/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System/Service/Permission/UsersService.cs b/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System/Service/Permission/UsersService.cs index e8a6f28..862005d 100755 --- a/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System/Service/Permission/UsersService.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/System/NCC.System/Service/Permission/UsersService.cs @@ -262,10 +262,31 @@ namespace NCC.System.Service.Permission } /// - /// 新建 + /// 新建用户 /// - /// 参数 - /// + /// + /// 可选传入 password(明文)。非空时按与「重置密码」相同的规则写入库,便于新建用户后直接以明文登录; + /// 未传或为空时仍使用系统默认密码哈希(兼容旧前端)。 + /// + /// 示例请求: + /// ```json + /// { + /// "account": "zhangsan", + /// "password": "PlainTextPwd", + /// "realName": "张三", + /// "organizeId": "orgId", + /// "headIcon": "/api/file/Image/userAvatar/001.png" + /// } + /// ``` + /// + /// 参数说明: + /// - password: 可选,非空时为初始明文密码 + /// + /// 创建用户参数 + /// 无返回体,成功即完成创建 + /// 创建成功 + /// 参数或业务校验失败 + /// 服务器内部错误 [HttpPost("")] public async Task Create([FromBody] UserCrInput input) { @@ -294,7 +315,12 @@ namespace NCC.System.Service.Permission entity.Birthday = input.birthday.IsNullOrEmpty() ? DateTime.Now : Ext.GetDateTime(input.birthday.ToString()); entity.QuickQuery = PinyinUtil.PinyinString(input.realName); entity.Secretkey = Guid.NewGuid().ToString(); - entity.Password = MD5Encryption.Encrypt(MD5Encryption.Encrypt(CommonConst.DEFAULT_PASSWORD) + entity.Secretkey); + if (string.IsNullOrWhiteSpace(input.password)) + { + throw NCCException.Oh("请输入密码"); + } + entity.Password = HashPasswordForStorage(input.password.Trim(), entity.Secretkey); + entity.Mdxx = input.mdxx; var headIcon = input.headIcon.Split('/').ToList().Last(); if (string.IsNullOrEmpty(headIcon)) @@ -680,7 +706,7 @@ namespace NCC.System.Service.Permission _ = entity ?? throw NCCException.Oh(ErrorCode.D1002); - var password = MD5Encryption.Encrypt(input.userPassword + entity.Secretkey); + var password = HashPasswordForStorage(input.userPassword, entity.Secretkey); var isOk = await _userRepository.Context.Updateable().SetColumns(it => new UserEntity() { @@ -798,7 +824,9 @@ namespace NCC.System.Service.Permission [NonAction] public async Task GetInfoByAccount(string account) { - return await _userRepository.FirstOrDefaultAsync(u => u.Account == account || u.MobilePhone == account && u.DeleteMark == null); + // 注意:必须同时过滤 DeleteMark,否则同账号存在“已删除历史记录”时会取错 Secretkey,导致登录永远校验失败 + return await _userRepository.FirstOrDefaultAsync(u => + (u.Account == account || u.MobilePhone == account) && u.DeleteMark == null); } /// @@ -930,6 +958,38 @@ namespace NCC.System.Service.Permission var ids = PositionIds.Split(","); return await _positionRepository.Entities.In(it => it.Id, ids).Select(it => new { id = it.Id, name = it.FullName }).MergeTable().Select().ToListAsync(); } + + /// + /// 用户密码入库哈希(与重置密码、个人修改密码、OAuth 登录一致)。 + /// + /// 明文密码 + /// 用户秘钥 + /// 可写入 的哈希值 + private static string HashPasswordForStorage(string passwordOrMd5, string secretkey) + { + // 系统密码入库口径(与 OAuth 登录一致): + // - 登录/改密/重置密码:前端通常传 MD5(明文),服务端再做 MD5(MD5值 + Secretkey) + // - 新建用户:本次新增支持传“明文 password”,这里自动先做一层 MD5,再按同口径入库 + var p = (passwordOrMd5 ?? "").Trim(); + if (string.IsNullOrEmpty(p)) return MD5Encryption.Encrypt(MD5Encryption.Encrypt(CommonConst.DEFAULT_PASSWORD) + secretkey); + + static bool LooksLikeMd5(string s) + { + if (s.Length != 32) return false; + foreach (var ch in s) + { + var isHex = (ch >= '0' && ch <= '9') + || (ch >= 'a' && ch <= 'f') + || (ch >= 'A' && ch <= 'F'); + if (!isHex) return false; + } + return true; + } + + var md5Once = LooksLikeMd5(p) ? p : MD5Encryption.Encrypt(p); + return MD5Encryption.Encrypt(md5Once + secretkey); + } + #endregion } } \ No newline at end of file diff --git a/Antis.Erp.Plat/netcore/src/Modularity/VisualDev/NCC.VisualDev/DashboardService.cs b/Antis.Erp.Plat/netcore/src/Modularity/VisualDev/NCC.VisualDev/DashboardService.cs index 1ae2c10..6c3b6b9 100755 --- a/Antis.Erp.Plat/netcore/src/Modularity/VisualDev/NCC.VisualDev/DashboardService.cs +++ b/Antis.Erp.Plat/netcore/src/Modularity/VisualDev/NCC.VisualDev/DashboardService.cs @@ -26,6 +26,7 @@ namespace NCC.VisualDev private readonly IUserManager _userManager; private readonly IDictionaryDataService _dictionaryDataService; private readonly SqlSugarScope _db; + private static bool _wtFysrdAuditColsEnsured; /// /// 初始化一个类型的新实例 @@ -344,6 +345,8 @@ namespace NCC.VisualDev /// private async Task> GetExtendBillTodoListAsync(string userId, string userAccount) { + EnsureWtFysrdAuditColumnsForTodo(); + EnsureWtYskzjjsAuditColumnsForTodo(); var list = new List(); list.AddRange(await QueryWtXsckdAuditTodosAsync("同价调拨单", userId, userAccount, null)); // 仅单据来源为「后台」或未标记(ly 空)的销售出库单进待办;备注「抖音订单:」同后端免审规则 @@ -361,12 +364,160 @@ namespace NCC.VisualDev list.AddRange(await QueryWtXsckdAuditTodosAsync("委托代销退货单", userId, userAccount, null)); list.AddRange(await QueryWtXsckdAuditTodosAsync("委托代销结算单", userId, userAccount, null)); list.AddRange(await QueryWtTjdAuditTodosAsync(userId, userAccount)); + list.AddRange(await QueryWtFysrdQtsrAuditTodosAsync(userId, userAccount)); + list.AddRange(await QueryWtYskzjjsQtAuditTodosAsync(userId, userAccount)); return list.OrderByDescending(x => x.creatorTime).ToList(); } + /// wt_shrysz.djmcWtFysrdWorkflowHelper.BillName 一致 + private const string FysrdQtsrTodoBillName = "其他收入单"; + + /// + /// 待办 SQL 依赖列存在;与 Extend 内 WtFysrdWorkflowHelper.EnsureFysrdAuditColumns 保持一致 + /// + private void EnsureWtFysrdAuditColumnsForTodo() + { + if (_wtFysrdAuditColsEnsured) return; + lock (typeof(DashboardService)) + { + if (_wtFysrdAuditColsEnsured) return; + try + { + if (!_db.DbMaintenance.IsAnyTable("wt_fysrd")) { _wtFysrdAuditColsEnsured = true; return; } + var cols = _db.DbMaintenance.GetColumnInfosByTableName("wt_fysrd"); + var names = cols.Select(c => c.DbColumnName.ToLowerInvariant()).ToHashSet(); + if (!names.Contains("djzt")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `djzt` varchar(32) NULL COMMENT '单据状态'"); + if (!names.Contains("spbz")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `spbz` text NULL COMMENT '审批备注'"); + if (!names.Contains("shr")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `shr` varchar(64) NULL COMMENT '审核人'"); + if (!names.Contains("shr1")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `shr1` varchar(500) NULL COMMENT '一级审核人'"); + if (!names.Contains("shr2")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `shr2` varchar(500) NULL COMMENT '二级审核人'"); + if (!names.Contains("wldw")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_fysrd` ADD COLUMN `wldw` varchar(64) NULL COMMENT '往来单位'"); + } + catch (Exception ex) { Console.WriteLine($"EnsureWtFysrdAuditColumnsForTodo: {ex.Message}"); } + _wtFysrdAuditColsEnsured = true; + } + } + /// wt_shrysz.djmcWtTjdWorkflowHelper.BillName 一致 private const string TjdTodoBillName = "商品调价单"; + /// wt_shrysz.djmc、其他应收单 djlx 一致 + private const string YskzjjsQtTodoBillName = "其他应收单"; + + private static bool _wtYskzjjsAuditColsEnsured; + + /// + /// 待办 SQL 依赖列存在;与 Extend 内 WtYskzjjsService 的列映射保持一致 + /// + private void EnsureWtYskzjjsAuditColumnsForTodo() + { + if (_wtYskzjjsAuditColsEnsured) return; + lock (typeof(DashboardService)) + { + if (_wtYskzjjsAuditColsEnsured) return; + try + { + if (!_db.DbMaintenance.IsAnyTable("wt_yskzjjs")) { _wtYskzjjsAuditColsEnsured = true; return; } + var cols = _db.DbMaintenance.GetColumnInfosByTableName("wt_yskzjjs"); + var names = cols.Select(c => c.DbColumnName.ToLowerInvariant()).ToHashSet(); + if (!names.Contains("djzt")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `djzt` varchar(32) NULL COMMENT '单据状态'"); + if (!names.Contains("spbz")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `spbz` text NULL COMMENT '审批备注'"); + if (!names.Contains("shr")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `shr` varchar(64) NULL COMMENT '审核人'"); + if (!names.Contains("shr1")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `shr1` varchar(500) NULL COMMENT '一级审核人'"); + if (!names.Contains("shr2")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `shr2` varchar(500) NULL COMMENT '二级审核人'"); + if (!names.Contains("wldw")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `wldw` varchar(64) NULL COMMENT '往来单位'"); + if (!names.Contains("djlx")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `djlx` varchar(64) NULL COMMENT '单据类型'"); + if (!names.Contains("djrq")) + _db.Ado.ExecuteCommand( + "ALTER TABLE `wt_yskzjjs` ADD COLUMN `djrq` datetime NULL COMMENT '单据日期'"); + } + catch (Exception ex) { Console.WriteLine($"EnsureWtYskzjjsAuditColumnsForTodo: {ex.Message}"); } + _wtYskzjjsAuditColsEnsured = true; + } + } + + /// + /// 其他收入单待办(wt_fysrd + wt_shrysz) + /// + private async Task> QueryWtFysrdQtsrAuditTodosAsync(string userId, string userAccount) + { + var result = new List(); + const string sql = @" +SELECT d.F_Id AS id, d.djrq, d.djzt, + IFNULL(NULLIF(TRIM(s.shr1), ''), s.shr) AS shr1_eff, + IFNULL(NULLIF(TRIM(s.shr2), ''), s.shr) AS shr2_eff +FROM wt_fysrd d +LEFT JOIN wt_shrysz s ON TRIM(s.djmc) = @billName +WHERE (d.F_Id LIKE 'QT%' OR TRIM(IFNULL(d.djlx,'')) IN ('其他收入单','其它收入单','其他收入','其它收入')) + AND IFNULL(d.djzt, '') <> '草稿' + AND IFNULL(d.djzt, '') <> '已审核' + AND IFNULL(d.djzt, '') <> '审核不通过' + AND ( + d.djzt IN ('待审核', '一级已审', '待二级', '一级已审/待二级') + OR d.djzt IS NULL + OR TRIM(IFNULL(d.djzt, '')) = '' + ) +ORDER BY d.djrq DESC"; + + var rows = await _db.Ado.SqlQueryAsync(sql, new { billName = FysrdQtsrTodoBillName }); + if (rows == null || rows.Count == 0) return result; + + foreach (var row in rows) + { + var status = Convert.ToString(row.djzt)?.Trim() ?? string.Empty; + var level1 = status == "待审核" || string.IsNullOrEmpty(status); + var level2 = status == "一级已审" || status == "待二级" || status == "一级已审/待二级"; + if (!level1 && !level2) continue; + + var configUsers = level1 ? Convert.ToString(row.shr1_eff) : Convert.ToString(row.shr2_eff); + if (!string.IsNullOrWhiteSpace(configUsers) && !IsInAuditUsers(configUsers, userId, userAccount)) + continue; + + DateTime? createTime = null; + var rawTime = Convert.ToString(row.djrq); + if (DateTime.TryParse(rawTime, out DateTime dt)) + createTime = dt; + + var id = Convert.ToString(row.id); + var levelText = level1 ? "一级审核待办" : "二级审核待办"; + result.Add(new FlowTodoOutput + { + id = id, + fullName = $"{FysrdQtsrTodoBillName} {id} - {levelText}", + creatorTime = createTime, + billType = FysrdQtsrTodoBillName + }); + } + + return result; + } + /// /// 商品调价单待办(wt_tjd + wt_shrysz) /// @@ -424,6 +575,62 @@ ORDER BY d.djrq DESC"; } /// + /// 其他应收单待办(wt_yskzjjs + wt_shrysz) + /// + private async Task> QueryWtYskzjjsQtAuditTodosAsync(string userId, string userAccount) + { + var result = new List(); + const string sql = @" +SELECT d.F_Id AS id, d.djrq, d.djzt, + IFNULL(NULLIF(TRIM(s.shr1), ''), s.shr) AS shr1_eff, + IFNULL(NULLIF(TRIM(s.shr2), ''), s.shr) AS shr2_eff +FROM wt_yskzjjs d +LEFT JOIN wt_shrysz s ON TRIM(s.djmc) = @billName +WHERE TRIM(IFNULL(d.djlx,'')) IN ('其他应收单','其他应收款') + AND IFNULL(d.djzt, '') <> '草稿' + AND IFNULL(d.djzt, '') <> '已审核' + AND IFNULL(d.djzt, '') <> '审核不通过' + AND ( + d.djzt IN ('待审核', '一级已审', '待二级', '一级已审/待二级') + OR d.djzt IS NULL + OR TRIM(IFNULL(d.djzt, '')) = '' + ) +ORDER BY d.djrq DESC"; + + var rows = await _db.Ado.SqlQueryAsync(sql, new { billName = YskzjjsQtTodoBillName }); + if (rows == null || rows.Count == 0) return result; + + foreach (var row in rows) + { + var status = Convert.ToString(row.djzt)?.Trim() ?? string.Empty; + var level1 = status == "待审核" || string.IsNullOrEmpty(status); + var level2 = status == "一级已审" || status == "待二级" || status == "一级已审/待二级"; + if (!level1 && !level2) continue; + + var configUsers = level1 ? Convert.ToString(row.shr1_eff) : Convert.ToString(row.shr2_eff); + if (!string.IsNullOrWhiteSpace(configUsers) && !IsInAuditUsers(configUsers, userId, userAccount)) + continue; + + DateTime? createTime = null; + var rawTime = Convert.ToString(row.djrq); + if (DateTime.TryParse(rawTime, out DateTime dt)) + createTime = dt; + + var id = Convert.ToString(row.id); + var levelText = level1 ? "一级审核待办" : "二级审核待办"; + result.Add(new FlowTodoOutput + { + id = id, + fullName = $"{YskzjjsQtTodoBillName} {id} - {levelText}", + creatorTime = createTime, + billType = YskzjjsQtTodoBillName + }); + } + + return result; + } + + /// /// 按单据类型查询待审核/待二级单据,并按 shr1/shr2(或 shr)过滤当前用户 /// /// 与 wt_xsckd.djlx、wt_shrysz.djmc 一致 diff --git a/Antis.Erp.Plat/sy/home.html b/Antis.Erp.Plat/sy/home.html index dc3dd0f..6421c2b 100755 --- a/Antis.Erp.Plat/sy/home.html +++ b/Antis.Erp.Plat/sy/home.html @@ -475,7 +475,7 @@ - 收银台 + {{ posCashierHeaderTitle }}